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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] -- 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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/485] 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 0fcc379ede03fdfa8d82272b4582566b8a1f3b58 Mon Sep 17 00:00:00 2001 From: 131stGutts Date: Sat, 12 Jan 2019 21:35:05 +0100 Subject: [PATCH 134/485] Fix bug when 2 humans are in the same group of planes If more than one human is in a plane's group, the menu bug and a lot of PSEUDOATC menu are shown. The new menu structure is: - F10 - PSEUDOATC - - For achieving that goal, the self structure has changed from self.player[GID] to self.group[GID].player[UID]. Doing that way, if the last player pit leave the group this functionality do not generate a nil error anymore. Some text have been changed to indicate CallSign or PlayerName. --- .../Moose/Functional/PseudoATC.lua | 313 ++++++++++-------- 1 file changed, 182 insertions(+), 131 deletions(-) diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua index bfb458078..17f30ac98 100644 --- a/Moose Development/Moose/Functional/PseudoATC.lua +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -81,7 +81,7 @@ -- @field #PSEUDOATC PSEUDOATC={ ClassName = "PSEUDOATC", - player={}, + group={}, Debug=false, mdur=30, mrefresh=120, @@ -383,16 +383,23 @@ function PSEUDOATC:PlayerEntered(unit) local PlayerName=unit:GetPlayerName() local UnitName=unit:GetName() local CallSign=unit:GetCallsign() + local UID=unit:GetDCSObject():getID() + + if not self.group[GID] then + self.group[GID]={} + self.group[GID].player={} + end + -- Init player table. - self.player[GID]={} - self.player[GID].group=group - self.player[GID].unit=unit - self.player[GID].groupname=GroupName - self.player[GID].unitname=UnitName - self.player[GID].playername=PlayerName - self.player[GID].callsign=CallSign - self.player[GID].waypoints=group:GetTaskRoute() + self.group[GID].player[UID]={} + self.group[GID].player[UID].group=group + self.group[GID].player[UID].unit=unit + self.group[GID].player[UID].groupname=GroupName + self.group[GID].player[UID].unitname=UnitName + self.group[GID].player[UID].playername=PlayerName + self.group[GID].player[UID].callsign=CallSign + self.group[GID].player[UID].waypoints=group:GetTaskRoute() -- Info message. local text=string.format("Player %s entered unit %s of group %s (id=%d).", PlayerName, UnitName, GroupName, GID) @@ -400,19 +407,26 @@ function PSEUDOATC:PlayerEntered(unit) MESSAGE:New(text, 30):ToAllIf(self.Debug) -- Create main F10 menu, i.e. "F10/Pseudo ATC" - self.player[GID].menu_main=missionCommands.addSubMenuForGroup(GID, "Pseudo ATC") + local countPlayerInGroup = 0 + for _ in pairs(self.group[GID].player) do countPlayerInGroup = countPlayerInGroup + 1 end + if countPlayerInGroup <= 1 then + self.group[GID].menu_main=missionCommands.addSubMenuForGroup(GID, "Pseudo ATC") + end + + -- Create/update custom menu for player + self:MenuCreatePlayer(GID,UID) -- Create/update list of nearby airports. - self:LocalAirports(GID) + self:LocalAirports(GID,UID) -- Create submenu of local airports. - self:MenuAirports(GID) + self:MenuAirports(GID,UID) -- Create submenu Waypoints. - self:MenuWaypoints(GID) + self:MenuWaypoints(GID,UID) -- Start scheduler to refresh the F10 menues. - self.player[GID].scheduler, self.player[GID].schedulerid=SCHEDULER:New(nil, self.MenuRefresh, {self, GID}, self.mrefresh, self.mrefresh) + self.group[GID].player[UID].scheduler, self.group[GID].player[UID].schedulerid=SCHEDULER:New(nil, self.MenuRefresh, {self, GID, UID}, self.mrefresh, self.mrefresh) end @@ -425,24 +439,23 @@ function PSEUDOATC:PlayerLanded(unit, place) -- Gather some information. local group=unit:GetGroup() - local id=group:GetID() - local PlayerName=self.player[id].playername - local Callsign=self.player[id].callsign - local UnitName=self.player[id].unitname - local GroupName=self.player[id].groupname - local CallSign=self.player[id].callsign + local GID=group:GetID() + local UID=unit:GetDCSObject():getID() + local PlayerName=self.group[GID].player[UID].playername + local UnitName=self.group[GID].player[UID].unitname + local GroupName=self.group[GID].player[UID].groupname -- Debug message. - local text=string.format("Player %s in unit %s of group %s (id=%d) landed at %s.", PlayerName, UnitName, GroupName, id, place) + local text=string.format("Player %s in unit %s of group %s (id=%d) landed at %s.", PlayerName, UnitName, GroupName, GID, place) self:T(PSEUDOATC.id..text) MESSAGE:New(text, 30):ToAllIf(self.Debug) -- Stop altitude reporting timer if its activated. - self:AltitudeTimerStop(id) + self:AltitudeTimerStop(GID,UID) -- Welcome message. if place and self.chatty then - local text=string.format("Touchdown! Welcome to %s. Have a nice day!", place) + local text=string.format("Touchdown! Welcome to %s pilot %s. Have a nice day!", place,PlayerName) MESSAGE:New(text, self.mdur):ToGroup(group) end @@ -457,15 +470,15 @@ function PSEUDOATC:PlayerTakeOff(unit, place) -- Gather some information. local group=unit:GetGroup() - local id=group:GetID() - local PlayerName=self.player[id].playername - local Callsign=self.player[id].callsign - local UnitName=self.player[id].unitname - local GroupName=self.player[id].groupname - local CallSign=self.player[id].callsign + local GID=group:GetID() + local UID=unit:GetDCSObject():getID() + local PlayerName=self.group[GID].player[UID].playername + local CallSign=self.group[GID].player[UID].callsign + local UnitName=self.group[GID].player[UID].unitname + local GroupName=self.group[GID].player[UID].groupname -- Debug message. - local text=string.format("Player %s in unit %s of group %s (id=%d) took off at %s.", PlayerName, UnitName, GroupName, id, place) + local text=string.format("Player %s in unit %s of group %s (id=%d) took off at %s.", PlayerName, UnitName, GroupName, GID, place) self:T(PSEUDOATC.id..text) MESSAGE:New(text, 30):ToAllIf(self.Debug) @@ -485,30 +498,44 @@ function PSEUDOATC:PlayerLeft(unit) -- Get id. local group=unit:GetGroup() - local id=group:GetID() + local GID=group:GetID() + local UID=unit:GetDCSObject():getID() - if self.player[id] then + if self.group[GID].player[UID] then + local PlayerName=self.group[GID].player[UID].playername + local CallSign=self.group[GID].player[UID].callsign + local UnitName=self.group[GID].player[UID].unitname + local GroupName=self.group[GID].player[UID].groupname -- Debug message. - local text=string.format("Player %s (callsign %s) of group %s just left unit %s.", self.player[id].playername, self.player[id].callsign, self.player[id].groupname, self.player[id].unitname) + local text=string.format("Player %s (callsign %s) of group %s just left unit %s.", PlayerName, CallSign, GroupName, UnitName) self:T(PSEUDOATC.id..text) MESSAGE:New(text, 30):ToAllIf(self.Debug) -- Stop scheduler for menu updates - if self.player[id].schedulerid then - self.player[id].scheduler:Stop(self.player[id].schedulerid) + if self.group[GID].player[UID].schedulerid then + self.group[GID].player[UID].scheduler:Stop(self.group[GID].player[UID].schedulerid) end -- Stop scheduler for reporting alt if it runs. - self:AltitudeTimerStop(id) + self:AltitudeTimerStop(GID,UID) + -- Remove own menu. + if self.group[GID].player[UID].menu_own then + missionCommands.removeItemForGroup(GID,self.group[GID].player[UID].menu_own) + end -- Remove main menu. - if self.player[id].menu_main then - missionCommands.removeItem(self.player[id].menu_main) + -- WARNING: Remove only if last human element of group + + local countPlayerInGroup = 0 + for _ in pairs(self.group[GID].player) do countPlayerInGroup = countPlayerInGroup + 1 end + + if self.group[GID].menu_main and countPlayerInGroup==1 then + missionCommands.removeItemForGroup(GID,self.group[GID].menu_main) end -- Remove player array. - self.player[id]=nil + self.group[GID].player[UID]=nil end end @@ -518,80 +545,94 @@ end --- Refreshes all player menues. -- @param #PSEUDOATC self. --- @param #number id Group id of player unit. -function PSEUDOATC:MenuRefresh(id) - self:F({id=id}) - +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuRefresh(GID,UID) + self:F({GID=GID,UID=UID}) -- Debug message. - local text=string.format("Refreshing menues for player %s in group %s.", self.player[id].playername, self.player[id].groupname) + local text=string.format("Refreshing menues for player %s in group %s.", self.group[GID].player[UID].playername, self.group[GID].player[UID].groupname) self:T(PSEUDOATC.id..text) MESSAGE:New(text,30):ToAllIf(self.Debug) -- Clear menu. - self:MenuClear(id) + self:MenuClear(GID,UID) -- Create list of nearby airports. - self:LocalAirports(id) + self:LocalAirports(GID,UID) -- Create submenu Local Airports. - self:MenuAirports(id) + self:MenuAirports(GID,UID) -- Create submenu Waypoints etc. - self:MenuWaypoints(id) + self:MenuWaypoints(GID,UID) end +--- Create player menus. +-- @param #PSEUDOATC self. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuCreatePlayer(GID,UID) + self:F({GID=GID,UID=UID}) + -- Table for menu entries. + local PlayerName=self.group[GID].player[UID].playername + self.group[GID].player[UID].menu_own=missionCommands.addSubMenuForGroup(GID, PlayerName, self.group[GID].menu_main) +end + + --- Clear player menus. -- @param #PSEUDOATC self. --- @param #number id Group id of player unit. -function PSEUDOATC:MenuClear(id) - self:F(id) +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuClear(GID,UID) + self:F({GID=GID,UID=UID}) -- Debug message. - local text=string.format("Clearing menus for player %s in group %s.", self.player[id].playername, self.player[id].groupname) + local text=string.format("Clearing menus for player %s in group %s.", self.group[GID].player[UID].playername, self.group[GID].player[UID].groupname) self:T(PSEUDOATC.id..text) MESSAGE:New(text,30):ToAllIf(self.Debug) -- Delete Airports menu. - if self.player[id].menu_airports then - missionCommands.removeItemForGroup(id, self.player[id].menu_airports) - self.player[id].menu_airports=nil + if self.group[GID].player[UID].menu_airports then + missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_airports) + self.group[GID].player[UID].menu_airports=nil else self:T2(PSEUDOATC.id.."No airports to clear menus.") end -- Delete waypoints menu. - if self.player[id].menu_waypoints then - missionCommands.removeItemForGroup(id, self.player[id].menu_waypoints) - self.player[id].menu_waypoints=nil + if self.group[GID].player[UID].menu_waypoints then + missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_waypoints) + self.group[GID].player[UID].menu_waypoints=nil end -- Delete report alt until touchdown menu command. - if self.player[id].menu_reportalt then - missionCommands.removeItemForGroup(id, self.player[id].menu_reportalt) - self.player[id].menu_reportalt=nil + if self.group[GID].player[UID].menu_reportalt then + missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_reportalt) + self.group[GID].player[UID].menu_reportalt=nil end -- Delete request current alt menu command. - if self.player[id].menu_requestalt then - missionCommands.removeItemForGroup(id, self.player[id].menu_requestalt) - self.player[id].menu_requestalt=nil + if self.group[GID].player[UID].menu_requestalt then + missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_requestalt) + self.group[GID].player[UID].menu_requestalt=nil end end --- Create "F10/Pseudo ATC/Local Airports/Airport Name/" menu items each containing weather report and BR request. -- @param #PSEUDOATC self --- @param #number id Group id of player unit for which menues are created. -function PSEUDOATC:MenuAirports(id) - self:F(id) +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuAirports(GID,UID) + self:F({GID=GID,UID=UID}) -- Table for menu entries. - self.player[id].menu_airports=missionCommands.addSubMenuForGroup(id, "Local Airports", self.player[id].menu_main) + self.group[GID].player[UID].menu_airports=missionCommands.addSubMenuForGroup(GID, "Local Airports", self.group[GID].player[UID].menu_own) local i=0 - for _,airport in pairs(self.player[id].airports) do + for _,airport in pairs(self.group[GID].player[UID].airports) do i=i+1 if i > 10 then @@ -603,37 +644,38 @@ function PSEUDOATC:MenuAirports(id) local pos=AIRBASE:FindByName(name):GetCoordinate() --F10menu_ATC_airports[ID][name] = missionCommands.addSubMenuForGroup(ID, name, F10menu_ATC) - local submenu=missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_airports) + local submenu=missionCommands.addSubMenuForGroup(GID, name, self.group[GID].player[UID].menu_airports) -- Create menu reporting commands - missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, name) - missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, name) + missionCommands.addCommandForGroup(GID, "Weather Report", submenu, self.ReportWeather, self, GID, UID, pos, name) + missionCommands.addCommandForGroup(GID, "Request BR", submenu, self.ReportBR, self, GID, UID, pos, name) -- Debug message. - self:T(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, id)) + self:T(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, GID)) end end --- Create "F10/Pseudo ATC/Waypoints/ menu items. -- @param #PSEUDOATC self --- @param #number id Group id of player unit for which menues are created. -function PSEUDOATC:MenuWaypoints(id) - self:F(id) +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuWaypoints(GID, UID) + self:F({GID=GID, UID=UID}) -- Player unit and callsign. - local unit=self.player[id].unit --Wrapper.Unit#UNIT - local callsign=self.player[id].callsign +-- local unit=self.group[GID].player[UID].unit --Wrapper.Unit#UNIT + local callsign=self.group[GID].player[UID].callsign -- Debug info. - self:T(PSEUDOATC.id..string.format("Creating waypoint menu for %s (ID %d).", callsign, id)) + self:T(PSEUDOATC.id..string.format("Creating waypoint menu for %s (ID %d).", callsign, GID)) - if #self.player[id].waypoints>0 then + if #self.group[GID].player[UID].waypoints>0 then -- F10/PseudoATC/Waypoints - self.player[id].menu_waypoints=missionCommands.addSubMenuForGroup(id, "Waypoints", self.player[id].menu_main) + self.group[GID].player[UID].menu_waypoints=missionCommands.addSubMenuForGroup(GID, "Waypoints", self.group[GID].player[UID].menu_own) local j=0 - for i, wp in pairs(self.player[id].waypoints) do + for i, wp in pairs(self.group[GID].player[UID].waypoints) do -- Increase counter j=j+1 @@ -647,16 +689,16 @@ function PSEUDOATC:MenuWaypoints(id) local name=string.format("Waypoint %d", i-1) -- "F10/PseudoATC/Waypoints/Waypoint X" - local submenu=missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_waypoints) + local submenu=missionCommands.addSubMenuForGroup(GID, name, self.group[GID].player[UID].menu_waypoints) -- Menu commands for each waypoint "F10/PseudoATC/My Aircraft (callsign)/Waypoints/Waypoint X/" - missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, name) - missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, name) + missionCommands.addCommandForGroup(GID, "Weather Report", submenu, self.ReportWeather, self, GID, UID, pos, name) + missionCommands.addCommandForGroup(GID, "Request BR", submenu, self.ReportBR, self, GID, UID, pos, name) end end - self.player[id].menu_reportalt = missionCommands.addCommandForGroup(id, "Talk me down", self.player[id].menu_main, self.AltidudeTimerToggle, self, id) - self.player[id].menu_requestalt = missionCommands.addCommandForGroup(id, "Request altitude", self.player[id].menu_main, self.ReportHeight, self, id) + self.group[GID].player[UID].menu_reportalt = missionCommands.addCommandForGroup(GID, "Talk me down", self.group[GID].player[UID].menu_own, self.AltidudeTimerToggle, self, GID, UID) + self.group[GID].player[UID].menu_requestalt = missionCommands.addCommandForGroup(GID, "Request altitude", self.group[GID].player[UID].menu_own, self.ReportHeight, self, GID, UID) end ----------------------------------------------------------------------------------------------------------------------------------------- @@ -664,14 +706,15 @@ end --- Weather Report. Report pressure QFE/QNH, temperature, wind at certain location. -- @param #PSEUDOATC self --- @param #number id Group id to which the report is delivered. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. -- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. -- @param #string location Name of the location at which the pressure is measured. -function PSEUDOATC:ReportWeather(id, position, location) - self:F({id=id, position=position, location=location}) +function PSEUDOATC:ReportWeather(GID, UID, position, location) + self:F({GID=GID, UID=UID, position=position, location=location}) -- Player unit system settings. - local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS local text=string.format("Local weather at %s:\n", location) @@ -723,23 +766,24 @@ function PSEUDOATC:ReportWeather(id, position, location) end -- Message text. - local text=text..string.format("Wind from %s at %s (%s).", Ds, Vs, Bd) + local text=text..string.format("%s, Wind from %s at %s (%s).", self.group[GID].player[UID].playername, Ds, Vs, Bd) -- Send message - self:_DisplayMessageToGroup(self.player[id].unit, text, self.mdur, true) + self:_DisplayMessageToGroup(self.group[GID].player[UID].unit, text, self.mdur, true) end --- Report absolute bearing and range form player unit to airport. -- @param #PSEUDOATC self --- @param #number id Group id to the report is delivered. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. -- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. -- @param #string location Name of the location at which the pressure is measured. -function PSEUDOATC:ReportBR(id, position, location) - self:F({id=id, position=position, location=location}) +function PSEUDOATC:ReportBR(GID, UID, position, location) + self:F({GID=GID, UID=UID, position=position, location=location}) -- Current coordinates. - local unit=self.player[id].unit --Wrapper.Unit#UNIT + local unit=self.group[GID].player[UID].unit --Wrapper.Unit#UNIT local coord=unit:GetCoordinate() -- Direction vector from current position (coord) to target (position). @@ -752,7 +796,7 @@ function PSEUDOATC:ReportBR(id, position, location) local Bs=string.format('%03d°', angle) -- Settings. - local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS local Rs=string.format("%.1f NM", UTILS.MetersToNM(range)) @@ -769,12 +813,13 @@ end --- Report altitude above ground level of player unit. -- @param #PSEUDOATC self --- @param #number id Group id to the report is delivered. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. -- @param #number dt (Optional) Duration the message is displayed. -- @param #boolean _clear (Optional) Clear previouse messages. -- @return #number Altitude above ground. -function PSEUDOATC:ReportHeight(id, dt, _clear) - self:F({id=id, dt=dt}) +function PSEUDOATC:ReportHeight(GID, UID, dt, _clear) + self:F({GID=GID, UID=UID, dt=dt}) local dt = dt or self.mdur if _clear==nil then @@ -791,7 +836,7 @@ function PSEUDOATC:ReportHeight(id, dt, _clear) end -- Get height AGL. - local unit=self.player[id].unit --Wrapper.Unit#UNIT + local unit=self.group[GID].player[UID].unit --Wrapper.Unit#UNIT if unit and unit:IsAlive() then @@ -800,7 +845,7 @@ function PSEUDOATC:ReportHeight(id, dt, _clear) local callsign=unit:GetCallsign() -- Settings. - local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS -- Height string. local Hs=string.format("%d ft", UTILS.MetersToFeet(height)) @@ -817,7 +862,7 @@ function PSEUDOATC:ReportHeight(id, dt, _clear) end -- Send message to player group. - self:_DisplayMessageToGroup(self.player[id].unit,_text, dt,_clear) + self:_DisplayMessageToGroup(self.group[GID].player[UID].unit,_text, dt,_clear) -- Return height return height @@ -830,47 +875,50 @@ end --- Toggle report altitude reporting on/off. -- @param #PSEUDOATC self. --- @param #number id Group id of player unit. -function PSEUDOATC:AltidudeTimerToggle(id) - self:F(id) +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:AltidudeTimerToggle(GID,UID) + self:F({GID=GID, UID=UID}) - if self.player[id].altimerid then + if self.group[GID].player[UID].altimerid then -- If the timer is on, we turn it off. - self:AltitudeTimerStop(id) + self:AltitudeTimerStop(GID, UID) else -- If the timer is off, we turn it on. - self:AltitudeTimeStart(id) + self:AltitudeTimeStart(GID, UID) end end --- Start altitude reporting scheduler. -- @param #PSEUDOATC self. --- @param #number id Group id of player unit. -function PSEUDOATC:AltitudeTimeStart(id) - self:F(id) +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:AltitudeTimeStart(GID, UID) + self:F({GID=GID, UID=UID}) -- Debug info. - self:T(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.", id)) + self:T(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.", UID)) -- Start timer. Altitude is reported every ~3 seconds. - self.player[id].altimer, self.player[id].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, id, 0.1, true}, 1, 3) + self.group[GID].player[UID].altimer, self.group[GID].player[UID].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, GID, UID, 0.1, true}, 1, 3) end --- Stop/destroy DCS scheduler function for reporting altitude. -- @param #PSEUDOATC self. --- @param #number id Group id of player unit. -function PSEUDOATC:AltitudeTimerStop(id) - +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:AltitudeTimerStop(GID, UID) + self:F({GID=GID,UID=UID}) -- Debug info. - self:T(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", id)) + self:T(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", UID)) -- Stop timer. - if self.player[id].altimerid then - self.player[id].altimer:Stop(self.player[id].altimerid) + if self.group[GID].player[UID].altimerid then + self.group[GID].player[UID].altimer:Stop(self.group[GID].player[UID].altimerid) end - self.player[id].altimer=nil - self.player[id].altimerid=nil + self.group[GID].player[UID].altimer=nil + self.group[GID].player[UID].altimerid=nil end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -878,16 +926,17 @@ end --- Create list of nearby airports sorted by distance to player unit. -- @param #PSEUDOATC self --- @param #number id Group id of player unit. -function PSEUDOATC:LocalAirports(id) - self:F(id) +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:LocalAirports(GID, UID) + self:F({GID=GID, UID=UID}) -- Airports table. - self.player[id].airports=nil - self.player[id].airports={} + self.group[GID].player[UID].airports=nil + self.group[GID].player[UID].airports={} -- Current player position. - local pos=self.player[id].unit:GetCoordinate() + local pos=self.group[GID].player[UID].unit:GetCoordinate() -- Loop over coalitions. for i=0,2 do @@ -903,7 +952,7 @@ function PSEUDOATC:LocalAirports(id) local d=q:Get2DDistance(pos) -- Add to table. - table.insert(self.player[id].airports, {distance=d, name=name}) + table.insert(self.group[GID].player[UID].airports, {distance=d, name=name}) end end @@ -914,7 +963,7 @@ function PSEUDOATC:LocalAirports(id) end -- Sort airports table w.r.t. distance to player. - table.sort(self.player[id].airports, compare) + table.sort(self.group[GID].player[UID].airports, compare) end @@ -992,3 +1041,5 @@ function PSEUDOATC:_myname(unitname) return string.format("%s (%s)", csign, pname) end + + From 6b85141a39d881d28c5f6fc6d97db47a3c87cce8 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 13 Jan 2019 00:23:27 +0100 Subject: [PATCH 135/485] 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 0d90eed2700b91d78adcb2d076caeb3d14900ac5 Mon Sep 17 00:00:00 2001 From: 131stGutts Date: Sun, 13 Jan 2019 12:45:21 +0100 Subject: [PATCH 136/485] Implements PersianGulf ATC_Ground supervisor All Persian Gulf airbases and runways added so ATC_Ground supervisor Minor correction to comment for Normandy (saying Caucasus) --- .../Moose/Functional/ATC_Ground.lua | 841 +++++++++++++++++- 1 file changed, 839 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index 259bade99..7c25b6f1a 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -125,6 +125,7 @@ end -- Atc_Ground = ATC_GROUND_CAUCAUS:New() -- Atc_Ground = ATC_GROUND_NEVADA:New() -- Atc_Ground = ATC_GROUND_NORMANDY:New() +-- Atc_Ground = ATC_GROUND_PERSIANGULF:New() -- -- -- Then use one of these methods... -- @@ -190,6 +191,7 @@ end -- Atc_Ground = ATC_GROUND_CAUCAUS:New() -- Atc_Ground = ATC_GROUND_NEVADA:New() -- Atc_Ground = ATC_GROUND_NORMANDY:New() +-- Atc_Ground = ATC_GROUND_PERSIANGULF:New() -- -- -- Then use one of these methods... -- @@ -1557,7 +1559,7 @@ end -- -- --- -- --- The default maximum speed for the airbases at Caucasus is **40 km/h**. Warnings are given if this speed limit is trespassed. +-- The default maximum speed for the airbases at Normandy is **40 km/h**. Warnings are given if this speed limit is trespassed. -- Players will be immediately kicked when driving faster than **100 km/h** on the taxi way. -- -- The ATC\_GROUND\_NORMANDY class monitors the speed of the airplanes at the airbase during taxi. @@ -2315,7 +2317,842 @@ function ATC_GROUND_NORMANDY:New( AirbaseNames ) return self end - + +--- @type ATC_GROUND_PERSIANGULF +-- @extends #ATC_GROUND + + +--- # ATC\_GROUND\_PERSIANGULF, extends @{#ATC_GROUND} +-- +-- The ATC\_GROUND\_PERSIANGULF class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- --- +-- +-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG) +-- +-- --- +-- +-- The default maximum speed for the airbases at Persian Gulf is **50 km/h**. Warnings are given if this speed limit is trespassed. +-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way. +-- +-- The ATC\_GROUND\_PERSIANGULF class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving +-- faster than the maximum allowed speed, the pilot will be kicked. +-- +-- Different airbases have different maximum speeds, according safety regulations. +-- +-- # Airbases monitored +-- +-- The following airbases are monitored at the PersianGulf region. +-- Use the @{Wrapper.Airbase#AIRBASE.PersianGulf} enumeration to select the airbases to be monitored. +-- +-- * `AIRBASE.PersianGulf.Abu_Musa_Island_Airport` +-- * `AIRBASE.PersianGulf.Al_Dhafra_AB` +-- * `AIRBASE.PersianGulf.Al_Maktoum_Intl` +-- * `AIRBASE.PersianGulf.Al_Minhad_AB` +-- * `AIRBASE.PersianGulf.Bandar_Abbas_Intl` +-- * `AIRBASE.PersianGulf.Bandar_Lengeh` +-- * `AIRBASE.PersianGulf.Dubai_Intl` +-- * `AIRBASE.PersianGulf.Fujairah_Intl` +-- * `AIRBASE.PersianGulf.Havadarya` +-- * `AIRBASE.PersianGulf.Kerman_Airport` +-- * `AIRBASE.PersianGulf.Khasab` +-- * `AIRBASE.PersianGulf.Lar_Airbase` +-- * `AIRBASE.PersianGulf.Qeshm_Island` +-- * `AIRBASE.PersianGulf.Sharjah_Intl` +-- * `AIRBASE.PersianGulf.Shiraz_International_Airport` +-- * `AIRBASE.PersianGulf.Sir_Abu_Nuayr` +-- * `AIRBASE.PersianGulf.Sirri_Island` +-- * `AIRBASE.PersianGulf.Tunb_Island_AFB` +-- * `AIRBASE.PersianGulf.Tunb_Kochak` +-- * `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` +-- +-- # Installation +-- +-- ## In Single Player Missions +-- +-- ATC\_GROUND is fully functional in single player. +-- +-- ## In Multi Player Missions +-- +-- ATC\_GROUND is functional in multi player, however ... +-- +-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player. +-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed +-- by Ciribob. +-- +-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight. +-- ATC\_GROUND is communicating with this modified script to kick players! +-- +-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob. +-- +-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock) +-- +-- # Script it! +-- +-- ## 1. ATC_GROUND_PERSIANGULF Constructor +-- +-- Creates a new ATC_GROUND_PERSIANGULF object that will monitor pilots taxiing behaviour. +-- +-- -- This creates a new ATC_GROUND_PERSIANGULF object. +-- +-- -- Monitor for these clients the airbases. +-- AirbasePoliceCaucasus = ATC_GROUND_PERSIANGULF:New() +-- +-- ATC_Ground = ATC_GROUND_PERSIANGULF:New( +-- { AIRBASE.PersianGulf.Kerman_Airport, +-- AIRBASE.PersianGulf.Al_Minhad_AB +-- } +-- ) +-- +-- +-- ## 2. Set various options +-- +-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes. +-- +-- ### 2.1 Speed limit at an airbase. +-- +-- * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour. +-- +-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately. +-- +-- * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour. +-- +-- @field #ATC_GROUND_PERSIANGULF +ATC_GROUND_PERSIANGULF = { + ClassName = "ATC_GROUND_PERSIANGULF", + Airbases = { + [AIRBASE.PersianGulf.Abu_Musa_Island_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-122813.71002344,["x"]=-31689.936027827,}, + [2]={["y"]=-122827.82488722,["x"]=-31590.105445836,}, + [3]={["y"]=-122769.5689949,["x"]=-31583.176330891,}, + [4]={["y"]=-122726.96776968,["x"]=-31614.998932862,}, + [5]={["y"]=-121293.92414543,["x"]=-31467.947715689,}, + [6]={["y"]=-121296.4904843,["x"]=-31432.018971528,}, + [7]={["y"]=-121236.18152088,["x"]=-31424.576588809,}, + [8]={["y"]=-121190.50068902,["x"]=-31458.452261875,}, + [9]={["y"]=-119839.83654246,["x"]=-31319.356695194,}, + [10]={["y"]=-119824.69514313,["x"]=-31423.293419374,}, + [11]={["y"]=-119886.80054375,["x"]=-31430.22253432,}, + [12]={["y"]=-119932.22474173,["x"]=-31395.320325706,}, + [13]={["y"]=-122813.9472789,["x"]=-31689.81193251,}, + }, + }, + }, + [AIRBASE.PersianGulf.Abu_Musa_Island_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-174672.06004916,["x"]=-209880.97145616,}, + [2]={["y"]=-174705.15693282,["x"]=-209923.15131918,}, + [3]={["y"]=-171819.05380065,["x"]=-212172.84298281,}, + [4]={["y"]=-171785.09826475,["x"]=-212129.87417284,}, + [5]={["y"]=-174671.96413454,["x"]=-209880.52453983,}, + }, + [2] = { + [1]={["y"]=-174351.95872272,["x"]=-211813.88516693,}, + [2]={["y"]=-174381.29169939,["x"]=-211851.81242636,}, + [3]={["y"]=-171493.65648904,["x"]=-214102.92235002,}, + [4]={["y"]=-171464.99693831,["x"]=-214062.78788361,}, + [5]={["y"]=-174351.8628081,["x"]=-211813.4382506,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Maktoum_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=-111879.49046471,["x"]=-138953.80105841,}, + [2]={["y"]=-111917.23447224,["x"]=-139018.2804046,}, + [3]={["y"]=-108092.98121312,["x"]=-141406.67838426,}, + [4]={["y"]=-108052.34416748,["x"]=-141341.82058294,}, + [5]={["y"]=-111879.5412879,["x"]=-138952.87693763,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Minhad_AB] = { + PointsRunways = { + [1] = { + [1]={["y"]=-91070.628933035,["x"]=-125989.64095162,}, + [2]={["y"]=-91072.346560159,["x"]=-126040.59722299,}, + [3]={["y"]=-87098.282779771,["x"]=-126039.41747017,}, + [4]={["y"]=-87099.632735396,["x"]=-125991.26905291,}, + [5]={["y"]=-91071.031270042,["x"]=-125987.44617225,}, + }, + }, + }, + [AIRBASE.PersianGulf.Bandar_Abbas_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=12988.484058788,["x"]=113979.99250505,}, + [2]={["y"]=13037.8836239,["x"]=113952.60241152,}, + [3]={["y"]=14877.313199902,["x"]=117414.37833333,}, + [4]={["y"]=14828.777486364,["x"]=117439.06043783,}, + [5]={["y"]=12988.939584604,["x"]=113979.52494386,}, + }, + [2] = { + [1]={["y"]=13203.406014284,["x"]=113848.44907555,}, + [2]={["y"]=13258.268500181,["x"]=113818.47303925,}, + [3]={["y"]=15315.015323566,["x"]=117694.27156647,}, + [4]={["y"]=15264.815746383,["x"]=117725.22168173,}, + [5]={["y"]=13203.861540099,["x"]=113847.98151436,}, + }, + }, + }, + [AIRBASE.PersianGulf.Bandar_Lengeh] = { + PointsRunways = { + [1] = { + [1]={["y"]=-142373.15541415,["x"]=41364.94047809,}, + [2]={["y"]=-142363.30071107,["x"]=41298.112282592,}, + [3]={["y"]=-142217.57151662,["x"]=41320.35666061,}, + [4]={["y"]=-142213.00856728,["x"]=41291.838227254,}, + [5]={["y"]=-142131.44584788,["x"]=41301.534494595,}, + [6]={["y"]=-142132.58658522,["x"]=41323.778872613,}, + [7]={["y"]=-142123.17550221,["x"]=41336.041798956,}, + [8]={["y"]=-139580.45381288,["x"]=41711.022304533,}, + [9]={["y"]=-139590.04241918,["x"]=41778.350996659,}, + [10]={["y"]=-139732.41237808,["x"]=41757.089304408,}, + [11]={["y"]=-139736.7897853,["x"]=41785.646675372,}, + [12]={["y"]=-139816.41690726,["x"]=41775.641173137,}, + [13]={["y"]=-139816.00001133,["x"]=41754.58792885,}, + [14]={["y"]=-139824.1294819,["x"]=41743.748634761,}, + [15]={["y"]=-142373.20183966,["x"]=41365.161507021,}, + }, + }, + }, + [AIRBASE.PersianGulf.Dubai_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=-89693.511670714,["x"]=-100490.47082052,}, + [2]={["y"]=-89731.488328846,["x"]=-100555.50584758,}, + [3]={["y"]=-85706.437275049,["x"]=-103076.68123933,}, + [4]={["y"]=-85669.519216262,["x"]=-103010.44994755,}, + [5]={["y"]=-89693.036962487,["x"]=-100489.9961123,}, + }, + [2] = { + [1]={["y"]=-90797.505501889,["x"]=-99344.082465487,}, + [2]={["y"]=-90835.482160021,["x"]=-99409.11749254,}, + [3]={["y"]=-87210.216900398,["x"]=-101681.72494832,}, + [4]={["y"]=-87171.474397253,["x"]=-101619.20256393,}, + [5]={["y"]=-90797.030793662,["x"]=-99343.607757261,}, + }, + }, + }, + [AIRBASE.PersianGulf.Fujairah_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=5808.8716147284,["x"]=-116602.15633995,}, + [2]={["y"]=5781.9885293892,["x"]=-116666.67574476,}, + [3]={["y"]=9435.1910907931,["x"]=-118192.91910235,}, + [4]={["y"]=9459.878635843,["x"]=-118134.40047704,}, + [5]={["y"]=5808.4078522575,["x"]=-116603.31550719,}, + }, + }, + }, + [AIRBASE.PersianGulf.Havadarya] = { + PointsRunways = { + [1] = { + [1]={["y"]=-7565.4887830428,["x"]=109074.13162774,}, + [2]={["y"]=-7557.8281079193,["x"]=109030.65729641,}, + [3]={["y"]=-4987.3556518085,["x"]=109524.49147773,}, + [4]={["y"]=-4996.215358578,["x"]=109566.57508489,}, + [5]={["y"]=-7565.4936338604,["x"]=109074.32262205,}, + }, + }, + }, + [AIRBASE.PersianGulf.Kerman_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=70375.468628778,["x"]=456046.12685302,}, + [2]={["y"]=70297.050081575,["x"]=456015.1578105,}, + [3]={["y"]=71814.291673715,["x"]=452165.51037702,}, + [4]={["y"]=71902.918622452,["x"]=452188.46411914,}, + [5]={["y"]=70860.465673482,["x"]=454829.89695989,}, + [6]={["y"]=70862.525255971,["x"]=454892.77675983,}, + [7]={["y"]=70816.157465062,["x"]=454922.77944807,}, + [8]={["y"]=70462.749176371,["x"]=455833.38051827,}, + [9]={["y"]=70483.400377364,["x"]=455901.17880077,}, + [10]={["y"]=70453.787334431,["x"]=455974.8217628,}, + [11]={["y"]=70405.860962315,["x"]=455961.57382254,}, + [12]={["y"]=70374.689338175,["x"]=456046.51649833,}, + }, + }, + }, + [AIRBASE.PersianGulf.Khasab] = { + PointsRunways = { + [1] = { + [1]={["y"]=-534.81827307392,["x"]=-1495.070060483,}, + [2]={["y"]=-434.82912685139,["x"]=-1519.8421462589,}, + [3]={["y"]=-405.55302547993,["x"]=-1413.0969766429,}, + [4]={["y"]=-424.92029254105,["x"]=-1352.0675653224,}, + [5]={["y"]=216.05735069389,["x"]=1206.9187095195,}, + [6]={["y"]=116.42961315781,["x"]=1229.9576238247,}, + [7]={["y"]=88.253643635887,["x"]=1123.7918160128,}, + [8]={["y"]=101.1741158476,["x"]=1042.6886109249,}, + [9]={["y"]=-535.31436058928,["x"]=-1494.8762081291,}, + }, + }, + }, + [AIRBASE.PersianGulf.Lar_Airbase] = { + PointsRunways = { + [1] = { + [1]={["y"]=-183987.5454359,["x"]=169021.72039309,}, + [2]={["y"]=-183988.41292374,["x"]=168955.27082471,}, + [3]={["y"]=-180847.92031188,["x"]=168930.46175795,}, + [4]={["y"]=-180806.58653731,["x"]=168888.39641215,}, + [5]={["y"]=-180740.37934087,["x"]=168886.56748407,}, + [6]={["y"]=-180735.62412787,["x"]=168932.65647164,}, + [7]={["y"]=-180685.14571291,["x"]=168934.11961411,}, + [8]={["y"]=-180682.5852136,["x"]=169001.78995301,}, + [9]={["y"]=-183987.48111493,["x"]=169021.35002828,}, + }, + }, + }, + [AIRBASE.PersianGulf.Qeshm_Island] = { + PointsRunways = { + [1] = { + [1]={["y"]=-35140.372717152,["x"]=63373.658918509,}, + [2]={["y"]=-35098.556715749,["x"]=63320.377239302,}, + [3]={["y"]=-34991.318905699,["x"]=63408.730403557,}, + [4]={["y"]=-34984.574389344,["x"]=63401.311435566,}, + [5]={["y"]=-34991.993357335,["x"]=63313.632722947,}, + [6]={["y"]=-34956.921872287,["x"]=63265.746656824,}, + [7]={["y"]=-34917.129225791,["x"]=63261.699947011,}, + [8]={["y"]=-34832.822771349,["x"]=63337.23853019,}, + [9]={["y"]=-34915.105870884,["x"]=63436.382920614,}, + [10]={["y"]=-34906.337999622,["x"]=63478.198922017,}, + [11]={["y"]=-32728.533668488,["x"]=65307.986209216,}, + [12]={["y"]=-32676.600892552,["x"]=65299.218337954,}, + [13]={["y"]=-32623.99366498,["x"]=65334.964274638,}, + [14]={["y"]=-32626.691471522,["x"]=65388.92040548,}, + [15]={["y"]=-31822.745121968,["x"]=66067.418750826,}, + [16]={["y"]=-31777.556862387,["x"]=66068.767654097,}, + [17]={["y"]=-31691.227053039,["x"]=65974.344425122,}, + [18]={["y"]=-31606.246146962,["x"]=66042.464040311,}, + [19]={["y"]=-31602.199437148,["x"]=66084.280041714,}, + [20]={["y"]=-31632.549760747,["x"]=66124.747139846,}, + [21]={["y"]=-31727.647441358,["x"]=66134.189462744,}, + [22]={["y"]=-31734.391957713,["x"]=66141.608430735,}, + [23]={["y"]=-31632.549760747,["x"]=66225.914885176,}, + [24]={["y"]=-31673.691310515,["x"]=66277.173209477,}, + [25]={["y"]=-35140.880825624,["x"]=63373.905965825,}, + }, + }, + }, + [AIRBASE.PersianGulf.Sharjah_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=-71668.808658476,["x"]=-93980.156242153,}, + [2]={["y"]=-75307.847363315,["x"]=-91617.097584505,}, + [3]={["y"]=-75280.458023829,["x"]=-91574.709321014,}, + [4]={["y"]=-72249.697184234,["x"]=-93529.134331507,}, + [5]={["y"]=-72179.919581256,["x"]=-93526.199759419,}, + [6]={["y"]=-72138.183444896,["x"]=-93597.933743788,}, + [7]={["y"]=-71638.654062835,["x"]=-93927.584008321,}, + [8]={["y"]=-71668.325847279,["x"]=-93979.428115206,}, + }, + [2] = { + [1]={["y"]=-71553.225408723,["x"]=-93775.312323319,}, + [2]={["y"]=-75168.13829548,["x"]=-91426.51571111,}, + [3]={["y"]=-75125.388157445,["x"]=-91363.754870166,}, + [4]={["y"]=-71510.511081666,["x"]=-93703.252275385,}, + [5]={["y"]=-71552.247218027,["x"]=-93775.638386885,}, + }, + }, + }, + [AIRBASE.PersianGulf.Shiraz_International_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-353995.75579778,["x"]=382327.42294273,}, + [2]={["y"]=-354029.77009807,["x"]=382265.46199492,}, + [3]={["y"]=-349407.98049238,["x"]=379941.14030526,}, + [4]={["y"]=-349376.87025024,["x"]=380004.69408564,}, + [5]={["y"]=-353995.71101815,["x"]=382327.59771695,}, + }, + [2] = { + [1]={["y"]=-354056.29510012,["x"]=381845.97598829,}, + [2]={["y"]=-354091.48797289,["x"]=381783.6025623,}, + [3]={["y"]=-349650.64038107,["x"]=379550.92898242,}, + [4]={["y"]=-349624.41889127,["x"]=379614.92719482,}, + [5]={["y"]=-354056.25032049,["x"]=381846.15076251,}, + }, + }, + }, + [AIRBASE.PersianGulf.Sir_Abu_Nuayr] = { + PointsRunways = { + [1] = { + [1]={["y"]=-203367.3128691,["x"]=-103017.22553918,}, + [2]={["y"]=-203373.59664477,["x"]=-103054.92819323,}, + [3]={["y"]=-202578.27577922,["x"]=-103188.26018333,}, + [4]={["y"]=-202571.37254488,["x"]=-103151.01482599,}, + [5]={["y"]=-203367.65259839,["x"]=-103016.48202662,}, + [6]={["y"]=-203291.39594004,["x"]=-102985.49774228,}, + }, + }, + }, + [AIRBASE.PersianGulf.Sirri_Island] = { + PointsRunways = { + [1] = { + [1]={["y"]=-169713.12842428,["x"]=-27766.658020853,}, + [2]={["y"]=-169682.02009414,["x"]=-27726.583172021,}, + [3]={["y"]=-169727.21866794,["x"]=-27691.632048154,}, + [4]={["y"]=-169694.28043602,["x"]=-27650.276268081,}, + [5]={["y"]=-169763.08474269,["x"]=-27598.490047901,}, + [6]={["y"]=-169825.30140298,["x"]=-27607.090586235,}, + [7]={["y"]=-171614.98889813,["x"]=-26246.247907014,}, + [8]={["y"]=-171620.85326172,["x"]=-26187.105176343,}, + [9]={["y"]=-171686.10990337,["x"]=-26138.56820961,}, + [10]={["y"]=-171716.55468456,["x"]=-26178.745338885,}, + [11]={["y"]=-171764.9668776,["x"]=-26142.810515186,}, + [12]={["y"]=-171796.29599657,["x"]=-26183.416460911,}, + [13]={["y"]=-169713.5628285,["x"]=-27766.883787223,}, + }, + }, + }, + [AIRBASE.PersianGulf.Tunb_Island_AFB] = { + PointsRunways = { + [1] = { + [1]={["y"]=-92923.634698863,["x"]=9547.6862547173,}, + [2]={["y"]=-92963.030803298,["x"]=9565.7274614215,}, + [3]={["y"]=-92934.128053782,["x"]=9619.2987996964,}, + [4]={["y"]=-92970.946842975,["x"]=9640.1014155901,}, + [5]={["y"]=-92949.591945243,["x"]=9682.8112110532,}, + [6]={["y"]=-92899.518391942,["x"]=9699.7478540817,}, + [7]={["y"]=-91969.13471408,["x"]=11464.627292768,}, + [8]={["y"]=-91983.666755417,["x"]=11515.293058512,}, + [9]={["y"]=-91960.101282978,["x"]=11557.710908902,}, + [10]={["y"]=-91921.021874517,["x"]=11539.251288825,}, + [11]={["y"]=-91893.725202275,["x"]=11589.720675632,}, + [12]={["y"]=-91859.751646175,["x"]=11571.850192366,}, + [13]={["y"]=-92922.149728329,["x"]=9547.2937058617,}, + }, + }, + }, + [AIRBASE.PersianGulf.Tunb_Kochak] = { + PointsRunways = { + [1] = { + [1]={["y"]=-109925.50271188,["x"]=8974.5666013181,}, + [2]={["y"]=-109905.7382908,["x"]=8937.53274444,}, + [3]={["y"]=-109009.93726324,["x"]=9072.2234968343,}, + [4]={["y"]=-109040.82867587,["x"]=9104.9871291834,}, + [5]={["y"]=-109925.26515172,["x"]=8974.091480998,}, + }, + }, + }, + [AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-176230.75865538,["x"]=-188732.01369812,}, + [2]={["y"]=-176274.78045186,["x"]=-188744.8049371,}, + [3]={["y"]=-175692.03171595,["x"]=-190564.17145168,}, + [4]={["y"]=-175649.7486572,["x"]=-190550.58435053,}, + [5]={["y"]=-176230.66274076,["x"]=-188731.5667818,}, + }, + }, + }, + [AIRBASE.PersianGulf.Bandar_e_Jask_airfield] = { + PointsRunways = { + [1] = { + [1]={["y"]=155156.73167657,["x"]=-57837.031277333,}, + [2]={["y"]=155130.38996239,["x"]=-57790.475605714,}, + [3]={["y"]=157137.17872571,["x"]=-56710.411783359,}, + [4]={["y"]=157148.46631801,["x"]=-56688.071756941,}, + [5]={["y"]=157220.07198163,["x"]=-56649.035500253,}, + [6]={["y"]=157227.83220133,["x"]=-56662.204357931,}, + [7]={["y"]=157359.6383572,["x"]=-56590.481115222,}, + [8]={["y"]=157383.03659539,["x"]=-56633.044744502,}, + [9]={["y"]=155156.7940421,["x"]=-57837.149989814,}, + }, + }, + }, + [AIRBASE.PersianGulf.Abu_Dhabi_International_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-163964.56943899,["x"]=-189427.63621921,}, + [2]={["y"]=-164005.96838287,["x"]=-189478.90226888,}, + [3]={["y"]=-160798.22080495,["x"]=-192054.59531727,}, + [4]={["y"]=-160755.05282258,["x"]=-192002.58569997,}, + [5]={["y"]=-163964.47352437,["x"]=-189427.18930288,}, + }, + [2] = { + [1]={["y"]=-163615.44952024,["x"]=-187144.00786922,}, + [2]={["y"]=-163656.84846411,["x"]=-187195.27391888,}, + [3]={["y"]=-160452.71811093,["x"]=-189764.86593382,}, + [4]={["y"]=-160411.94568221,["x"]=-189715.47961171,}, + [5]={["y"]=-163615.35360562,["x"]=-187143.56095289,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Bateen_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-183207.51774197,["x"]=-189871.8319832,}, + [2]={["y"]=-183240.61462564,["x"]=-189914.01184622,}, + [3]={["y"]=-180748.88998479,["x"]=-191943.30402837,}, + [4]={["y"]=-180711.83076051,["x"]=-191896.52435182,}, + [5]={["y"]=-183207.42182735,["x"]=-189871.38506688,}, + }, + }, + }, + [AIRBASE.PersianGulf.Kish_International_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-227330.79164594,["x"]=42691.91536494,}, + [2]={["y"]=-227321.58531968,["x"]=42758.113234714,}, + [3]={["y"]=-223235.73004619,["x"]=42313.579195302,}, + [4]={["y"]=-223240.99080406,["x"]=42247.819722016,}, + [5]={["y"]=-227330.67774245,["x"]=42691.785682556,}, + }, + [2] = { + [1]={["y"]=-227283.77911886,["x"]=42987.748941936,}, + [2]={["y"]=-227274.5727926,["x"]=43053.946811711,}, + [3]={["y"]=-222907.94761294,["x"]=42580.826755904,}, + [4]={["y"]=-222915.76510871,["x"]=42514.58376547,}, + [5]={["y"]=-227283.66521537,["x"]=42987.619259553,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Ain_International_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-65165.315648901,["x"]=-209042.45716363,}, + [2]={["y"]=-65112.933878375,["x"]=-209048.84518442,}, + [3]={["y"]=-65672.013626755,["x"]=-213019.66479976,}, + [4]={["y"]=-65722.555424932,["x"]=-213013.91596964,}, + [5]={["y"]=-65165.400582791,["x"]=-209042.15059908,}, + }, + }, + }, + [AIRBASE.PersianGulf.Lavan_Island_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-288099.83301495,["x"]=76353.443273049,}, + [2]={["y"]=-288119.51457685,["x"]=76302.756224611,}, + [3]={["y"]=-288070.96603401,["x"]=76283.898526152,}, + [4]={["y"]=-288085.61084238,["x"]=76247.386812114,}, + [5]={["y"]=-288032.04695421,["x"]=76224.316223573,}, + [6]={["y"]=-287991.12173627,["x"]=76245.38067398,}, + [7]={["y"]=-287489.96435675,["x"]=76037.610404141,}, + [8]={["y"]=-287497.65444594,["x"]=76017.686082159,}, + [9]={["y"]=-287453.61120787,["x"]=75998.111309685,}, + [10]={["y"]=-287419.70490555,["x"]=76007.199596905,}, + [11]={["y"]=-285642.24565503,["x"]=75279.787069797,}, + [12]={["y"]=-285625.46727862,["x"]=75239.239326815,}, + [13]={["y"]=-285570.23845628,["x"]=75217.217707782,}, + [14]={["y"]=-285555.20782742,["x"]=75252.172658628,}, + [15]={["y"]=-285505.92134673,["x"]=75231.199688121,}, + [16]={["y"]=-285484.28380792,["x"]=75284.258832895,}, + [17]={["y"]=-288099.97979219,["x"]=76354.32393647,}, + }, + }, + }, + [AIRBASE.PersianGulf.Jiroft_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=140376.87310595,["x"]=283748.07558774,}, + [2]={["y"]=140299.43760975,["x"]=283655.81201779,}, + [3]={["y"]=143008.43807723,["x"]=281517.41347718,}, + [4]={["y"]=143052.6952428,["x"]=281573.25195709,}, + [5]={["y"]=142946.60213095,["x"]=281656.5960586,}, + [6]={["y"]=142975.14179847,["x"]=281687.20381796,}, + [7]={["y"]=142932.12548801,["x"]=281724.01585287,}, + [8]={["y"]=142870.49635092,["x"]=281719.05243244,}, + [9]={["y"]=140437.35783025,["x"]=283640.84253664,}, + [10]={["y"]=140433.27045062,["x"]=283705.80267729,}, + [11]={["y"]=140376.77702493,["x"]=283747.8442964,}, + }, + }, + }, + }, +} + + +--- Creates a new ATC_GROUND_PERSIANGULF object. +-- @param #ATC_GROUND_PERSIANGULF self +-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.PersianGulf enumerator). +-- @return #ATC_GROUND_PERSIANGULF self +function ATC_GROUND_PERSIANGULF:New( AirbaseNames ) + + -- Inherits from BASE + local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) -- #ATC_GROUND_PERSIANGULF + + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, 0.05 ) + + self:SetKickSpeedKmph( 50 ) + self:SetMaximumKickSpeedKmph( 150 ) + + -- These lines here are for the demonstration mission. + -- They create in the dcs.log the coordinates of the runway polygons, that are then + -- taken by the moose designer from the dcs.log and reworked to define the + -- Airbases structure, which is part of the class. + -- When new airbases are added or airbases are changed on the map, + -- the MOOSE designer willde-comment this section and apply the changes in the demo + -- mission, and do a re-run to create a new dcs.log, and then add the changed coordinates + -- in the Airbases structure. + -- So, this needs to stay commented normally once a map has been finished. + + + --[[ + + -- Abu_Musa_Island_Airport + do + local VillagePrefix = "Abu_Musa_Island_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Dhafra_AB + do + local VillagePrefix = "Al_Dhafra_AB" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Maktoum_Intl + do + local VillagePrefix = "Al_Maktoum_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Minhad_AB + do + local VillagePrefix = "Al_Minhad_AB" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bandar_Abbas_Intl + do + local VillagePrefix = "Bandar_Abbas_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bandar_Lengeh + do + local VillagePrefix = "Bandar_Lengeh" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Dubai_Intl + do + local VillagePrefix = "Dubai_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Fujairah_Intl + do + local VillagePrefix = "Fujairah_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Havadarya + do + local VillagePrefix = "Havadarya" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Kerman_Airport + do + local VillagePrefix = "Kerman_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Khasab + do + local VillagePrefix = "Khasab" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Lar_Airbase + do + local VillagePrefix = "Lar_Airbase" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Qeshm_Island + do + local VillagePrefix = "Qeshm_Island" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sharjah_Intl + do + local VillagePrefix = "Sharjah_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Shiraz_International_Airport + do + local VillagePrefix = "Shiraz_International_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sir_Abu_Nuayr + do + local VillagePrefix = "Sir_Abu_Nuayr" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sirri_Island + do + local VillagePrefix = "Sirri_Island" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Tunb_Island_AFB + do + local VillagePrefix = "Tunb_Island_AFB" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Tunb_Kochak + do + local VillagePrefix = "Tunb_Kochak" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sas_Al_Nakheel_Airport + do + local VillagePrefix = "Sas_Al_Nakheel_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bandar_e_Jask_airfield + do + local VillagePrefix = "Bandar_e_Jask_airfield" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Abu_Dhabi_International_Airport + do + local VillagePrefix = "Abu_Dhabi_International_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Bateen_Airport + do + local VillagePrefix = "Al_Bateen_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Kish_International_Airport + do + local VillagePrefix = "Kish_International_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Ain_International_Airport + do + local VillagePrefix = "Al_Ain_International_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Lavan_Island_Airport + do + local VillagePrefix = "Lavan_Island_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Jiroft_Airport + do + local VillagePrefix = "Jiroft_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bandar_Abbas_Intl + do + local VillagePrefix = "Bandar_Abbas_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + --]] + + return self +end + + \ No newline at end of file From 47778c6fe79fcf3bff1d95708186c4bbf903f317 Mon Sep 17 00:00:00 2001 From: 131stGutts Date: Sun, 13 Jan 2019 16:27:58 +0100 Subject: [PATCH 137/485] Set the time frequency of alert with PersianGulf Add new functions "Start" for ATC_GROUND_CAUCASUS, ATC_GROUND_NEVADA, ATC_GROUND_NORMANDY, ATC_GROUND_PERSIANGULF which accept none or one parameter for setting the SCHEDULER frequency. If none defined, value set before (0.05) is used. The SCHEDULER is not anymore in "New" functions of ATC_GROUND_XXX but in "Start" Usages: atcGroundCaucasus=ATC_GROUND_CAUCASUS:New() atcGroundCausasus:Start() or atcGroundCaucasus=ATC_GROUND_CAUCASUS:New() atcGroundCausasus:Start(0.5) Relative to PullRequest #1094, #1096, #1098 Include grammar issues fix from ticket #753 --- .../Moose/Functional/ATC_Ground.lua | 52 ++++++++++++++----- Moose Development/Moose/Wrapper/Airbase.lua | 17 ++++++ 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index 7c25b6f1a..ba407eb37 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -275,7 +275,7 @@ function ATC_GROUND:_AirbaseMonitor() self:E( Taxi ) if Taxi == false then local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed ) - Client:Message( "Welcome at " .. AirbaseID .. ". The maximum taxiing speed is " .. + Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " .. Velocity:ToString() , 20, "ATC" ) Client:SetState( self, "Taxi", true ) end @@ -299,7 +299,7 @@ function ATC_GROUND:_AirbaseMonitor() end if Speeding == true then MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. - " is kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() + " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() Client:Destroy() Client:SetState( self, "Speeding", false ) Client:SetState( self, "Warnings", 0 ) @@ -331,7 +331,7 @@ function ATC_GROUND:_AirbaseMonitor() Velocity:ToString(), 5, "ATC" ) Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) else - MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " is kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() + MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() --- @param Wrapper.Client#CLIENT Client Client:Destroy() Client:SetState( self, "Speeding", false ) @@ -363,7 +363,7 @@ function ATC_GROUND:_AirbaseMonitor() Client:Message( "Warning " .. OffRunwayWarnings .. "/3! Airbase traffic rule violation! Get back on the taxi immediately!", 5, "ATC" ) Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 ) else - MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " is kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() + MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() --- @param Wrapper.Client#CLIENT Client Client:Destroy() Client:SetState( self, "IsOffRunway", false ) @@ -788,8 +788,6 @@ function ATC_GROUND_CAUCASUS:New( AirbaseNames ) -- Inherits from BASE local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, 0.05 ) - self:SetKickSpeedKmph( 50 ) self:SetMaximumKickSpeedKmph( 150 ) @@ -1002,6 +1000,15 @@ function ATC_GROUND_CAUCASUS:New( AirbaseNames ) end +--- Start SCHEDULER for ATC_GROUND_CAUCASUS object. +-- @param #ATC_GROUND_CAUCASUS self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_CAUCASUS:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end + --- @type ATC_GROUND_NEVADA @@ -1379,8 +1386,6 @@ function ATC_GROUND_NEVADA:New( AirbaseNames ) -- Inherits from BASE local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, 0.05 ) - self:SetKickSpeedKmph( 50 ) self:SetMaximumKickSpeedKmph( 150 ) @@ -1544,6 +1549,16 @@ function ATC_GROUND_NEVADA:New( AirbaseNames ) return self end +--- Start SCHEDULER for ATC_GROUND_NEVADA object. +-- @param #ATC_GROUND_NEVADA self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_NEVADA:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end + + --- @type ATC_GROUND_NORMANDY -- @extends #ATC_GROUND @@ -2033,8 +2048,6 @@ function ATC_GROUND_NORMANDY:New( AirbaseNames ) -- Inherits from BASE local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) -- #ATC_GROUND_NORMANDY - - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, 0.05 ) self:SetKickSpeedKmph( 40 ) self:SetMaximumKickSpeedKmph( 100 ) @@ -2317,6 +2330,15 @@ function ATC_GROUND_NORMANDY:New( AirbaseNames ) return self end + +--- Start SCHEDULER for ATC_GROUND_NORMANDY object. +-- @param #ATC_GROUND_NORMANDY self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_NORMANDY:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end --- @type ATC_GROUND_PERSIANGULF -- @extends #ATC_GROUND @@ -2889,8 +2911,6 @@ function ATC_GROUND_PERSIANGULF:New( AirbaseNames ) -- Inherits from BASE local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) -- #ATC_GROUND_PERSIANGULF - - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, 0.05 ) self:SetKickSpeedKmph( 50 ) self:SetMaximumKickSpeedKmph( 150 ) @@ -3152,6 +3172,14 @@ function ATC_GROUND_PERSIANGULF:New( AirbaseNames ) return self end +--- Start SCHEDULER for ATC_GROUND_PERSIANGULF object. +-- @param #ATC_GROUND_PERSIANGULF self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_PERSIANGULF:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 71b020489..fe8a7824c 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -238,6 +238,15 @@ AIRBASE.Normandy = { -- * AIRBASE.PersianGulf.Sharjah_Intl -- * AIRBASE.PersianGulf.Shiraz_International_Airport -- * AIRBASE.PersianGulf.Kerman_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", @@ -259,6 +268,14 @@ AIRBASE.PersianGulf = { ["Sharjah_Intl"] = "Sharjah Intl", ["Shiraz_International_Airport"] = "Shiraz International Airport", ["Kerman_Airport"] = "Kerman 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". From dd15da28b28bfd4c9b26b7b670b6bbdb75ead296 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 20 Jan 2019 02:02:22 +0100 Subject: [PATCH 138/485] 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 139/485] 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 140/485] 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 141/485] 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 142/485] 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 143/485] 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 144/485] 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 145/485] 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 146/485] 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 147/485] 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 148/485] 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 149/485] 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 150/485] 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 From dddf056b20b4905d64a0fc3c350307ceabe73f26 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 31 Jan 2019 21:54:44 +0100 Subject: [PATCH 151/485] AIRBOSS v0.9.3 - Added tower frequency. - Added AI exclude set. --- Moose Development/Moose/Ops/Airboss.lua | 53 ++++++++++++++++++++- Moose Development/Moose/Utilities/Utils.lua | 4 -- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 79b5a9970..f29aea288 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -110,6 +110,7 @@ -- @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 #number TowerFreq Tower radio frequency in MHz. -- @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. @@ -172,6 +173,7 @@ -- @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 Core.Set#SET_GROUP excludesetAI AI groups in this set will be explicitly excluded from handling by the airboss and not forced into the Marshal pattern. -- @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. @@ -836,6 +838,7 @@ AIRBOSS = { MarshalRadio = nil, MarshalFreq = nil, MarshalModu = nil, + TowerFreq = nil, radiotimer = nil, zoneCCA = nil, zoneCCZ = nil, @@ -897,6 +900,7 @@ AIRBOSS = { turnintowind = nil, detour = nil, squadsetAI = nil, + excludesetAI = nil, menusingle = nil, collisiondist = nil, Tmessage = nil, @@ -1681,6 +1685,9 @@ function AIRBOSS:New(carriername, alias) -- Create carrier beacon. self.beacon=BEACON:New(self.carrier) + -- Set Tower Frequency of carrier. + self:_GetTowerFrequency() + -- Init player scores table. self.playerscores={} @@ -2217,6 +2224,15 @@ function AIRBOSS:SetSquadronAI(setgroup) return self end +--- Define a set of AI groups that excluded from AI handling. Members of this set will be left allone by the airboss and not forced into the Marshal pattern. +-- @param #AIRBOSS self +-- @param Core.Set#SET_GROUP setgroup The set of AI groups which are excluded. +-- @return #AIRBOSS self +function AIRBOSS:SetExcludeAI(setgroup) + self.excludesetAI=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. @@ -4119,8 +4135,10 @@ 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. + -- Defines if AI group should be handled by the airboss. local iscarriersquad=true + + -- Check if AI group is part of the group set if a set was defined. if self.squadsetAI then local group=self.squadsetAI:FindGroup(groupname) if group then @@ -4129,6 +4147,15 @@ function AIRBOSS:_ScanCarrierZone() iscarriersquad=false end end + + -- Check if group was explicitly excluded. + if self.excludesetAI then + local group=self.excludesetAI:FindGroup(groupname) + if group then + iscarriersquad=false + end + end + -- Get distance to carrier. local dist=knownflight.group:GetCoordinate():Get2DDistance(self:GetCoordinate()) @@ -10449,6 +10476,25 @@ function AIRBOSS:_GetOnboardNumbers(group, playeronly) return numbers end +--- Get Tower frequency of carrier. +-- @param #AIRBOSS self +function AIRBOSS:_GetTowerFrequency() + + -- Tower frequency in MHz + self.TowerFreq=0 + + -- Get Template of Strike Group + local striketemplate=self.carrier:GetGroup():GetTemplate() + + -- Find the carrier unit. + for _,unit in pairs(striketemplate.units) do + if self.carrier:GetName()==unit.name then + self.TowerFreq=unit.frequency/1000000 + return + 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.) @@ -12268,6 +12314,8 @@ function AIRBOSS:_DisplayQueue(_unitname, queue, qname) end + + --- Report information about carrier. -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. @@ -12292,7 +12340,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) local carrierheading=self.carrier:GetHeading() local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocityMPS()) - -- Tacan/ICLS. + -- TACAN/ICLS. local tacan="unknown" local icls="unknown" if self.TACANon and self.TACANchannel~=nil then @@ -12357,6 +12405,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("Tower frequency %.3f MHz\n", self.TowerFreq) 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) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 0ab538ef4..301433c61 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -59,10 +59,6 @@ DCSMAP = { --- 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={ From e063379cd83bddaa490f80166e3974d556c4fbba Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 4 Feb 2019 20:34:45 +0100 Subject: [PATCH 152/485] AIRBOSS v0.9.4 - Added AV-8B and USS Tarawa. - Corrected A-4E AoA threshold. - Other minor adjustments. RADIO: Fixed frequency check. --- Moose Development/Moose/Core/Radio.lua | 4 +- Moose Development/Moose/Ops/Airboss.lua | 991 ++++++++++++++++++------ 2 files changed, 751 insertions(+), 244 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 6155aa036..9a2e6d570 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -160,7 +160,7 @@ end --- 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 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-87.995 / 108-173.995 / 225-399.975MHz. -- @return #RADIO self function RADIO:SetFrequency(Frequency) self:F2(Frequency) @@ -168,7 +168,7 @@ function RADIO:SetFrequency(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 + if (Frequency >= 30 and Frequency <= 87.995) or (Frequency >= 108 and Frequency <= 173.995) or (Frequency >= 225 and Frequency <= 399.975) then -- Convert frequency from MHz to Hz self.Frequency = Frequency * 1000000 diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index f29aea288..7d09bcd49 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -16,7 +16,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. +-- * Combine multiple human player to sections (WIP). -- * Many parameters customizable by convenient user API functions. -- * Multiple carrier support due to object oriented approach. -- * Unlimited number of players. @@ -26,11 +26,13 @@ -- **Supported Carriers:** -- -- * [USS John C. Stennis](https://en.wikipedia.org/wiki/USS_John_C._Stennis) (CVN-74) +-- * [USS Tarawa](https://en.wikipedia.org/wiki/USS_Tarawa_(LHA-1)) (LHA-1) [**WIP**] -- -- **Supported Aircraft:** -- -- * [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) +-- * [AV-8B N/A Harrier](https://forums.eagle.ru/forumdisplay.php?f=555) (Player & AI) [**WIP**] -- * F/A-18C Hornet (AI) -- * F-14A Tomcat (AI) -- * E-2D Hawkeye (AI) @@ -39,10 +41,14 @@ -- 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 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 implementation is kept general. So other aircraft and carriers possible in future. [*Winter is coming!*](https://forums.eagle.ru/forumdisplay.php?f=395) +-- The AV-8B Harrier and the USS Tarawa are very much WIP. Those two can only be used together, i.e. the Tarawa is the only carrier the harrier is supposed to land on and +-- the no other fixed wing aircraft (human or AI controlled) are supposed to land on the Tarawa. Currently only Case I is supported. Case II/III take slightly steps from the CVN carrier. +-- However, the two Case II/III pattern are very similar so this is not a big drawback. +-- +-- The implementation is kept general. Therefore, other aircraft and carriers can be added 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. +-- **PLEASE NOTE** that his class is work in progress. 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 @@ -59,9 +65,21 @@ -- -- ## Youtube Videos -- +-- Early AIRBOSS Groove Testing: +-- -- * [[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) -- +-- Lex explaining Boat Ops: +-- +-- * [( DCS HORNET ) Some boat ops basics VID 1](https://www.youtube.com/watch?v=LvGQS-3AzMc) +-- * [( DCS HORNET ) Some boat ops basics VID 1](https://www.youtube.com/watch?v=bN44wvtRsw0) +-- +-- Jabbers Case I and III Recovery Tutorials: +-- +-- * [DCS World - F/A-18 - Case I Carrier Recovery Tutorial](https://www.youtube.com/watch?v=lm-M3VUy-_I) +-- * [DCS World - Case I Recovery Tutorial - Followup](https://www.youtube.com/watch?v=cW5R32Q6xC8) +-- * [DCS World - CASE III Recovery Tutorial](https://www.youtube.com/watch?v=Lnfug5CVAvo) -- -- ### Open Questions? -- @@ -196,7 +214,7 @@ -- -- * **CASE I** during daytime and good weather, -- * **CASE II** during daytime but poor visibility conditions, --- * **CASE III** during nighttime recoveries. +-- * **CASE III** when below Case II conditions, e.g. during nighttime. -- -- 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. -- @@ -601,7 +619,7 @@ -- 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)) +-- airbossStennis:AddRecoveryWindow(string.format("08:05:00+%d", i), string.format("08:50:00+%d", i)) -- end -- -- ### Turning into the Wind @@ -610,7 +628,9 @@ -- -- 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. +-- 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 so that to total wind above the deck. +-- For example, if the is blowing with 5 knots, the carrier will go 15 knots so that it adds up to the specified 20 knots. +-- -- 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 @@ -907,20 +927,9 @@ AIRBOSS = { soundfolder = 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 A-4E Community mod. -AIRBOSS.AircraftPlayer={ - --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 (not yet supported). +-- @field #string AV8B AV-8B Night Harrier. Works only with the USS Tarawa. -- @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. @@ -931,7 +940,7 @@ AIRBOSS.AircraftPlayer={ -- @field #string S3BTANKER Lockheed S-3B Viking tanker. -- @field #string E2D Grumman E-2D Hawkeye AWACS. AIRBOSS.AircraftCarrier={ - --AV8B="AV8BNA", + AV8B="AV8BNA", HORNET="FA-18C_hornet", A4EC="A-4E-C", F14A="F-14A_tomcat", @@ -1006,6 +1015,8 @@ AIRBOSS.CarrierType={ -- @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_AL "Groove Abeam Landing Spot". +-- @field #string GROOVE_LC "Groove Level Cross". -- @field #string GROOVE_IW "Groove In the Wires". -- @field #string BOLTER "Bolter Pattern". -- @field #string DEBRIEF "Debrief". @@ -1034,6 +1045,8 @@ AIRBOSS.PatternStep={ GROOVE_IC="Groove In Close", GROOVE_AR="Groove At the Ramp", GROOVE_IW="Groove In the Wires", + GROOVE_AL="Groove Abeam Landing Spot", + GROOVE_LC="Groove Level Cross", BOLTER="Bolter Pattern", DEBRIEF="Debrief", } @@ -1045,6 +1058,8 @@ AIRBOSS.PatternStep={ -- @field #string IM "IM": In the middle. -- @field #string IC "IC": In close. -- @field #string AR "AR": At the ramp. +-- @field #string AL "AL": Abeam landing position (Tarawa). +-- @field #string LC "LC": Level crossing (Tarawa). -- @field #string IW "IW": In the wires. AIRBOSS.GroovePos={ X0="X0", @@ -1052,6 +1067,8 @@ AIRBOSS.GroovePos={ IM="IM", IC="IC", AR="AR", + AL="AL", + LC="LC", IW="IW", } @@ -1071,7 +1088,7 @@ AIRBOSS.GroovePos={ -- @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. +-- @field #string sender Sender of the message (optional). Default radio alias. --- LSO radio calls. -- @type AIRBOSS.LSOCall @@ -1092,6 +1109,12 @@ AIRBOSS.GroovePos={ -- @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 EXPECTHEAVYWAVEOFF "Expect heavy wavoff" call. +-- @field #AIRBOSS.RadioCall EXPECTSPOT75 "Expect spot 7.5" call. +-- @field #AIRBOSS.RadioCall CLEAREDTOLAND "Cleared to land" call. +-- @field #AIRBOSS.RadioCall CHECK "CHECK" call. +-- @field #AIRBOSS.RadioCall STABILIZED "Stabilized" call. +-- @field #AIRBOSS.RadioCall IDLE "Idle" call. -- @field #AIRBOSS.RadioCall N0 "Zero" call. -- @field #AIRBOSS.RadioCall N1 "One" call. -- @field #AIRBOSS.RadioCall N2 "Two" call. @@ -1241,6 +1264,54 @@ AIRBOSS.LSOCall={ duration=1.0, subduration=5, }, + EXPECTHEAVYWAVEOFF={ + file="LSO-ExpectHeavyWaveoff", + suffix="ogg", + loud=false, + subtitle="Expect heavy waveoff", + duration=1.2, + subduration=5, + }, + EXPECTSPOT75={ + file="LSO-ExpectSpot75", + suffix="ogg", + loud=false, + subtitle="Expect spot 7.5", + duration=2.0, + subduration=5, + }, + CLEAREDTOLAND={ + file="LSO-ClearedToLand", + suffix="ogg", + loud=false, + subtitle="Cleared to land", + duration=1.0, + subduration=5, + }, + CHECK={ + file="LSO-Check", + suffix="ogg", + loud=false, + subtitle="Check", + duration=0.45, + subduration=2.5, + }, + STABILIZED={ + file="LSO-Stabilized", + suffix="ogg", + loud=false, + subtitle="Stabilized", + duration=0.9, + subduration=5, + }, + IDLE={ + file="LSO-Idle", + suffix="ogg", + loud=false, + subtitle="Idle", + duration=0.45, + subduration=5, + }, N0={ file="LSO-N0", suffix="ogg", @@ -1580,7 +1651,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.3" +AIRBOSS.version="0.9.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1778,8 +1849,8 @@ function AIRBOSS:New(carriername, alias) -- TODO: Carl Vinson parameters. self:_InitStennis() elseif self.carriertype==AIRBOSS.CarrierType.TARAWA then - -- TODO: Tarawa parameters. - self:_InitStennis() + -- Tarawa parameters. + self:_InitTarawa() elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then -- Kusnetsov parameters - maybe... self:_InitStennis() @@ -1834,7 +1905,7 @@ function AIRBOSS:New(carriername, alias) self:GetCoordinate():FlareYellow() -- Stern - stern:FlareGreen() + stern:FlareYellow() -- Bow bow:FlareYellow() @@ -1855,17 +1926,36 @@ function AIRBOSS:New(carriername, alias) -- Left 40 meters from stern. local cL=stern:Translate(self.carrierparam.totwidthport, hdg-90) cL:FlareYellow() + - -- 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() + -- Carrier specific. + if self.carrier:GetTypeName()~=AIRBOSS.CarrierType.TARAWA then + + -- 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() + + else + + -- Abeam landing spot zone. + local ALSPT=self:_GetZoneAbeamLandingSpot() + ALSPT:FlareZone(FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters(120)) + + -- Primary landing spot zone. + local LSPT=self:_GetZoneLandingSpot() + LSPT:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) + -- Landing spot coordinate. + local PLSC=self:_GetLandingSpotCoordinate() + PLSC:FlareWhite() + end + -- Flare carrier and landing runway. local cbox=self:_GetZoneCarrierBox() local rbox=self:_GetZoneRunwayBox() @@ -2745,7 +2835,7 @@ end -- FSM event functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +--- On after Start event. Starts the AIRBOSS. Addes event handlers and schedules status updates of reqests and queue. -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. @@ -3165,10 +3255,7 @@ function AIRBOSS:_CheckRecoveryTimes() 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 + self:CarrierTurnIntoWind(t, v) end -- Set current recovery window. @@ -3547,7 +3634,7 @@ function AIRBOSS:_InitStennis() 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.Zmin=-2000 -- Not more than 2 km port. self.Final.Zmax= nil self.Final.LimitXmin=nil -- No limits. Check is carried out differently. self.Final.LimitXmax=nil @@ -3567,6 +3654,46 @@ function AIRBOSS:_InitStennis() end +--- Init parameters for USS Stennis carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitTarawa() + + -- Init Stennis as default. + self:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist =-125 + self.carrierparam.deckheight = 21 --69 ft + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=245 + self.carrierparam.totwidthport=10 + self.carrierparam.totwidthstarboard=25 + + -- Landing runway. + self.carrierparam.rwyangle = 0 + self.carrierparam.rwylength = 225 + self.carrierparam.rwywidth = 15 + + -- Wires. + self.carrierparam.wire1=nil + self.carrierparam.wire2=nil + self.carrierparam.wire3=nil + self.carrierparam.wire4=nil + + -- 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? + self.BreakLate.Zmin=-UTILS.NMToMeters(1.6) -- Not more than 1.6 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.5) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 + self.BreakLate.LimitZmax= nil + +end + --- Get optimal aircraft AoA parameters.. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -3604,22 +3731,14 @@ function AIRBOSS:_GetAircraftAoA(playerData) elseif skyhawk then -- A-4E-C Skyhawk parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 -- 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 + -- Github repo suggests they simply use a factor of two to get from degrees to units. + aoa.SLOW = 9.50 --=19.0/2 + aoa.Slow = 9.25 --=18.5/2 + aoa.OnSpeedMax = 9.00 --=18.0/2 + aoa.OnSpeed = 8.75 --=17.5/2 8.1 + aoa.OnSpeedMin = 8.50 --=17.0/2 + aoa.Fast = 8.25 --=17.5/2 + aoa.FAST = 8.00 --=16.5/2 elseif harrier then -- AV-8B Harrier parameters. This might need further tuning. aoa.SLOW=14.0 @@ -3646,7 +3765,10 @@ function AIRBOSS:_AoAUnit2Deg(playerData, aoaunits) -- Check aircraft type of player. if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then - -- F-14A/B + + ------------- + -- F-14A/B -- + ------------- -- NATOPS: -- unit=0 ==> alpha=-10 degrees. @@ -3656,13 +3778,21 @@ function AIRBOSS:_AoAUnit2Deg(playerData, aoaunits) degrees=-10+50/30*aoaunits elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then - -- A-4E + + ---------- + -- A-4E -- + ---------- + + -- A-4E-C source code suggests a imple factor of 1/2 for conversion. + degrees=0.5*aoaunits + --[[ -- 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 @@ -3681,7 +3811,10 @@ function AIRBOSS:_AoADeg2Units(playerData, degrees) -- Check aircraft type of player. if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then - -- F-14A/B + + ------------- + -- F-14A/B -- + ------------- -- NATOPS: -- unit=0 ==> alpha=-10 degrees. @@ -3690,10 +3823,16 @@ function AIRBOSS:_AoADeg2Units(playerData, 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 + + ---------- + -- A-4E -- + ---------- + -- A-4E source code suggests a simple factor of two as conversion. + aoaunits=2*degrees + + --[[ -- 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 @@ -3702,8 +3841,8 @@ function AIRBOSS:_AoADeg2Units(playerData, degrees) local a=1.9/5.5 local a=1/3 local b=8.0-17.5*a - aoaunits=(degrees-b)/a -]] + ]] + end return aoaunits @@ -3726,6 +3865,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) 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 + local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B -- Return values. local alt @@ -3770,7 +3910,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.INITIAL then - if hornet or tomcat then + if hornet or tomcat or harrier then alt=UTILS.FeetToMeters(800) speed=UTILS.KnotsToMps(350) elseif skyhawk then @@ -3780,7 +3920,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.BREAKENTRY then - if hornet or tomcat then + if hornet or tomcat or harrier then alt=UTILS.FeetToMeters(800) speed=UTILS.KnotsToMps(350) elseif skyhawk then @@ -3790,7 +3930,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.EARLYBREAK then - if hornet or tomcat then + if hornet or tomcat or harrier then alt=UTILS.FeetToMeters(800) elseif skyhawk then alt=UTILS.FeetToMeters(600) @@ -3798,7 +3938,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.LATEBREAK then - if hornet or tomcat then + if hornet or tomcat or harrier then alt=UTILS.FeetToMeters(800) elseif skyhawk then alt=UTILS.FeetToMeters(600) @@ -3806,7 +3946,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.ABEAM then - if hornet or tomcat then + if hornet or tomcat or harrier then alt=UTILS.FeetToMeters(600) elseif skyhawk then alt=UTILS.FeetToMeters(500) @@ -3814,14 +3954,21 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) aoa=aoaac.OnSpeed - dist=UTILS.NMToMeters(1.2) + if harrier then + -- 0.8 to 1.0 NM + dist=UTILS.NMToMeters(0.9) + else + dist=UTILS.NMToMeters(1.2) + end elseif step==AIRBOSS.PatternStep.NINETY then if hornet or tomcat then alt=UTILS.FeetToMeters(500) elseif skyhawk then - alt=UTILS.FeetToMeters(500) + alt=UTILS.FeetToMeters(500) + elseif harrier then + alt=UTILS.FeetToMeters(425) end aoa=aoaac.OnSpeed @@ -3833,6 +3980,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif skyhawk then alt=UTILS.FeetToMeters(370) --? end + -- Harrier wont get into wake pos. Runway is not angled and it stays port. aoa=aoaac.OnSpeed @@ -3841,7 +3989,10 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) if hornet or tomcat then alt=UTILS.FeetToMeters(300) elseif skyhawk then - alt=UTILS.FeetToMeters(300) --? + alt=UTILS.FeetToMeters(300) --? + elseif harrier then + -- 300-325 ft + alt=UTILS.FeetToMeters(300) end aoa=aoaac.OnSpeed @@ -4612,6 +4763,15 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- Second point 1.5 NM ahead. p2=Carrier:Translate(UTILS.NMToMeters(1.5), hdg) + -- Tarawa Delta pattern. + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + + -- Pattern is directly overhead the carrier. + p1=Carrier:Translate(UTILS.NMToMeters(1.0), hdg+90) + p2=p1:Translate(2.5, hdg) + + end + else -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. @@ -5643,29 +5803,9 @@ function AIRBOSS:_CheckPlayerStatus() end -- 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 or - playerData.step==AIRBOSS.PatternStep.GROOVE_IM or - playerData.step==AIRBOSS.PatternStep.GROOVE_IC then - - self:_CheckFoulDeck(playerData) - end - - else + self:_CheckFoulDeck(playerData) - -- Case III: Check is done at 3/4 NM according to NATOPS - 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 - end - + -- Check current step. if playerData.step==AIRBOSS.PatternStep.UNDEFINED then -- Status undefined. @@ -5773,6 +5913,8 @@ function AIRBOSS:_CheckPlayerStatus() 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_AL or + playerData.step==AIRBOSS.PatternStep.GROOVE_LC or playerData.step==AIRBOSS.PatternStep.GROOVE_IW then -- CASE I/II: In the groove. @@ -5962,7 +6104,10 @@ function AIRBOSS:OnEventLand(EventData) -- Check if player or AI landed. if _unit and _playername then - -- Human Player landed. + + ------------------------- + -- Human Player landed -- + ------------------------- -- Get info. local _uid=_unit:GetID() @@ -6009,6 +6154,12 @@ function AIRBOSS:OnEventLand(EventData) else + -- We did land. + playerData.landed=true + + -- Switch attitude monitor off if on. + playerData.attitudemonitor=false + -- Coordinate at landing event. local coord=playerData.unit:GetCoordinate() @@ -6025,15 +6176,6 @@ function AIRBOSS:OnEventLand(EventData) 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, 75) - - -- 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 if gdataX0 then @@ -6042,22 +6184,38 @@ function AIRBOSS:OnEventLand(EventData) 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.", X, Z, rho) - self:T(self.lid..text) + -- Check carrier type. + if self.carriertype==AIRBOSS.CarrierType.TARAWA then - -- We did land. - playerData.landed=true + -- Power "Idle". + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.IDLE, false, 2) - -- Unkonwn step until we now more. - playerData.step=AIRBOSS.PatternStep.UNDEFINED + -- Next step debrief. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) - -- 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) + else + + -- Get wire. We additionally shift the landing coord back because landing event for players is unfortunately delayed. + local wire=self:_GetWire(coord, 75) + + -- 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 + + -- 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.", X, Z, rho) + self:T(self.lid..text) + + -- 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(nil, self._Trapped, {self, playerData}, 1) + + end end @@ -6070,26 +6228,33 @@ function AIRBOSS:OnEventLand(EventData) else - -- AI unit landed. + -------------------- + -- AI unit landed -- + -------------------- - -- Coordinate at landing event - local coord=EventData.IniUnit:GetCoordinate() + if self.carriertype~=AIRBOSS.CarrierType.TARAWA then - -- Debug mark of player landing coord. - local dist=coord:Get2DDistance(self:GetCoordinate()) - - -- Get wire - local wire=self:_GetWire(coord, 0) - - -- Aircraft type. - local _type=EventData.IniUnit:GetTypeName() - - -- Debug 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) + -- 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(coord, 0) + + -- Aircraft type. + local _type=EventData.IniUnit:GetTypeName() + + -- Debug 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) + + end -- AI always lands ==> remove unit from flight group and queues. self:_RemoveUnitFromFlight(EventData.IniUnit) + end end end @@ -6532,7 +6697,7 @@ function AIRBOSS:_Initial(playerData) local hint=string.format("Initial") -- Hook down for students. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then hint=hint.." - Hook down!" end @@ -6755,20 +6920,26 @@ function AIRBOSS:_DirtyUp(playerData) -- Hint alt and speed. local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) - -- Hint turn and set TACAN. + -- Dirty up. if playerData.difficulty==AIRBOSS.Difficulty.EASY then - hint=hint.."\nDirty up! Hook, gear and flaps down." + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + hint=hint.."\nFAF! Checks completed. Nozzles 50°." + else + hint=hint.."\nDirty up! Hook, gear and flaps down." + end end self:MessageToPlayer(playerData, hint, "MARSHAL", "") end -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. - 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) - + if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then + 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) + end + -- TODO: Make Fly Bullseye call if no automatic ICLS is active. -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). @@ -6815,12 +6986,19 @@ function AIRBOSS:_Bullseye(playerData) -- Hint follow the needles. if playerData.difficulty==AIRBOSS.Difficulty.EASY then - hint=hint..string.format("Intercept glideslope and follow the needles.") + if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then + hint=hint..string.format("Intercept glideslope and follow the needles.") + end end self:MessageToPlayer(playerData, hint, "MARSHAL", "") end + -- LSO expect spot 7.5 call + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.EXPECTSPOT75) + end + -- Next step: Groove Call the ball. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) @@ -6855,8 +7033,7 @@ function AIRBOSS:_BolterPattern(playerData) else nextstep=AIRBOSS.PatternStep.BULLSEYE end - self:_SetPlayerStep(playerData, nextstep) - + self:_SetPlayerStep(playerData, nextstep) end end @@ -6934,11 +7111,15 @@ function AIRBOSS:_Break(playerData, part) -- 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.."\nDirty up! Gear down, flaps down. Check hook down." + if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then + hint=hint.."\nDirty up! Gear down, flaps down. Check hook down." + end end + ]] self:MessageToPlayer(playerData, hint, "MARSHAL", "") end @@ -6966,9 +7147,14 @@ 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. + -- 1.5 NM from carrier is too far. local limit=UTILS.NMToMeters(-1.5) + -- For the tarawa we give a bit more space. + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + limit=UTILS.NMToMeters(-2.0) + end + -- 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? + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.DEPARTANDREENTER) + playerData.patternwo=true + -- Debrief. + self:_AddToDebrief(playerData, "Overshoot at wake - Pattern Waveoff!") + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) end end @@ -7185,7 +7385,7 @@ function AIRBOSS:_Final(playerData) groovedata.Step=playerData.step groovedata.Alt=alt groovedata.AoA=aoa - groovedata.GSE=self:_Glideslope(playerData.unit, 3.5) + groovedata.GSE=self:_Glideslope(playerData.unit) groovedata.LUE=self:_Lineup(playerData.unit, true) groovedata.Roll=roll groovedata.Rhdg=relhead @@ -7233,7 +7433,7 @@ function AIRBOSS:_Groove(playerData) local lineupError=self:_Lineup(playerData.unit, true) -- Glideslope. - local glideslopeError=self:_Glideslope(playerData.unit, 3.5) + local glideslopeError=self:_Glideslope(playerData.unit) -- Get AoA. local AoA=playerData.unit:GetAoA() @@ -7281,8 +7481,7 @@ function AIRBOSS:_Groove(playerData) playerData.valid=true -- Next step: in the middle. - playerData.step=AIRBOSS.PatternStep.GROOVE_IM - playerData.warning=nil + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IM) elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then @@ -7295,8 +7494,7 @@ function AIRBOSS:_Groove(playerData) playerData.groove.IM=groovedata -- Next step: in close. - playerData.step=AIRBOSS.PatternStep.GROOVE_IC - playerData.warning=nil + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IC) elseif rho<=RIC and playerData.step==AIRBOSS.PatternStep.GROOVE_IC then @@ -7308,9 +7506,8 @@ function AIRBOSS:_Groove(playerData) -- Store data. playerData.groove.IC=groovedata - -- Next step: AR at the ramp. - playerData.step=AIRBOSS.PatternStep.GROOVE_AR - playerData.warning=nil + -- Next step: AR at the ramp. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_AR) elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then @@ -7323,8 +7520,70 @@ function AIRBOSS:_Groove(playerData) playerData.groove.AR=groovedata -- Next step: in the wires. - playerData.step=AIRBOSS.PatternStep.GROOVE_IW - playerData.warning=nil + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_AL) + else + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IW) + end + + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AL then + + -- Store data. + playerData.groove.AL=groovedata + + -- Get zone abeam LDG spot. + local ZoneALS=self:_GetZoneAbeamLandingSpot() + + -- Get player velocity in km/h. + local vplayer=playerData.unit:GetVelocityKMH() + + -- Get carrier velocity in km/h. + local vcarrier=self.carrier:GetVelocityKMH() + + -- Speed difference. + local dv=math.abs(vplayer-vcarrier) + + -- Stable when speed difference < 10 km/h. + local stable=dv<10 + + -- Check if player is inside the zone. + if playerData.unit:IsInZone(ZoneALS) and stable then + + -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.CLEAREDTOLAND) + + -- Next step: Level cross. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_LC) + end + + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_LC then + + -- Store data. + playerData.groove.LC=groovedata + + -- Get zone primary LDG spot. + local ZoneLS=self:_GetZoneLandingSpot() + + -- Get player velocity in km/h. + local vplayer=playerData.unit:GetVelocityKMH() + + -- Get carrier velocity in km/h. + local vcarrier=self.carrier:GetVelocityKMH() + + -- Speed difference. + local dv=math.abs(vplayer-vcarrier) + + -- Stable when v<5 km/h. + local stable=dv<5 + + -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. + if playerData.unit:IsInZone(ZoneLS) and stable and playerData.warning==false then + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.STABILIZED) + playerData.warning=true + end + + -- We keep it in this step until landed. + end -------------- @@ -7464,8 +7723,7 @@ function AIRBOSS:_Groove(playerData) end -- Next step: debrief. - playerData.step=AIRBOSS.PatternStep.DEBRIEF - playerData.warning=nil + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) end @@ -7488,22 +7746,35 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- Assume we're all good. local waveoff=false + -- Parameters + local glMax= 1.8 + local glMin=-1.2 + local luAbs= 3.0 + + -- For the harrier, we allow a bit more room. + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + glMax= 4.0 + glMin=-3.0 + luAbs= 5.0 + return false + end + -- Too high or too low? - if glideslopeError>1.8 then - local text=string.format("Wave off due to glideslope error %.2f > 1.8 degrees!", glideslopeError) + if glideslopeError>glMax then + local text=string.format("Wave off due to glideslope error %.2f > %.1f degrees!", glideslopeError, glMax) 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 glideslope error %.2f < -1.2 degrees!", glideslopeError) + elseif glideslopeError3 then - local text=string.format("Wave off due to line up error |%.1f| > 3 degrees!", lineupError) + if math.abs(lineupError)>luAbs then + local text=string.format("Wave off due to line up error |%.1f| > %.1f degrees!", lineupError, luAbs) self:T(self.lid..string.format("%s: %s", playerData.name, text)) self:_AddToDebrief(playerData, text) waveoff=true @@ -7536,8 +7807,31 @@ end -- @return boolean If true, we have a foul deck. function AIRBOSS:_CheckFoulDeck(playerData) + -- Assume no check necessary. + local check=false + + -- Case I/II: Check is done, when AC is at the wake according to NATOPS. At the wake we switch to final. + if playerData.case<3 and playerData.step==AIRBOSS.PatternStep.FINAL then + check=true + end + + -- Case III: Check is done at 3/4 NM according to NATOPS. + if playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC then + check=true + end + + -- AV-8B check until + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + if playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_AL then + check=true + end + end + -- Check if player was already waved off. Should not be necessary as player step is set to debrief afterwards! - if playerData.fouldeckwo==true then + if playerData.fouldeckwo==true or check==false then -- Player was already waved off. return end @@ -7545,6 +7839,11 @@ function AIRBOSS:_CheckFoulDeck(playerData) -- Landing runway zone. local runway=self:_GetZoneRunwayBox() + -- For AB-8B we just check the primary landing spot. + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + runway=self:_GetZoneLandingSpot() + end + -- Scan radius. local R=250 @@ -7628,7 +7927,16 @@ 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(7, FB+90) + local stern=self:GetCoordinate() + + -- Stern coordinate (sterndist<0). + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- Tarawa: Translate 8 meters port. + stern=stern:Translate(self.carrierparam.sterndist, hdg):Translate(8, FB-90) + else + -- Stennis: translate 7 meters starboard wrt Final bearing. + stern=stern:Translate(self.carrierparam.sterndist, hdg):Translate(7, FB+90) + end -- Set altitude. stern:SetAltitude(self.carrierparam.deckheight) @@ -8127,7 +8435,7 @@ function AIRBOSS:_GetZoneCarrierBox() return zone end ---- Get zone of landing runway +--- Get zone of landing runway. -- @param #AIRBOSS self -- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. function AIRBOSS:_GetZoneRunwayBox() @@ -8159,6 +8467,73 @@ function AIRBOSS:_GetZoneRunwayBox() return zone end + +--- Get zone of primary abeam landing position of USS Tarawa. Box length and width 30 meters. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. +function AIRBOSS:_GetZoneAbeamLandingSpot() + + -- Primary landing Spot coordinate. + local S=self:_GetOptLandingCoordinate() + + -- Current carrier heading. + local FB=self:GetFinalBearing(false) + + -- Coordinate array. + local p={} + + -- Points. + p[1]=S:Translate( 15, FB):Translate(15, FB+90) -- Top-Right + p[2]=S:Translate(-15, FB):Translate(15, FB+90) -- Bottom-Right + p[3]=S:Translate(-15, FB):Translate(15, FB-90) -- Bottom-Left + p[4]=S:Translate( 15, FB):Translate(15, FB-90) -- Top-Left + + -- 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("Abeam Landing Spot Zone", vec2) + + return zone +end + + +--- Get zone of the primary landing spot of the USS Tarawa. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. +function AIRBOSS:_GetZoneLandingSpot() + + -- Primary landing Spot coordinate. + local S=self:_GetLandingSpotCoordinate() + + -- Current carrier heading. + local FB=self:GetFinalBearing(false) + + -- Coordinate array. + local p={} + + -- Points. + p[1]=S:Translate( 10, FB):Translate(10, FB+90) -- Top-Right + p[2]=S:Translate(-10, FB):Translate(10, FB+90) -- Bottom-Right + p[3]=S:Translate(-10, FB):Translate(10, FB-90) -- Bottom-Left + p[4]=S:Translate( 10, FB):Translate(10, FB-90) -- Top-left + + -- 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 Spot Zone", vec2) + + return zone +end + + --- Get holding zone of player. -- @param #AIRBOSS self -- @param #number case Recovery case. @@ -8174,7 +8549,7 @@ function AIRBOSS:_GetZoneHolding(case, stack) return nil end - -- Pattern alitude. + -- Pattern altitude. local patternalt, c1, c2=self:_GetMarshalAltitude(stack, case) -- Select case. @@ -8192,6 +8567,11 @@ function AIRBOSS:_GetZoneHolding(case, stack) -- Create holding zone. zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", Post:GetVec2(), self.marshalradius) + + -- Delta pattern. + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", self.carrier:GetVec2(), UTILS.NMToMeters(5)) + end else -- CASE II/II @@ -8238,8 +8618,20 @@ function AIRBOSS:_GetZoneCommence(case) -- Three position local Three=self:GetCoordinate():Translate(D, hdg+275) + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + + local Dx=UTILS.NMToMeters(2.25) + + local Dz=UTILS.NMToMeters(2.25) + + R=UTILS.NMToMeters(1) + + Three=self:GetCoordinate():Translate(Dz, hdg-90):Translate(Dx, hdg-180) + + end + -- Create holding zone. - zone=ZONE_RADIUS:New("CASE I Holding Zone", Three:GetVec2(), R) + zone=ZONE_RADIUS:New("CASE I Commence Zone", Three:GetVec2(), R) else -- Case II/III @@ -8290,6 +8682,8 @@ function AIRBOSS:_AttitudeMonitor(playerData) 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_AL or + playerData.step==AIRBOSS.PatternStep.GROOVE_LC or playerData.step==AIRBOSS.PatternStep.GROOVE_IW then step=self:_GS(step,-1) end @@ -8311,11 +8705,19 @@ function AIRBOSS:_AttitudeMonitor(playerData) 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_AL or + playerData.step==AIRBOSS.PatternStep.GROOVE_LC or playerData.step==AIRBOSS.PatternStep.GROOVE_IW then local lineup=self:_Lineup(playerData.unit, true) - local glideslope=self:_Glideslope(playerData.unit, 3.5) + local glideslope=self:_Glideslope(playerData.unit) local dist=self:_GetOptLandingCoordinate():Get2DDistance(playerData.unit) - text=text..string.format("\nDist=%.1f m Alt=%.1f m", dist, self:_GetAltCarrier(playerData.unit)) + -- Get player velocity in km/h. + local vplayer=playerData.unit:GetVelocityKMH() + -- Get carrier velocity in km/h. + local vcarrier=self.carrier:GetVelocityKMH() + -- Speed difference. + local dv=math.abs(vplayer-vcarrier) + text=text..string.format("\nDist=%.1f m Alt=%.1f m delta|V|=%.1f km/h", dist, self:_GetAltCarrier(playerData.unit), dv) 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("\nGrade: %s %.1f PT - %s", grade, points, analysis) @@ -8334,11 +8736,15 @@ end -- @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 - + if optangle==nil then + if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then + optangle=3.0 + else + optangle=3.5 + end + end -- Landing coordinate - local landingcoord=self:_GetOptLandingCoordinate() + local landingcoord=self:_GetOptLandingCoordinate() -- Distance from stern to aircraft. local x=unit:GetCoordinate():Get2DDistance(landingcoord) @@ -8346,6 +8752,11 @@ function AIRBOSS:_Glideslope(unit, optangle) -- Altitude of unit corrected by the deck height of the carrier. local h=self:_GetAltCarrier(unit) + -- Harrier should be 40-50 ft above the deck. + if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then + h=unit:GetAltitude()-(UTILS.FeetToMeters(50)+self.carrierparam.deckheight+2) + end + -- Glide slope. local glideslope=math.atan(h/x) @@ -8429,7 +8840,7 @@ function AIRBOSS:_GetAltCarrier(unit) return h end ---- Get optimal landing position of the aircraft. Usually between second and third wire. +--- Get optimal landing position of the aircraft. Usually between second and third wire. In case of Tarawa we take the abeam landing spot 120 ft abeam the 7.5 position. -- @param #AIRBOSS self -- @return Core.Point#COORDINATE Optimal landing coordinate. function AIRBOSS:_GetOptLandingCoordinate() @@ -8437,16 +8848,52 @@ 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) + -- Final bearing. + local FB=self:GetFinalBearing(false) + + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + + -- Landing 100 ft abeam, 120 ft alt. + stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) + + -- Alitude 120 ft. + stern:SetAltitude(UTILS.FeetToMeters(120)) + + else + + -- 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, FB, true) + end + end return stern end +--- Get landing spot on Tarawa. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Primary landing spot coordinate. +function AIRBOSS:_GetLandingSpotCoordinate() + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + + -- Landing 100 ft abeam, 120 alt. + local hdg=self:GetHeading() + + -- Primary landing spot 7.5 + stern=stern:Translate(57, hdg):SetAltitude(self.carrierparam.deckheight) + + 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. @@ -8723,7 +9170,6 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Advice time. local advice=0 - local text="" -- Glideslope high/low calls. --TODO: introduce GSE enumerator values. @@ -8744,11 +9190,9 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, false) advice=advice+AIRBOSS.LSOCall.POWER.duration else - text="Good altitude." + -- "Good altitude." end - text=text..string.format(" Glideslope Error = %.2f°", glideslopeError) - text=text.."\n" -- Lineup left/right calls. -- TODO: introduce LUE enumerator values. @@ -8769,48 +9213,45 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RIGHTFORLINEUP, false) advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration else - text=text.."Good lineup." + -- "Good lineup." end - text=text..string.format(" Lineup Error = %.1f°\n", lineupError) - -- Get current AoA. local AOA=playerData.unit:GetAoA() -- Get aircraft AoA parameters. 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 - --S=underline("SLO") - elseif AOA>acaoa.Slow then - -- "Your're slow." - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.SLOW, false) - advice=advice+AIRBOSS.LSOCall.SLOW.duration - --S="SLO" - elseif AOA>acaoa.OnSpeedMax then - -- No call. - --S=little("SLO") - elseif AOAacaoa.SLOW then + -- "Your're slow!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.SLOW, true) + advice=advice+AIRBOSS.LSOCall.SLOW.duration + --S=underline("SLO") + elseif AOA>acaoa.Slow then + -- "Your're slow." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.SLOW, false) + advice=advice+AIRBOSS.LSOCall.SLOW.duration + --S="SLO" + elseif AOA>acaoa.OnSpeedMax then + -- No call. + --S=little("SLO") + elseif AOA Date: Mon, 4 Feb 2019 23:15:12 +0100 Subject: [PATCH 153/485] AIRBOSS v0.95 - Fixed bug in counting units - Carrier will only turn into the wind if wind > 0.1 m/s --- Moose Development/Moose/Ops/Airboss.lua | 62 ++++++++++++++++++------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 7d09bcd49..37a75cf4c 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1651,7 +1651,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.4" +AIRBOSS.version="0.9.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -4928,7 +4928,10 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) -- 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() + local _,vwind=self:GetCoordinate():GetWind(50) + if vwind>0.1 then + brc=self:GetBRCintoWind() + end end -- Get charlie time estimate. @@ -5170,17 +5173,20 @@ function AIRBOSS:_GetFlightUnits(flight, onground) 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 + if units then + 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 + self:T2(self.lid..string.format("Unit %s is in AIR", unit:GetName())) + n=n+1 + end + else + -- Count units in air or on the ground. n=n+1 end - else - -- Count units in air or on the ground. - n=n+1 end end end @@ -5718,11 +5724,17 @@ function AIRBOSS:_RemoveUnitFromFlight(unit) if removed then - -- 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. + -- Get number of units (excluding section members). For AI only those that are still in air as we assume once they landed, they are out of the game. local _,nunits=self:_GetFlightUnits(flight, not flight.ai) + -- Number of flight elements still left. + local nelements=#flight.elements + + -- Debug info. + self:T(self.lid..string.format("Removed unit %s: nunits=%d, nelements=%d", unit:GetName(), nunits, nelements)) + -- Check if no units are left. - if nunits==0 then + if nunits==0 or nelements==0 then -- Remove flight from all queues. self:_RemoveFlight(flight) end @@ -10529,6 +10541,11 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck) -- Wind speed. local _,vwind=self:GetCoordinate():GetWind(50) + -- Check that wind is >= 0.1 m/s. + if vwind<0.1 then + return + end + -- Speed of carrier in m/s. local vtot=vdeck-vwind @@ -12036,9 +12053,9 @@ 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._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 + missionCommands.addCommandForGroup(gid, "Marshal Queue", _kneeboardPath, self._DisplayQueue, self, _unitName, "Marshal") -- F5 + missionCommands.addCommandForGroup(gid, "Pattern Queue", _kneeboardPath, self._DisplayQueue, self, _unitName, "Pattern") -- F6 + missionCommands.addCommandForGroup(gid, "Waiting Queue", _kneeboardPath, self._DisplayQueue, self, _unitName, "Waiting") -- F7 ------------------------- -- F10/Airboss// @@ -12753,9 +12770,8 @@ end --- Display marshal or pattern queue. -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. --- @param #table queue The queue to display. -- @param #string qname Name of the queue. -function AIRBOSS:_DisplayQueue(_unitname, queue, qname) +function AIRBOSS:_DisplayQueue(_unitname, qname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) @@ -12768,6 +12784,16 @@ function AIRBOSS:_DisplayQueue(_unitname, queue, qname) if playerData then + -- Queue to display. + local queue=nil + if qname=="Marshal" then + queue=self.Qmarshal + elseif qname=="Pattern" then + queue=self.Qpattern + elseif qname=="Waiting" then + queue=self.Qwaiting + end + -- Number of group and units in queue local Nqueue,nqueue=self:_GetQueueInfo(queue, playerData.case) From 83ed456c299cf6a0ba162f05dde5d4805ca9c0b8 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 5 Feb 2019 16:41:20 +0100 Subject: [PATCH 154/485] AB 0.9.5wip --- Moose Development/Moose/Ops/Airboss.lua | 202 +++++++++++++++++++----- 1 file changed, 159 insertions(+), 43 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 37a75cf4c..6a1943716 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -58,10 +58,11 @@ -- -- ## IMPORTANT -- --- Due to technical restrictions of DCS make sure you have: +-- Some important restrictions of DCS you should be aware of: -- --- * Each player slot in a separate group. DCS does only allow to send messages to groups and not to individual units. +-- * Each player slot (client) should be in a separate group as DCS does only allow for sending messages to groups and not 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. +-- * The modex (tail number) of an aircraft should be changed dynamically in the mission by a player. Unfortunately, there is no way to get this information via scripting API functions. -- -- ## Youtube Videos -- @@ -73,7 +74,7 @@ -- Lex explaining Boat Ops: -- -- * [( DCS HORNET ) Some boat ops basics VID 1](https://www.youtube.com/watch?v=LvGQS-3AzMc) --- * [( DCS HORNET ) Some boat ops basics VID 1](https://www.youtube.com/watch?v=bN44wvtRsw0) +-- * [( DCS HORNET ) Some boat ops basics VID 2](https://www.youtube.com/watch?v=bN44wvtRsw0) -- -- Jabbers Case I and III Recovery Tutorials: -- @@ -1607,10 +1608,11 @@ AIRBOSS.Difficulty={ --- Parameters of an element in a flight group. -- @type AIRBOSS.FlightElement -- @field Wrapper.Unit#UNIT unit Aircraft unit. +-- @field #string unitname Name of the unit. -- @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. +-- @field #boolean recovered If true, element was successfully recovered. --- Player data table holding all important parameters of each player. -- @type AIRBOSS.PlayerData @@ -1651,18 +1653,21 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.5" +AIRBOSS.version="0.9.5wip" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - + +-- TODO: Despawn AI after landing/engine shutdown option. +-- TODO: Do not remove recovered elements but only set switch. Remove only groups which are completely recovered. +-- TODO: Handle cases where AI crashes on carrier deck. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! -- TODO: Player eject and crash debrief "gradings". -- 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: 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. @@ -5511,60 +5516,78 @@ function AIRBOSS:_InitPlayer(playerData, step) end ---- Get flight from group. +--- Get flight from group in a queue. -- @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.FlightGroup Flight group. --- @return #number Queue index. +-- @return #AIRBOSS.FlightGroup Flight group or nil. +-- @return #number Queue index or nil. 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.FlightGroup - - if flight.groupname==name then - return flight, i - end - end + if group then - self:T2(self.lid..string.format("WARNING: Flight group %s could not be found in queue.", name)) + -- Group name + local name=group:GetName() + + -- Loop over all flight groups in queue + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + + 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)) + end + + self:T2(self.lid..string.format("WARNING: Flight group could not be found in queue. Group is nil!")) 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 #AIRBOSS.FlightElement Element of the flight. -- @return #number Element index. -function AIRBOSS:_GetFlightElement(unitname, flight) +function AIRBOSS:_GetFlightElement(unitname) - -- Loop over all elements in flight group. - for i,_element in pairs(flight.elements) do - local element=_element --#AIRBOSS.FlightElement - - if element.unit:GetName()==unitname then - return element, i + -- Get the unit. + local unit=UNIT:FindByName(unitname) + + -- Check if unit exists. + if unit then + + -- Get flight element from all flights. + local flight=self:_GetFlightFromGroupInQueue(unit:GetGroup(), self.flights) + + -- Check if fight exists. + if flight then + + -- Loop over all elements in flight group. + for i,_element in pairs(flight.elements) 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)) 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. -- @return #boolean If true, element could be removed or nil otherwise. -function AIRBOSS:_RemoveFlightElement(unitname, flight) +function AIRBOSS:_RemoveFlightElement(unitname) -- Get table index. - local element,idx=self:_GetFlightElement(unitname, flight) + local element,idx=self:_GetFlightElement(unitname) if idx then table.remove(flight.elements, idx) @@ -5626,6 +5649,31 @@ function AIRBOSS:_RemoveDeadFlightGroups() end +--- Chech if all elements of a flight were recovered. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight +-- @return #boolean If true, all elements landed. +function AIRBOSS:_CheckAllRecovered(flight) + + for _,_element in pair(flight.elements) do + local element=_element --#AIROBSS.FlightElement + if not element.recovered then + return false + end + end + + return true +end + +--- Sets flag recovered=true for a flight element, which was successfully recovered (landed). +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The aircraft unit that was recovered. +-- @return #boolean If true, all flight elements were rerovered. +function AIRBOSS:_RecoveredElement(unit) + local element=self:_GetFlightElement(unit:GetName()) --#AIRBOSS.FlightElement + element.recovered=true +end + --- 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. @@ -5720,7 +5768,7 @@ function AIRBOSS:_RemoveUnitFromFlight(unit) if flight then -- Remove element from flight group. - local removed=self:_RemoveFlightElement(unit:GetName(), flight) + local removed=self:_RemoveFlightElement(unit:GetName()) if removed then @@ -6200,7 +6248,7 @@ function AIRBOSS:OnEventLand(EventData) if self.carriertype==AIRBOSS.CarrierType.TARAWA then -- Power "Idle". - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.IDLE, false, 2) + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.IDLE, false, 1) -- Next step debrief. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) @@ -6212,9 +6260,9 @@ function AIRBOSS:OnEventLand(EventData) -- 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 + --if wire>4 then + -- self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.BOLTER) + --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) @@ -6222,7 +6270,8 @@ function AIRBOSS:OnEventLand(EventData) self:T(self.lid..text) -- Unkonwn step until we now more. - playerData.step=AIRBOSS.PatternStep.UNDEFINED + playerData.step=AIRBOSS.PatternStep.UNDEFINED + playerData.warning=nil -- Call trapped function in 1 second to make sure we did not bolter. SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1) @@ -6271,11 +6320,67 @@ function AIRBOSS:OnEventLand(EventData) end end +--- Airboss event handler for event that a unit shuts down its engines. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +--function AIRBOSS:OnEventPlayerLeaveUnit(EventData) +function AIRBOSS:OnEventEngineShutdown(EventData) + self:F3({eventengineshutdown=EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."ENGINESHUTDOWN: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."ENGINESHUTDOWN: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."ENGINESHUTDOWN: player = "..tostring(_playername)) + + if _unit and _playername then + + -- Debug message. + self:T(self.lid..string.format("Player %s shutted down its engines!",_playername)) + + else + + -- Debug message. + self:T2(self.lid..string.format("AI unit %s shutted down its engines!", _unitName)) + + end + +end + +--- Airboss event handler for event that a unit takes off. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +--function AIRBOSS:OnEventPlayerLeaveUnit(EventData) +function AIRBOSS:OnEventTakeoff(EventData) + self:F3({eventtakeoff=EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."TAKEOFF: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."TAKEOFF: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."TAKEOFF: player = "..tostring(_playername)) + + if _unit and _playername then + + -- Debug message. + self:T(self.lid..string.format("Player %s took off!",_playername)) + + else + + -- Debug message. + self:T2(self.lid..string.format("AI unit %s took off!", _unitName)) + + end + +end + --- Airboss event handler for event crash. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData function AIRBOSS:OnEventCrash(EventData) - self:F3({eventland = EventData}) + self:F3({eventcrash = EventData}) local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) @@ -8066,6 +8171,8 @@ function AIRBOSS:_Trapped(playerData) -- A-4E gets slowed down much faster the the F/A-18C! dcorr=56 end + + -- Get wire. local wire=self:_GetWire(coord, dcorr) -- Debug. @@ -8074,6 +8181,15 @@ function AIRBOSS:_Trapped(playerData) -- Call this function again until v < threshold. Player comes to a standstill ==> Get wire! if v>5 then + + -- Check if we passed all wires. + if wire>4 and v>10 and not playerData.warning then + -- Looks like we missed the wires ==> Bolter! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.BOLTER) + playerData.warning=true + end + + -- Call function again and check if converged or back in air. SCHEDULER:New(nil, self._Trapped, {self, playerData}, 0.1) return end From 70d1f96681e1b8b0310cc590b967018a9e8595a8 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 6 Feb 2019 00:12:24 +0100 Subject: [PATCH 155/485] AB v0.95wip --- Moose Development/Moose/Core/Spawn.lua | 6 +-- .../Moose/Functional/Warehouse.lua | 24 ++++----- Moose Development/Moose/Ops/Airboss.lua | 52 ++++++++++++------- 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index d65aae7a2..2bc851087 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1615,12 +1615,12 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT -- Get parking data. local parkingdata=SpawnAirbase:GetParkingSpotsTable(termtype) - self:T2(string.format("Parking at %s, terminal type %s:", SpawnAirbase:GetName(), tostring(termtype))) + self:E(string.format("Parking at %s, terminal type %s:", SpawnAirbase:GetName(), tostring(termtype))) for _,_spot in pairs(parkingdata) do - self:T2(string.format("%s, Termin Index = %3d, Term Type = %03d, Free = %5s, TOAC = %5s, Term ID0 = %3d, Dist2Rwy = %4d", + self:E(string.format("%s, Termin Index = %3d, Term Type = %03d, Free = %5s, TOAC = %5s, Term ID0 = %3d, Dist2Rwy = %4d", SpawnAirbase:GetName(), _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy)) end - self:T(string.format("%s at %s: free parking spots = %d - number of units = %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), nfree, nunits)) + self:E(string.format("%s at %s: free parking spots = %d - number of units = %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), nfree, nunits)) -- Set this to true if not enough spots are available for emergency air start. local _notenough=false diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index c2b21cdd7..4064e2f9c 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1719,12 +1719,12 @@ WAREHOUSE.Quantity = { } --- Warehouse database. Note that this is a global array to have easier exchange between warehouses. --- @type WAREHOUSE.db +-- @type _WAREHOUSEDB -- @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 Warehouses Table holding all defined @{#WAREHOUSE} objects by their unique ids. -WAREHOUSE.db = { +_WAREHOUSEDB = { AssetID = 0, Assets = {}, WarehouseID = 0, @@ -1835,10 +1835,10 @@ function WAREHOUSE:New(warehouse, alias) self.warehouse=warehouse -- Increase global warehouse counter. - WAREHOUSE.db.WarehouseID=WAREHOUSE.db.WarehouseID+1 + _WAREHOUSEDB.WarehouseID=_WAREHOUSEDB.WarehouseID+1 -- Set unique ID for this warehouse. - self.uid=WAREHOUSE.db.WarehouseID + self.uid=_WAREHOUSEDB.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()) @@ -1854,7 +1854,7 @@ function WAREHOUSE:New(warehouse, alias) self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone", self.warehouse:GetName()), warehouse:GetVec2(), 250) -- Add warehouse to database. - WAREHOUSE.db.Warehouses[self.uid]=self + _WAREHOUSEDB.Warehouses[self.uid]=self ----------------------- --- FSM Transitions --- @@ -2933,7 +2933,7 @@ end -- @param #number uid The unique ID of the warehouse. -- @return #WAREHOUSE The warehouse object or nil if no warehouse exists. function WAREHOUSE:FindWarehouseInDB(uid) - return WAREHOUSE.db.Warehouses[uid] + return _WAREHOUSEDB.Warehouses[uid] end --- Find nearest warehouse in service, i.e. warehouses which are not started, stopped or destroyed are not considered. @@ -2977,7 +2977,7 @@ function WAREHOUSE:FindNearestWarehouse(MinAssets, Descriptor, DescriptorValue, -- Loop over all warehouses. local nearest=nil local distmin=nil - for wid,warehouse in pairs(WAREHOUSE.db.Warehouses) do + for wid,warehouse in pairs(_WAREHOUSEDB.Warehouses) do local warehouse=warehouse --#WAREHOUSE -- Distance from this warehouse to the other warehouse. @@ -3029,7 +3029,7 @@ function WAREHOUSE:FindAssetInDB(group) if aid~=nil then - local asset=WAREHOUSE.db.Assets[aid] + local asset=_WAREHOUSEDB.Assets[aid] self:E({asset=asset}) if asset==nil then self:_ErrorMessage(string.format("ERROR: Asset for group %s not found in the data base!", group:GetName()), 0) @@ -3210,7 +3210,7 @@ end -- @param #string Event Event. -- @param #string To To state. function WAREHOUSE:onafterStatus(From, Event, To) - self:I(self.wid..string.format("Checking status of warehouse %s. Current FSM state %s. Global warehouse assets = %d.", self.alias, self:GetState(), #WAREHOUSE.db.Assets)) + self:I(self.wid..string.format("Checking status of warehouse %s. Current FSM state %s. Global warehouse assets = %d.", self.alias, self:GetState(), #_WAREHOUSEDB.Assets)) -- Check if any pending jobs are done and can be deleted from the self:_JobDone() @@ -3726,10 +3726,10 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, local asset={} --#WAREHOUSE.Assetitem -- Increase asset unique id counter. - WAREHOUSE.db.AssetID=WAREHOUSE.db.AssetID+1 + _WAREHOUSEDB.AssetID=_WAREHOUSEDB.AssetID+1 -- Set parameters. - asset.uid=WAREHOUSE.db.AssetID + asset.uid=_WAREHOUSEDB.AssetID asset.templatename=templategroupname asset.template=UTILS.DeepCopy(_DATABASE.Templates.Groups[templategroupname].Template) asset.category=Category @@ -3755,7 +3755,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, end -- Add asset to global db. - WAREHOUSE.db.Assets[asset.uid]=asset + _WAREHOUSEDB.Assets[asset.uid]=asset -- Add asset to the table that is retured. table.insert(assets,asset) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 6a1943716..d5927fb8d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -4253,7 +4253,7 @@ function AIRBOSS:_ScanCarrierZone() local unit=_unit --Wrapper.Unit#UNIT -- Necessary conditions to be met: - local airborne=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) @@ -5000,9 +5000,6 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Only collapse stacks above the new pattern flight. if mstack>stack then - - -- OLD: New stack is old stack minus one. - --local newstack=mstack-1 -- 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. @@ -5327,6 +5324,10 @@ function AIRBOSS:_PrintQueue(queue, name) local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, case)) text=text..string.format(" stackalt=%d ft", alt) end + for j,_element in pairs(flight.elements) do + local element=_element --#AIRBOSS.FlightElement + text=text..string.format("\n (%d) %s (%s): ai=%s, ballcall=%s, recovered=%s", j, element.onboard, element.unitname, tostring(element.ai), tostring(element.ballcall), tostring(element.recovered)) + end end end self:T(self.lid..text) @@ -5383,13 +5384,14 @@ function AIRBOSS:_CreateFlightGroup(group) local units=group:GetUnits() 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.unitname=unit:GetName() + element.onboard=flight.onboardnumbers[element.unitname] element.ballcall=false 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)) + element.recovered=false + text=text..string.format("\n[%d] %s onboard #%s, AI=%s", i, element.unitname, tostring(element.onboard), tostring(element.ai)) table.insert(flight.elements, element) end self:T(self.lid..text) @@ -5548,8 +5550,9 @@ end --- Get element in flight. -- @param #AIRBOSS self -- @param #string unitname Name of the unit. --- @return #AIRBOSS.FlightElement Element of the flight. --- @return #number Element index. +-- @return #AIRBOSS.FlightElement Element of the flight or nil. +-- @return #number Element index or nil. +-- @return #AIRBOSS.FlightGroup The Flight group or nil function AIRBOSS:_GetFlightElement(unitname) -- Get the unit. @@ -5569,7 +5572,7 @@ function AIRBOSS:_GetFlightElement(unitname) local element=_element --#AIRBOSS.FlightElement if element.unit:GetName()==unitname then - return element, i + return element, i, flight end end @@ -5577,7 +5580,7 @@ function AIRBOSS:_GetFlightElement(unitname) end end - return nil, nil + return nil, nil, nil end --- Get element in flight. @@ -5587,7 +5590,7 @@ end function AIRBOSS:_RemoveFlightElement(unitname) -- Get table index. - local element,idx=self:_GetFlightElement(unitname) + local element,idx, flight=self:_GetFlightElement(unitname) if idx then table.remove(flight.elements, idx) @@ -5655,7 +5658,7 @@ end -- @return #boolean If true, all elements landed. function AIRBOSS:_CheckAllRecovered(flight) - for _,_element in pair(flight.elements) do + for _,_element in pairs(flight.elements) do local element=_element --#AIROBSS.FlightElement if not element.recovered then return false @@ -6314,7 +6317,13 @@ function AIRBOSS:OnEventLand(EventData) end -- AI always lands ==> remove unit from flight group and queues. - self:_RemoveUnitFromFlight(EventData.IniUnit) + self:_RecoveredElement(EventData.IniUnit) + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + local allrecovered=self:_CheckAllRecovered(flight) + if allrecovered then + self:_RemoveFlightFromQueue(self.Qpattern, flight) + end + --self:_RemoveUnitFromFlight(EventData.IniUnit) end end @@ -10372,8 +10381,14 @@ function AIRBOSS:_Debrief(playerData) -- 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) + -- Set recovered flag. + self:_RecoveredElement(playerData.unit) + + local allrecovered=self:_CheckAllRecovered(playerData) + + if allrecovered then + self:_RemoveFlightFromQueue(self.Qpattern, playerData) + end end -- Increase number of passes. @@ -10689,7 +10704,8 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck) -- 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. + -- Let the carrier make a detour from its route but return to its current position. + -- TODO: Add downwind speed self:CarrierDetour(pos1, speedknots, true) -- Set switch that we are currently turning into the wind. @@ -13089,7 +13105,7 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) -- Get atmospheric data at carrier location. local T=coord:GetTemperature() local P=coord:GetPressure() - local Wd,Ws=coord:GetWind() + local Wd,Ws=coord:GetWind(50) -- Get Beaufort wind scale. local Bn,Bd=UTILS.BeaufortScale(Ws) From 6992e47a6683455c9e31bd04a102ddd84c7059c3 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 6 Feb 2019 16:16:29 +0100 Subject: [PATCH 156/485] AB 0.95wip --- Moose Development/Moose/Ops/Airboss.lua | 353 ++++++++++++++++++------ 1 file changed, 268 insertions(+), 85 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index d5927fb8d..71be2db27 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -85,10 +85,9 @@ -- ### 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 next step after a pattern wave off during Case II or III recovery? A: Go back to start! -- * 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. -- @@ -197,6 +196,7 @@ -- @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. +-- @field #boolean despawnshutdown Despawn group after engine shutdown. -- @extends Core.Fsm#FSM --- Be the boss! @@ -926,6 +926,7 @@ AIRBOSS = { collisiondist = nil, Tmessage = nil, soundfolder = nil, + despawnshutdown= nil, } --- Aircraft types capable of landing on carrier (human+AI). @@ -1604,6 +1605,7 @@ AIRBOSS.Difficulty={ -- @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. +-- @field #string name Player name or name of first AI unit. --- Parameters of an element in a flight group. -- @type AIRBOSS.FlightElement @@ -1613,11 +1615,11 @@ AIRBOSS.Difficulty={ -- @field #string onboard Onboard number of the aircraft. -- @field #boolean ballcall If true, flight called the ball in the groove. -- @field #boolean recovered If true, element was successfully recovered. +-- @field #boolean isseclead If true, element is the section 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.Unit#UNIT unit Aircraft of the player. -- @field Wrapper.Client#CLIENT client Client object of player. -- @field #string callsign Callsign of player. -- @field #string difficulty Difficulty level. @@ -1659,14 +1661,14 @@ AIRBOSS.version="0.9.5wip" -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Despawn AI after landing/engine shutdown option. --- TODO: Do not remove recovered elements but only set switch. Remove only groups which are completely recovered. --- TODO: Handle cases where AI crashes on carrier deck. +-- TODO: Handle cases where AI crashes on carrier deck ==> Clean up deck. -- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! -- TODO: Player eject and crash debrief "gradings". --- 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. +-- DONE: Despawn AI after engine shutdown option. +-- DONE: What happens when section lead or member dies? +-- DONE: Do not remove recovered elements but only set switch. Remove only groups which are completely recovered. -- DONE: 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? @@ -1810,6 +1812,9 @@ function AIRBOSS:New(carriername, alias) -- Airboss is a nice guy. self:SetAirbossNiceGuy() + -- No despawn after engine shutdown by default. + self:SetDespawnOnEngineShutdown(false) + -- Mission uses static weather by default. self:SetStaticWeather() @@ -2433,6 +2438,20 @@ function AIRBOSS:SetAirbossNiceGuy(switch) return self end +--- Despawn AI groups after they they shut down their engines +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, AI groups are despawned. +-- @return #AIRBOSS self +function AIRBOSS:SetDespawnOnEngineShutdown(switch) + if switch==true or switch==nil then + self.despawnshutdown=true + else + self.despawnshutdown=false + end + 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. @@ -2897,10 +2916,12 @@ function AIRBOSS:onafterStart(From, Event, To) -- Handle events. self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.EngineShutdown) + self:HandleEvent(EVENTS.Takeoff) self:HandleEvent(EVENTS.Crash) self:HandleEvent(EVENTS.Ejection) self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) - --self:HandleEvent(EVENTS.MissionEnd) + self:HandleEvent(EVENTS.MissionEnd) -- Start status check in 1 second. self:__Status(1) @@ -4041,7 +4062,6 @@ function AIRBOSS:_GetNextMarshalFight() return nil end - --- Check marshal and pattern queues. -- @param #AIRBOSS self function AIRBOSS:_CheckQueue() @@ -4086,13 +4106,20 @@ function AIRBOSS:_CheckQueue() -- Check if carrier is currently in recovery mode. if not self:IsRecovering() then + + ----------------------------- + -- Switching Recovery Case -- + ----------------------------- -- 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 + -- TODO: In principle this should be done/necessary only if case 1-->2/3 or 2/3-->1, right? + -- When recovery switches from 2->3 or 3-->2 nothing changes in the marshal stack. + + -- Check if a change of stack is necessary. + if (flight.case==1 and self.case>1) or (flight.case>1 and self.case==1) then -- Remove flight from marshal queue. local removed=self:_RemoveFlightFromQueue(self.Qmarshal, flight) @@ -4113,8 +4140,15 @@ function AIRBOSS:_CheckQueue() 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 + break + + elseif flight.case~=self.case then + + -- This should handle 2-->3 or 3-->2 + flight.case=self.case + end + end end @@ -4173,6 +4207,7 @@ function AIRBOSS:_CheckQueue() 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 @@ -4447,19 +4482,27 @@ function AIRBOSS:_MarshalPlayer(playerData, stack) self:_AddMarshalGroup(playerData, stack) -- Set step to holding. - playerData.step=AIRBOSS.PatternStep.HOLDING - playerData.warning=nil + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.HOLDING) -- 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 - -- TODO: inform section members. local flight=_flight --#AIRBOSS.PlayerData - flight.case=playerData.case - flight.step=AIRBOSS.PatternStep.HOLDING + + -- TODO: Inform player? Should be done by lead via radio? + + -- Set step. + self:_SetPlayerStep(flight, AIRBOSS.PatternStep.HOLDING) + + -- Holding to nil, until arrived. flight.holding=nil + + -- Set case to that of lead. + flight.case=playerData.case + + -- Set stack flag. flight.flag:Set(stack) end @@ -5326,7 +5369,8 @@ function AIRBOSS:_PrintQueue(queue, name) end for j,_element in pairs(flight.elements) do local element=_element --#AIRBOSS.FlightElement - text=text..string.format("\n (%d) %s (%s): ai=%s, ballcall=%s, recovered=%s", j, element.onboard, element.unitname, tostring(element.ai), tostring(element.ballcall), tostring(element.recovered)) + text=text..string.format("\n (%d) %s (%s): ai=%s, ballcall=%s, recovered=%s", + j, element.onboard, element.unitname, tostring(element.ai), tostring(element.ballcall), tostring(element.recovered)) end end end @@ -5371,9 +5415,7 @@ function AIRBOSS:_CreateFlightGroup(group) flight.section={} 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() + flight.name=flight.group:GetUnit(1):GetName() --Will be overwritten in _Newplayer with player name if human player in the group. -- Note, this should be re-set elsewhere! flight.case=self.case @@ -5390,7 +5432,7 @@ function AIRBOSS:_CreateFlightGroup(group) element.onboard=flight.onboardnumbers[element.unitname] element.ballcall=false element.ai=not self:_IsHumanUnit(unit) - element.recovered=false + element.recovered=nil text=text..string.format("\n[%d] %s onboard #%s, AI=%s", i, element.unitname, tostring(element.onboard), tostring(element.ai)) table.insert(flight.elements, element) end @@ -5400,6 +5442,7 @@ function AIRBOSS:_CreateFlightGroup(group) if flight.ai then local onboard=flight.onboardnumbers[flight.seclead] flight.onboard=onboard + flight.elements[1].isseclead=true else flight.onboard=self:_GetOnboardNumberPlayer(group) end @@ -5652,29 +5695,85 @@ function AIRBOSS:_RemoveDeadFlightGroups() end ---- Chech if all elements of a flight were recovered. +--- Get the lead flight group of a flight group. -- @param #AIRBOSS self --- @param #AIRBOSS.FlightGroup flight --- @return #boolean If true, all elements landed. -function AIRBOSS:_CheckAllRecovered(flight) +-- @param #AIRBOSS.FlightGroup flight Flight group to check. +-- @return #AIRBOSS.FlightGroup Flight group of the leader or flight itself if no other leader. +function AIRBOSS:_GetLeadFlight(flight) - for _,_element in pairs(flight.elements) do + -- Init. + local lead=flight + + -- Only human players can be section leads of other players. + if flight.name~=flight.seclead then + lead=self.players[flight.seclead] + end + + return lead +end + +--- Check if all elements of a flight were recovered. This also checks potential section members. +-- If so, flight is removed from the queue. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group to check. +-- @return #boolean If true, all elements landed. +function AIRBOSS:_CheckSectionRecovered(flight) + + -- Nil check. + if flight==nil then + return true + end + + -- Get the lead flight first, so that we can also check all section members. + local lead=self:_GetLeadFlight(flight) + + -- Check all elements of the lead flight group. + for _,_element in pairs(lead.elements) do local element=_element --#AIROBSS.FlightElement if not element.recovered then return false end end - + + -- Now check all section members, if any. + for _,_section in pairs(lead.section) do + local sectionmember=_section --#AIRBOSS.FlightGroup + + -- Check all elements of the secmember flight group. + for _,_element in pairs(sectionmember.elements) do + local element=_element --#AIROBSS.FlightElement + if not element.recovered then + return false + end + end + end + + -- Remove lead flight from pattern queue. It is this flight who is added to the queue. + self:_RemoveFlightFromQueue(self.Qpattern, lead) + + -- Just for now, check if it is in other queues as well. + if self:_InQueue(self.Qmarshal, lead.group) then + self:E("ERROR: flight group should not be in marshal queue") + self:_RemoveFlightFromMarshalQueue(lead, true) + end + -- Just for now, check if it is in other queues as well. + if self:_InQueue(self.Qwaiting, lead.group) then + self:E("ERROR: flight group should not be in pattern queue") + self:_RemoveFlightFromQueue(self.Qwaiting, lead) + end + return true end --- Sets flag recovered=true for a flight element, which was successfully recovered (landed). -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit The aircraft unit that was recovered. --- @return #boolean If true, all flight elements were rerovered. +-- @return #AIRBOSS.FlightGroup Flight group of element. function AIRBOSS:_RecoveredElement(unit) - local element=self:_GetFlightElement(unit:GetName()) --#AIRBOSS.FlightElement + local element, idx, flight=self:_GetFlightElement(unit:GetName()) --#AIRBOSS.FlightElement element.recovered=true + + return flight end --- 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. @@ -5797,6 +5896,65 @@ function AIRBOSS:_RemoveUnitFromFlight(unit) end +--- Update section if a flight is removed. +-- If removed flight is member of a section, he is removed for the leaders section. +-- If removed flight is the section lead, we try to find a new leader. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight The flight to be removed. +function AIRBOSS:_UpdateFlightSection(flight) + + -- Check if this player is the leader of a section. + if flight.seclead==flight.name then + + -------------------- + -- Section Leader -- + -------------------- + + -- This player is the leader ==> We need a new one. + if #flight.section>=1 then + + -- New leader. + local newlead=flight.section[1] --#AIRBOSS.FlightGroup + newlead.seclead=newlead.name + + -- Adjust new section members. + for i=2,#flight.section do + local member=flight.section[i] --#AIRBOSS.FlightGroup + + -- Add remaining members new leaders table. + table.insert(newlead.section, member) + + -- Set new section lead of member. + member.seclead=newlead.name + end + + end + + -- Flight section empty + flight.section={} + + else + + -------------------- + -- Section Member -- + -------------------- + + -- Remove this flight group from the section of the leader. + local lead=self.players[flight.seclead] --#AIRBOSS.FlightGroup + if lead then + for i,sec in pairs(lead.section) do + local sectionmember=sec --#AIRBOSS.FlightGroup + if sectionmember.name==flight.name then + table.remove(lead.section, i) + break + end + end + end + + end + +end + --- 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 @@ -5811,12 +5969,33 @@ function AIRBOSS:_RemoveFlight(flight, completely) -- Check if player or AI if flight.ai then + -- Remove AI flight completely. self:_RemoveFlightFromQueue(self.flights, flight) + else + if completely then - -- Remove HUMAN flight completely. + + -- TODO: update section and checksectinrecovered at the beginning to ensure that not anybody else from the section is in the queue?! + + -- Update flight section. Remove flight from section lead or find new section lead. + self:_UpdateFlightSection(flight) + + -- Check if section has recovered, just in case. + self:_CheckSectionRecovered(flight) + + -- Remove HUMAN flight completely, e.g. when player died or left. self:_RemoveFlightFromQueue(self.flights, flight) + + -- Remove all grades until a final grade is reached. + local grades=self.playerscores[flight.name] + if grades and #grades>0 then + while #grades>0 and grades[#grades].finalscore==nil do + table.remove(grades, #grades) + end + end + else -- Set Playerstep to undefined. flight.step=AIRBOSS.PatternStep.UNDEFINED @@ -6317,13 +6496,10 @@ function AIRBOSS:OnEventLand(EventData) end -- AI always lands ==> remove unit from flight group and queues. - self:_RecoveredElement(EventData.IniUnit) - local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) - local allrecovered=self:_CheckAllRecovered(flight) - if allrecovered then - self:_RemoveFlightFromQueue(self.Qpattern, flight) - end - --self:_RemoveUnitFromFlight(EventData.IniUnit) + local flight=self:_RecoveredElement(EventData.IniUnit) + + -- Check if all were recovered. If so update pattern queue. + self:_CheckSectionRecovered(flight) end end @@ -6346,12 +6522,32 @@ function AIRBOSS:OnEventEngineShutdown(EventData) if _unit and _playername then -- Debug message. - self:T(self.lid..string.format("Player %s shutted down its engines!",_playername)) + self:T(self.lid..string.format("Player %s shut down its engines!",_playername)) else -- Debug message. - self:T2(self.lid..string.format("AI unit %s shutted down its engines!", _unitName)) + self:T2(self.lid..string.format("AI unit %s shut down its engines!", _unitName)) + + if self.despawnshutdown then + + -- Get flight. + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + + -- Only AI flights. + if flight and flight.ai then + + -- Check if all elements were recovered. + local recovered=self:_CheckSectionRecovered(flight) + + -- Despawn group and completely remove flight. + if recovered then + -- TODO: This creates an Event RemoveUnit. Should be captured. + EventData.IniGroup:Destroy() + self:_RemoveFlight(flight) + end + end + end end @@ -6406,18 +6602,11 @@ function AIRBOSS:OnEventCrash(EventData) local flight=self.players[_playername] -- Remove flight completely from all queues and collapse marshal if necessary. + -- This also updates the section, if any and removes unfinished gradings. 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 crashed!", EventData.IniUnitName)) @@ -6443,6 +6632,7 @@ 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] @@ -6451,20 +6641,17 @@ function AIRBOSS:OnEventEjection(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 ejected!", EventData.IniUnitName)) - -- Remove unit from flight and queues. - self:_RemoveUnitFromFlight(EventData.IniUnit) + -- Remove element/unit from flight group and from all queues if no elements alive. + self:_RemoveUnitFromFlight(EventData.IniUnit) + + -- What could happen is, that another element has landed (recovered) already and this one crashes. + -- This would mean that the flight would not be deleted from the queue ==> Check if section recovered. + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + self:_CheckSectionRecovered(flight) end end @@ -6496,31 +6683,17 @@ function AIRBOSS:_PlayerLeft(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 - end end ---[[ --- Airboss event function handling the mission end event. -- Handles the case when the mission is ended. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData Event data. function AIRBOSS:OnEventMissionEnd(EventData) - - -- Auto save player results. - if self.autosave then - self:Save(self.autosavepath, self.autosavefile) - end + self:T3(self.lid.."Mission Ended") end -]] ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- PATTERN functions @@ -10384,11 +10557,8 @@ function AIRBOSS:_Debrief(playerData) -- Set recovered flag. self:_RecoveredElement(playerData.unit) - local allrecovered=self:_CheckAllRecovered(playerData) - - if allrecovered then - self:_RemoveFlightFromQueue(self.Qpattern, playerData) - end + -- Check if all elements + self:_CheckSectionRecovered(playerData) end -- Increase number of passes. @@ -10620,14 +10790,19 @@ end -- @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. +-- @param #number uspeed Speed in knots after U-turn. Default is same as before. -- @return #AIRBOSS self -function AIRBOSS:CarrierDetour(coord, speed, uturn) +function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed) -- Current coordinate of the carrier. local pos0=self:GetCoordinate() + + -- Default. + speed=speed or self.carrier:GetVelocityKNOTS() -- Speed in km/h. - local speedkmh=UTILS.KnotsToKmph(speed or self.carrier:GetVelocityKNOTS()) + local speedkmh=UTILS.KnotsToKmph(speed) + local uspeedkmh=UTILS.KnotsToKmph(uspeed) -- Waypoint table. local wp={} @@ -10638,7 +10813,7 @@ function AIRBOSS:CarrierDetour(coord, speed, uturn) -- If enabled, go back to where you came from. if uturn then - table.insert(wp, pos0:WaypointGround(speedkmh)) + table.insert(wp, pos0:WaypointGround(uspeedkmh)) end -- Get carrier group. @@ -10688,7 +10863,7 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck) local distNM=UTILS.MetersToNM(dist) -- Debug output - self:E(self.lid..string.format("Carrier steaming into the wind (%.1f kts). Distance=%.1f NM, Speed=%.1f knots, Time=%d sec.", UTILS.MpsToKnots(vwind), distNM, speedknots, time)) + self:I(self.lid..string.format("Carrier steaming into the wind (%.1f kts). Distance=%.1f NM, Speed=%.1f knots, Time=%d sec.", UTILS.MpsToKnots(vwind), distNM, speedknots, time)) -- Get heading into the wind accounting for angled runway. local intowind=self:GetHeadingIntoWind(false) @@ -10703,10 +10878,18 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck) -- Return to coordinate if collision is detected. self.Creturnto=self:GetCoordinate() + + -- 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 + + -- For downwind, we take the velocity at the next WP. + local vdownwind=UTILS.MpsToKnots(nextwp:GetVelocity()) -- Let the carrier make a detour from its route but return to its current position. - -- TODO: Add downwind speed - self:CarrierDetour(pos1, speedknots, true) + self:CarrierDetour(pos1, speedknots, true, vdownwind) -- Set switch that we are currently turning into the wind. self.turnintowind=true From 71126eb34d37abd238db6531ef18625faf76bde1 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 6 Feb 2019 23:50:41 +0100 Subject: [PATCH 157/485] AIRBOSS v0.96 --- Moose Development/Moose/Ops/Airboss.lua | 150 +++++++++++++++------- Moose Development/Moose/Wrapper/Group.lua | 40 +++--- 2 files changed, 127 insertions(+), 63 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 71be2db27..486ade2f9 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -16,7 +16,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 (WIP). +-- * Combine multiple human players to sections (WIP). -- * Many parameters customizable by convenient user API functions. -- * Multiple carrier support due to object oriented approach. -- * Unlimited number of players. @@ -1655,7 +1655,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.5wip" +AIRBOSS.version="0.9.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -3522,10 +3522,15 @@ end -- @param #string Event Event. -- @param #string To To state. function AIRBOSS:onafterStop(From, Event, To) + -- Unhandle events. self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.EngineShutdown) + self:UnHandleEvent(EVENTS.Takeoff) self:UnHandleEvent(EVENTS.Crash) self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.PlayerLeaveUnit) + self:UnHandleEvent(EVENTS.MissionEnd) end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5118,7 +5123,8 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) self:T(self.lid..string.format("Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal)) -- Add flight to pattern queue. - table.insert(self.Qpattern, flight) + --table.insert(self.Qpattern, flight) + self:_AddFlightToPatternQueue(flight) end @@ -5323,8 +5329,8 @@ function AIRBOSS:_PrintQueue(queue, name) --local nqueue=#queue local Nqueue, nqueue=self:_GetQueueInfo(queue) - local text=string.format("%s Queue N=%d, n=%d:", name, Nqueue, nqueue) - if nqueue==0 then + local text=string.format("%s Queue N=%d (#%d), n=%d:", name, Nqueue, #queue, nqueue) + if #queue==0 then text=text.." empty." else for i,_flight in pairs(queue) do @@ -5554,7 +5560,8 @@ function AIRBOSS:_InitPlayer(playerData, step) self:MessageToPlayer(playerData, "Group name contains \"Groove\". Happy groove testing.") playerData.attitudemonitor=true playerData.step=AIRBOSS.PatternStep.FINAL - table.insert(self.Qpattern, playerData) + --table.insert(self.Qpattern, playerData) + self:_AddFlightToPatternQueue(flight) end return playerData @@ -5765,6 +5772,28 @@ function AIRBOSS:_CheckSectionRecovered(flight) return true end +--- Add flight to pattern queue and set recoverd to false for all elements of the flight and its section members. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup Flight group of element. +function AIRBOSS:_AddFlightToPatternQueue(flight) + + -- Add flight to table. + table.insert(self.Qpattern, flight) + + -- Init recovered switch. + flight.recovered=false + for _,elem in pairs(flight.elements) do + elem.recoverd=false + end + + -- Set recovered for all section members. + for _,sec in pairs(flight.section) do + for _,elem in pairs(sec.elements) do + elem.recoverd=false + end + end +end + --- Sets flag recovered=true for a flight element, which was successfully recovered (landed). -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit The aircraft unit that was recovered. @@ -5780,11 +5809,12 @@ end -- @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. +-- @return #boolean True, flight was removed or false otherwise. +-- @return #number Table index of the flight in the Marshal queue. function AIRBOSS:_RemoveFlightFromMarshalQueue(flight, nopattern) -- Remove flight from marshal queue if it is in. - local removed=self:_RemoveFlightFromQueue(self.Qmarshal, flight) + local removed, idx=self:_RemoveFlightFromQueue(self.Qmarshal, flight) -- Collapse marshal stack if flight was removed. if removed then @@ -5820,18 +5850,18 @@ function AIRBOSS:_RemoveFlightFromMarshalQueue(flight, nopattern) -- Remove flight from waiting queue. self:_RemoveFlightFromQueue(self.Qwaiting, nextflight) - end - + end end - return removed + return removed, idx 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. +-- @return #boolean True, flight was in Queue and removed. False otherwise. +-- @return #number Table index of removed queue element or nil. function AIRBOSS:_RemoveFlightFromQueue(queue, flight) -- Loop over all flights in group. @@ -5842,14 +5872,14 @@ 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 true + return true, i end end - return false + return false, nil end ---- Remove a unit from a flight group (e.g. when landed) and update all queues if the whole flight group is gone. +--- Remove a unit and its element 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) @@ -5896,6 +5926,29 @@ function AIRBOSS:_RemoveUnitFromFlight(unit) end +--- Remove a flight, which is a member of a section, from this section. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight The flight to be removed from the section +function AIRBOSS:_RemoveFlightFromSection(flight) + + -- First check if player is not the lead. + if flight.name~=flight.seclead then + + -- Remove this flight group from the section of the leader. + local lead=self.players[flight.seclead] --#AIRBOSS.FlightGroup + if lead then + for i,sec in pairs(lead.section) do + local sectionmember=sec --#AIRBOSS.FlightGroup + if sectionmember.name==flight.name then + table.remove(lead.section, i) + break + end + end + end + end + +end + --- Update section if a flight is removed. -- If removed flight is member of a section, he is removed for the leaders section. -- If removed flight is the section lead, we try to find a new leader. @@ -5939,17 +5992,8 @@ function AIRBOSS:_UpdateFlightSection(flight) -- Section Member -- -------------------- - -- Remove this flight group from the section of the leader. - local lead=self.players[flight.seclead] --#AIRBOSS.FlightGroup - if lead then - for i,sec in pairs(lead.section) do - local sectionmember=sec --#AIRBOSS.FlightGroup - if sectionmember.name==flight.name then - table.remove(lead.section, i) - break - end - end - end + -- Remove flight from its leaders section. + self:_RemoveFlightFromSection(flight) end @@ -5961,31 +6005,28 @@ end -- @param #AIRBOSS.PlayerData flight The flight to be removed. -- @param #boolean completely If true, also remove human flight from all flights table. function AIRBOSS:_RemoveFlight(flight, completely) - + self:F(self.lid.. string.format("Removing flight %s, ai=%s completely=%s.", tostring(flight.groupname), tostring(flight.ai), tostring(completely))) + -- Remove flight from all queues. self:_RemoveFlightFromMarshalQueue(flight, true) self:_RemoveFlightFromQueue(self.Qpattern, flight) - self:_RemoveFlightFromQueue(self.Qwaiting, flight) + self:_RemoveFlightFromQueue(self.Qwaiting, flight) -- Check if player or AI if flight.ai then - -- Remove AI flight completely. + -- Remove AI flight completely. Pure AI flights have no sections and cannot be members. self:_RemoveFlightFromQueue(self.flights, flight) else - + + -- Check if flight should be completely removed, e.g. after the player died or simply left the slot. if completely then - - -- TODO: update section and checksectinrecovered at the beginning to ensure that not anybody else from the section is in the queue?! - - -- Update flight section. Remove flight from section lead or find new section lead. + + -- Update flight section. Remove flight from section or find new section leader if flight was the lead. self:_UpdateFlightSection(flight) - - -- Check if section has recovered, just in case. - self:_CheckSectionRecovered(flight) - -- Remove HUMAN flight completely, e.g. when player died or left. + -- Remove completely. self:_RemoveFlightFromQueue(self.flights, flight) -- Remove all grades until a final grade is reached. @@ -5997,8 +6038,19 @@ function AIRBOSS:_RemoveFlight(flight, completely) end else - -- Set Playerstep to undefined. - flight.step=AIRBOSS.PatternStep.UNDEFINED + + -- Set player step to undefined. + self:_SetPlayerStep(flight, AIRBOSS.PatternStep.UNDEFINED) + + -- Also set this for the section members as they are in the same boat. + for _,sectionmember in pairs(flight.section) do + self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.UNDEFINED) + end + + -- TODO: What if flight is member of a section. His status is now undefined. Should he be removed from the section? + -- I think yes, if he pulls the trigger. + self:_RemoveFlightFromSection(flight) + end end @@ -6527,7 +6579,7 @@ function AIRBOSS:OnEventEngineShutdown(EventData) else -- Debug message. - self:T2(self.lid..string.format("AI unit %s shut down its engines!", _unitName)) + self:T(self.lid..string.format("AI unit %s shut down its engines!", _unitName)) if self.despawnshutdown then @@ -6542,8 +6594,8 @@ function AIRBOSS:OnEventEngineShutdown(EventData) -- Despawn group and completely remove flight. if recovered then - -- TODO: This creates an Event RemoveUnit. Should be captured. - EventData.IniGroup:Destroy() + self:T(self.lid..string.format("AI group %s completely recovered. Despawning group after engine shutdown event as requested in 5 seconds.", tostring(EventData.IniGroupName))) + EventData.IniGroup:Destroy(nil, 5) self:_RemoveFlight(flight) end end @@ -6572,11 +6624,15 @@ function AIRBOSS:OnEventTakeoff(EventData) -- Debug message. self:T(self.lid..string.format("Player %s took off!",_playername)) + -- TODO: Set recoverd status. + else -- Debug message. self:T2(self.lid..string.format("AI unit %s took off!", _unitName)) + -- TODO: Set recoverd status. + end end @@ -6602,7 +6658,7 @@ function AIRBOSS:OnEventCrash(EventData) local flight=self.players[_playername] -- Remove flight completely from all queues and collapse marshal if necessary. - -- This also updates the section, if any and removes unfinished gradings. + -- This also updates the section, if any and removes any unfinished gradings of the player. if flight then self:_RemoveFlight(flight, true) end @@ -6643,7 +6699,7 @@ function AIRBOSS:OnEventEjection(EventData) else -- Debug message. - self:T2(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) + self:T(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) -- Remove element/unit from flight group and from all queues if no elements alive. self:_RemoveUnitFromFlight(EventData.IniUnit) @@ -12413,6 +12469,7 @@ function AIRBOSS:_ResetPlayerStatus(_unitName) self:MessageToPlayer(playerData, text, "AIRBOSS") -- Remove flight from queues. Collapse marshal stack if necessary. + -- TODO: This has not the completely tag. What to do with section members if flight is lead or not? self:_RemoveFlight(playerData) -- Initialize player data. @@ -12606,7 +12663,8 @@ function AIRBOSS:_RequestCommence(_unitName) 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) + --table.insert(self.Qpattern, playerData) + self:_AddFlightToPatternQueue(playerData) end -- Clear player for commence. diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 8525f0e83..7388259af 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -326,6 +326,7 @@ end -- To raise these events, provide the `GenerateEvent` parameter. -- @param #GROUP self -- @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. +-- @param #number delay Delay in seconds before despawning the group. -- @usage -- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. -- Helicopter = GROUP:FindByName( "Helicopter" ) @@ -344,30 +345,35 @@ end -- Ship = GROUP:FindByName( "Boat" ) -- Ship:Destroy( false ) -- Don't generate an event upon destruction. -- -function GROUP:Destroy( GenerateEvent ) +function GROUP:Destroy( GenerateEvent, delay ) self:F2( self.GroupName ) + + if delay and delay>0 then + SCHEDULER:New(nil, GROUP.Destroy, {self, GenerateEvent}, delay) + else - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - if GenerateEvent and GenerateEvent == true then - if self:IsAir() then - self:CreateEventCrash( timer.getTime(), UnitData ) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + if GenerateEvent and GenerateEvent == true then + if self:IsAir() then + self:CreateEventCrash( timer.getTime(), UnitData ) + else + self:CreateEventDead( timer.getTime(), UnitData ) + end + elseif GenerateEvent == false then + -- Do nothing! else - self:CreateEventDead( timer.getTime(), UnitData ) + self:CreateEventRemoveUnit( timer.getTime(), UnitData ) end - elseif GenerateEvent == false then - -- Do nothing! - else - self:CreateEventRemoveUnit( timer.getTime(), UnitData ) end + USERFLAG:New( self:GetName() ):Set( 100 ) + DCSGroup:destroy() + DCSGroup = nil end - USERFLAG:New( self:GetName() ):Set( 100 ) - DCSGroup:destroy() - DCSGroup = nil end - + return nil end From e64703de03290672bd4223988c7e7a2fd8284677 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 7 Feb 2019 16:16:17 +0100 Subject: [PATCH 158/485] AB v0.96w --- Moose Development/Moose/Ops/Airboss.lua | 126 ++++++++++++++++---- Moose Development/Moose/Utilities/Utils.lua | 8 ++ 2 files changed, 110 insertions(+), 24 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 486ade2f9..638c99a76 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -38,10 +38,9 @@ -- * 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 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. +-- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) and A-4E community mod as aircraft and the USS John C. Stennis as carrier. -- --- The AV-8B Harrier and the USS Tarawa are very much WIP. Those two can only be used together, i.e. the Tarawa is the only carrier the harrier is supposed to land on and +-- The AV-8B Harrier and the USS Tarawa are WIP. Those two can only be used together, i.e. the Tarawa is the only carrier the harrier is supposed to land on and -- the no other fixed wing aircraft (human or AI controlled) are supposed to land on the Tarawa. Currently only Case I is supported. Case II/III take slightly steps from the CVN carrier. -- However, the two Case II/III pattern are very similar so this is not a big drawback. -- @@ -62,7 +61,8 @@ -- -- * Each player slot (client) should be in a separate group as DCS does only allow for sending messages to groups and not 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. --- * The modex (tail number) of an aircraft should be changed dynamically in the mission by a player. Unfortunately, there is no way to get this information via scripting API functions. +-- * The modex (tail number) of an aircraft should be changed dynamically in the mission by a player. Unfortunately, there is no way to get this information via scripting API functions. +-- * The A-4E-C mod needs *easy comms* activated to interact with the F10 radio menu. -- -- ## Youtube Videos -- @@ -107,6 +107,8 @@ -- @field #string ClassName Name of the class. -- @field #boolean Debug Debug mode. Messages to all about status. -- @field #string lid Class id string for output to DCS log file. +-- @field #string theatre The DCS map used in the mission. +-- @field #string missionname The Name of the mission. -- @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 specific parameters. @@ -421,11 +423,11 @@ -- 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. +-- If your 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 +-- With this command, you can define a section of human flights. The player who issues the command becomes the section lead and all other human players -- within a radius of 100 meters become members of the section. -- -- The responsibilities of the section leader are: @@ -446,7 +448,7 @@ -- -- ![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 Charlie 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. -- @@ -547,7 +549,7 @@ -- * 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. +-- * 0.0 Points **CUT**: "Cut pass", when player was waved off but landed anyway. -- -- ## Foul Deck Waveoff -- @@ -838,6 +840,8 @@ AIRBOSS = { ClassName = "AIRBOSS", Debug = false, lid = nil, + theatre = nil, + missionname = nil, carrier = nil, carriertype = nil, carrierparam = {}, @@ -1564,7 +1568,7 @@ AIRBOSS.Difficulty={ -- @field #number TGroove Time stamp when pilot entered the groove. -- @field #string FlyThrough Fly through up "/" or fly through down "\\". ---- LSO grade +--- LSO grade data. -- @type AIRBOSS.LSOgrade -- @field #string grade LSO grade, i.e. _OK_, OK, (OK), --, CUT -- @field #number points Points received. @@ -1573,6 +1577,15 @@ AIRBOSS.Difficulty={ -- @field #number wire Wire caught. -- @field #number Tgroove Time in the groove in seconds. -- @field #number case Recovery case. +-- @field #string time Mission time. +-- @field #string wind Wind speed on deck in knots. +-- @field #string airframe Aircraft type name of player. +-- @field #string modex Onboard number. +-- @field #string carriertype Carrier type name. +-- @field #string carriername Carrier name/alias. +-- @field #string theatre DCS map. +-- @field #string missionname Name of the mission. +-- @field #string date Real live date. Needs **os** to be desanitized. --- Checkpoint parameters triggering the next step in the pattern. -- @type AIRBOSS.Checkpoint @@ -1655,7 +1668,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.6" +AIRBOSS.version="0.9.6w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -2869,15 +2882,16 @@ 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)) - -- Current map. - local theatre=env.mission.theatre - self:T2(self.lid..string.format("Theatre = %s", tostring(theatre))) - -- Activate TACAN. if self.TACANon then self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) end + -- Current map. + self.theatre=env.mission.theatre + self.missionname=env.mission.name --TODO check! + self:T2(self.lid..string.format("Theatre = %s, mission = %s", tostring(self.theatre), tostring(self.missionname))) + -- Activate ICLS. if self.ICLSon then self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) @@ -4657,7 +4671,6 @@ function AIRBOSS:_MarshalAI(flight, nstack) if case==1 then -- Initial point 7 NM and a bit port of carrier. - -- TODO: Test and tune! local pE=Carrier:Translate(UTILS.NMToMeters(7), hdg-30):SetAltitude(altitude) -- Entry point 5 NM port and slightly astern the boat. @@ -5049,9 +5062,9 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Only collapse stacks above the new pattern flight. if mstack>stack then - -- 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. + -- TODO: Is this now right as we allow more flights per stack? + -- 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) -- Free stack has to be below. @@ -6047,7 +6060,7 @@ function AIRBOSS:_RemoveFlight(flight, completely) self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.UNDEFINED) end - -- TODO: What if flight is member of a section. His status is now undefined. Should he be removed from the section? + -- What if flight is member of a section. His status is now undefined. Should he be removed from the section? -- I think yes, if he pulls the trigger. self:_RemoveFlightFromSection(flight) @@ -9291,6 +9304,46 @@ function AIRBOSS:GetBRC() return self:GetHeading(true) end +--- Get wind direction and speed at carrier position. +-- @param #AIRBOSS self +-- @param #number alt Altitude in meters. Default 50 m. +-- @return #number Direction the wind is blowing **from** in degrees. +-- @return #number Wind speed in m/s. +function AIRBOSS:GetWind(alt) + + local cv=self:GetCoordinate() + + local Wdir, Wspeed=cv:GetWind(alt or 50) + + return Wdir, Wspeed +end + +--- Get wind direction and speed on carrier deck. +-- @param #AIRBOSS self +-- @param #number alt Altitude in meters. Default 50 m. +-- @return #number Direction the wind is blowing **from** in degrees. +-- @return #number Wind speed in m/s. +function AIRBOSS:GetWindOnDeck(alt) + + -- Position of carrier + local cv=self:GetCoordinate() + + -- Velocity vector of carrier. + local vc=self.carrier:GetVelocityVec3() + + -- Wind (from) vector + local vw=cv:GetWindWithTurbulenceVec3(alt or 50) + + -- Carrier velocity has to be negative. If carrier drives in the direction the wind is blowing from, we have less wind in total. + local vd=UTILS.VecSubstract(vw, vc) + + -- Strength. + local vabs=UTILS.VecNorm(vd) + + return vd, vabs +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. @@ -10428,6 +10481,18 @@ function AIRBOSS:_Debrief(playerData) mygrade.finalscore=Points end mygrade.case=playerData.case + mygrade.time=UTILS.SecondsToClock(timer.getAbsTime()) + mygrade.wind=tostring(UTILS.Round(self:_GetWindOnDeck(), 1)) + mygrade.airframe=playerData.actype + mygrade.modex=playerData.onboard + mygrade.carriertype=self.carriertype + mygrade.carriername=self.alias + mygrade.theatre=self.theatre + mygrade.missionname=self.missionname + mygrade.date="n/a" + if os then + mygrade.date=os.date() --os.date("%d.%m.%Y") + end -- Add LSO grade to player grades table. table.insert(self.playerscores[playerData.name], mygrade) @@ -10482,8 +10547,7 @@ 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... - -- Can become nil when I crashed and changed to observer. Which events are captured? Nil check for unit? - + -- Can become nil when I crashed and changed to observer. Which events are captured? Nil check for unit? if playerData.unit:IsAlive() then -- Heading and distance tip. @@ -13880,7 +13944,7 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) self:I(self.lid..text) -- Header line - local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case\n" + local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Mission Time,Wind,Airframe,Modex,Carrier Type,Carrier Name,Map, Mission Name,Date\n" -- Loop over all players. for playername,grades in pairs(self.playerscores) do @@ -13906,7 +13970,10 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) 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) + --scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d\n", + scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s\n", + playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case, + grade.time, grade.wind, grade.airframe, grade.modex, grade.carriertype, grade.carrieralias, grade.theatre, grade.missionname, grade.date) end end @@ -14031,7 +14098,9 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) -- Grade table local grade={} --#AIRBOSS.LSOgrade - -- Line format: playername, i, grade.finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, case + --- Line format: + -- playername, i, grade.finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, case, + -- time, wind, airframe, modex, carriertype, carriername, theatre, missionname, date local playername=gradedata[1] if gradedata[3]~=nil and gradedata[3]~="n/a" then grade.finalscore=tonumber(gradedata[3]) @@ -14046,6 +14115,15 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) grade.Tgroove=tonumber(gradedata[8]) end grade.case=tonumber(gradedata[9]) + grade.time=gradedata[10] or "n/a" + grade.wind=gradedata[11] or "n/a" + grade.airframe=gradedata[12] or "n/a" + grade.modex=gradedata[13] or "n/a" + grade.carriertype=gradedata[14] or "n/a" + grade.carriername=gradedata[15] or "n/a" + grade.theatre=gradedata[16] or "n/a" + grade.missionname=gradedata[17] or "n/a" + grade.date=gradedata[18] or "n/a" -- Init player table if necessary. self.playerscores[playername]=self.playerscores[playername] or {} diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 301433c61..436c59b4b 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -782,6 +782,14 @@ function UTILS.VecSubstract(a, b) return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} end +--- Calculate the total vector of two 3D vectors by adding the x,y,z components of 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.VecAdd(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. From 5ed768065ce49257edbf3473132ff699bc50c603 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 8 Feb 2019 01:04:07 +0100 Subject: [PATCH 159/485] Update Airboss.lua --- Moose Development/Moose/Ops/Airboss.lua | 27 ++++++++++--------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 638c99a76..6734f03e4 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -108,7 +108,6 @@ -- @field #boolean Debug Debug mode. Messages to all about status. -- @field #string lid Class id string for output to DCS log file. -- @field #string theatre The DCS map used in the mission. --- @field #string missionname The Name of the mission. -- @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 specific parameters. @@ -841,7 +840,6 @@ AIRBOSS = { Debug = false, lid = nil, theatre = nil, - missionname = nil, carrier = nil, carriertype = nil, carrierparam = {}, @@ -1584,7 +1582,6 @@ AIRBOSS.Difficulty={ -- @field #string carriertype Carrier type name. -- @field #string carriername Carrier name/alias. -- @field #string theatre DCS map. --- @field #string missionname Name of the mission. -- @field #string date Real live date. Needs **os** to be desanitized. --- Checkpoint parameters triggering the next step in the pattern. @@ -1606,7 +1603,7 @@ AIRBOSS.Difficulty={ -- @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.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 #number flag Flag value describing the current stack. -- @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. @@ -2888,9 +2885,8 @@ function AIRBOSS:onafterStart(From, Event, To) end -- Current map. - self.theatre=env.mission.theatre - self.missionname=env.mission.name --TODO check! - self:T2(self.lid..string.format("Theatre = %s, mission = %s", tostring(self.theatre), tostring(self.missionname))) + self.theatre=env.mission.theatre + self:T2(self.lid..string.format("Theatre = %s.", tostring(self.theatre))) -- Activate ICLS. if self.ICLSon then @@ -5574,7 +5570,7 @@ function AIRBOSS:_InitPlayer(playerData, step) playerData.attitudemonitor=true playerData.step=AIRBOSS.PatternStep.FINAL --table.insert(self.Qpattern, playerData) - self:_AddFlightToPatternQueue(flight) + self:_AddFlightToPatternQueue(playerData) end return playerData @@ -10482,13 +10478,13 @@ function AIRBOSS:_Debrief(playerData) end mygrade.case=playerData.case mygrade.time=UTILS.SecondsToClock(timer.getAbsTime()) - mygrade.wind=tostring(UTILS.Round(self:_GetWindOnDeck(), 1)) + local _,windondeck=self:GetWindOnDeck() + mygrade.wind=tostring(UTILS.Round(windondeck, 1)) mygrade.airframe=playerData.actype mygrade.modex=playerData.onboard mygrade.carriertype=self.carriertype mygrade.carriername=self.alias mygrade.theatre=self.theatre - mygrade.missionname=self.missionname mygrade.date="n/a" if os then mygrade.date=os.date() --os.date("%d.%m.%Y") @@ -13944,7 +13940,7 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) self:I(self.lid..text) -- Header line - local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Mission Time,Wind,Airframe,Modex,Carrier Type,Carrier Name,Map, Mission Name,Date\n" + local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Mission Time,Wind,Airframe,Modex,Carrier Type,Carrier Name,Theatre,Date\n" -- Loop over all players. for playername,grades in pairs(self.playerscores) do @@ -13971,9 +13967,9 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) -- Compile grade line. --scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d\n", - scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s\n", + scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s\n", playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case, - grade.time, grade.wind, grade.airframe, grade.modex, grade.carriertype, grade.carrieralias, grade.theatre, grade.missionname, grade.date) + grade.time, grade.wind, grade.airframe, grade.modex, grade.carriertype, grade.carriername, grade.theatre, grade.date) end end @@ -14100,7 +14096,7 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) --- Line format: -- playername, i, grade.finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, case, - -- time, wind, airframe, modex, carriertype, carriername, theatre, missionname, date + -- time, wind, airframe, modex, carriertype, carriername, theatre, date local playername=gradedata[1] if gradedata[3]~=nil and gradedata[3]~="n/a" then grade.finalscore=tonumber(gradedata[3]) @@ -14122,8 +14118,7 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) grade.carriertype=gradedata[14] or "n/a" grade.carriername=gradedata[15] or "n/a" grade.theatre=gradedata[16] or "n/a" - grade.missionname=gradedata[17] or "n/a" - grade.date=gradedata[18] or "n/a" + grade.date=gradedata[17] or "n/a" -- Init player table if necessary. self.playerscores[playername]=self.playerscores[playername] or {} From d01b55c790277ffbe37abf4c4e9cfe9945e8a623 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 8 Feb 2019 16:41:23 +0100 Subject: [PATCH 160/485] AB v0.9.7w --- Moose Development/Moose/Ops/Airboss.lua | 265 ++++++++++++++++++++---- 1 file changed, 225 insertions(+), 40 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 6734f03e4..56de15611 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -155,6 +155,7 @@ -- @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 Qspinning Queue of aircraft currently spinning. -- @field #table RQMarshal Radio queue of marshal. -- @field #table RQLSO Radio queue of LSO. -- @field #number Nmaxpattern Max number of aircraft in landing pattern. @@ -298,7 +299,8 @@ -- * **F3 Request Marshal** -- * **F4 Request Commence** -- * **F5 Request Refueling** --- * **F6 [Reset My Status]** +-- * **F6 Spinning** +-- * **F7 [Reset My Status]** -- -- ### Request Marshal -- @@ -327,6 +329,17 @@ -- -- 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 itself at its home base. Once the re-fueling is complete, the player has to re-register to the marshal stack. +-- +-- ### Spinning +-- +-- If the pattern is full, players can go into the spinning pattern. This step is only allowed, if the player is in the pattern and his next step +-- is initial, break entry, early/late break. +-- +-- If a player is in the spin pattern, flights in the Marshal queue should hold their altitude and are not allowed into the pattern until the spinning aircraft +-- proceeds. +-- +-- +-- If a player -- -- ### [Reset My Status] -- @@ -712,6 +725,15 @@ -- * *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. +-- * *Wind*: Wind on deck in knots during approach. +-- * *Modex*: Tail number of the player. +-- * *Airframe*: Aircraft type used in the recovery. +-- * *Carrier Type*: Type name of the carrier. +-- * *Carrier Name*: Name/alias of the carrier. +-- * *Theatre*: DCS map. +-- * *Mission Time*: Mission time at the end of the approach. +-- * *Mission Date*: Mission date in yyyy/mm/dd format. +-- * *OS Date*: Real life date from os.date(). Needs **os** to be desanitized. -- -- ## Load Results -- @@ -754,6 +776,45 @@ -- For example as -- -- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles/") +-- +-- ## Remarks +-- +-- DCS offers two (actually three) ways to send radio messages. Each one has its advantages and disadvantages and it is important to understand the differences. +-- +-- ### Transmission via Command +-- +-- *In principle*, the best way to transmit messages is via the [TransmitMessage](https://wiki.hoggitworld.com/view/DCS_command_transmitMessage) command. +-- This method has the advantage that subtitles can be used and these subtitles are only displayed to the players who dialed in the same radio frequency as +-- used for the transmission. +-- However, this method unfortunately only works if the sending unit is an **aircraft**. There it is not usable by the AIRBOSS per se as the transmission comes from +-- a naval unit (the carrier). +-- +-- As a workaround, you can put an aircraft, e.g. a Helicopter on the deck of the carrier or another ship of the strike group. The aircraft should be set to +-- uncontrolled and maybe even to immortal. With the @{#AIRBOSS.SetRadioUnit}(*unitname*) function you can use this unit as "radio repeater". +-- Of course you can also use any other aircraft in the vicinity of the carrier, e.g. a rescue helo or a recovery tanker. It is just important that this +-- unit is and stays close the the boat as the distance from the sender to the receiver is modeled in DCS. So messages from too far away might not reach the players. +-- +-- **Note** that not all radio messages the airboss sends have voice overs. Therefore, if you use a radio relay unit, users should *not* disable the +-- subtitles in the DCS game menu. +-- +-- ### Transmission via Trigger +-- +-- Another way to broadcast messages is via the [radio transmission trigger](https://wiki.hoggitworld.com/view/DCS_func_radioTransmission). This method can be used for all +-- units (land, air, naval). However, messages cannot be subtitled. Therefore, subtitles are displayed to the players via normal textout messages. +-- The disadvantage is that is is impossible to know which players have the right radio frequencies dialed in. There subtitles of the Marshal radio are displayed to all players +-- inside the CCA. Subtitles on the LSO radio frequency are displayed to all players in the pattern. +-- +-- ### Sound to User +-- +-- The third way to play sounds to the user via the [outsound trigger](https://wiki.hoggitworld.com/view/DCS_func_outSound). +-- These sounds are not coming from a radio station and therefore can be heard by players independent of their actual radio frequency setting. +-- The AIRBOSS class uses this method to play sounds to players which are of a more "private" nature - for example when a player has left his assigned altitude +-- in the Marshal stack. Often this is the modex of the player in combination with a textout messaged displayed on screen. +-- +-- If you want to use this method for all radio messages you can enable it via the @{#AIRBOSS.SetUserSoundRadio}() function. This is the analogue of activating easy comms in DCS. +-- +-- Note that this method is used for all players who are in the A-4E community mod as this mod does not have the ability to use radios due to current DCS restrictions. +-- Therefore, A-4E drivers will hear all radio transmissions from the Marshal/Airboss and all LSO messages as soon as their commence the pattern. -- -- === -- @@ -796,11 +857,11 @@ -- 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. +-- -- Case I from 9 to 10 am. Carrier will turn into the wind 5 min before window opens and go at a speed so that wind over the deck is 25 knots. +-- local window1=AirbossStennis:AddRecoveryWindow("9:00", "10:00", 1, nil, true, 25) +-- -- Case II with +15 degrees holding offset from 15:00 for 60 min. +-- local window2=AirbossStennis:AddRecoveryWindow("15:00", "16:00", 2, 15) +-- -- Case III with +30 degrees holding offset from 21:00 to 23:30. -- 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). @@ -887,6 +948,7 @@ AIRBOSS = { Qpattern = {}, Qmarshal = {}, Qwaiting = {}, + Qspinning = {}, RQMarshal = {}, RQLSO = {}, Nmaxpattern = nil, @@ -1665,17 +1727,17 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.6w" +AIRBOSS.version="0.9.7w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Handle cases where AI crashes on carrier deck ==> Clean up deck. --- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! -- TODO: Player eject and crash debrief "gradings". -- TODO: PWO during case 2/3. -- TODO: PWO when player comes too close to other flight. +-- DONE: Spin pattern. Add radio menu entry. Not sure what to add though?! -- DONE: Despawn AI after engine shutdown option. -- DONE: What happens when section lead or member dies? -- DONE: Do not remove recovered elements but only set switch. Remove only groups which are completely recovered. @@ -4057,7 +4119,7 @@ function AIRBOSS:_GetNextMarshalFight() local flight=_flight --#AIRBOSS.FlightGroup -- Current stack. - local stack=flight.flag:Get() + local stack=flight.flag -- Total marshal time in seconds. local Tmarshal=timer.getAbsTime()-flight.time @@ -4088,6 +4150,7 @@ function AIRBOSS:_CheckQueue() self:_PrintQueue(self.Qmarshal, "Marshal") self:_PrintQueue(self.Qpattern, "Pattern") self:_PrintQueue(self.Qwaiting, "Waiting") + self:_PrintQueue(self.Qspinning, "Spinning") -- 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 @@ -4167,7 +4230,6 @@ function AIRBOSS:_CheckQueue() end end - -- Not recovering ==> skip the rest! return end @@ -4175,11 +4237,14 @@ function AIRBOSS:_CheckQueue() -- Get number of airborne aircraft units(!) currently in pattern. local _,npattern=self:_GetQueueInfo(self.Qpattern) + -- Get number of aircraft units spinning. + local _,nspinning=self:_GetQueueInfo(self.Qspinning) + -- Get next marshal flight. local marshalflight=self:_GetNextMarshalFight() - -- Check if there are flights waiting in the Marshal stack and if the pattern is free. - if marshalflight and npatternUTILS.NMToMeters(5) and knownflight.flag:Get()==-100 and iscarriersquad then + if closein>UTILS.NMToMeters(5) and knownflight.flag==-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 @@ -4518,7 +4583,7 @@ function AIRBOSS:_MarshalPlayer(playerData, stack) flight.case=playerData.case -- Set stack flag. - flight.flag:Set(stack) + flight.flag=stack end else @@ -4534,7 +4599,7 @@ end function AIRBOSS:_WaitAI(flight) -- Set flag to something other than -100 and <0 - flight.flag:Set(-99) + flight.flag=-99 -- Add AI flight to waiting queue. table.insert(self.Qwaiting, flight) @@ -4612,14 +4677,14 @@ function AIRBOSS:_MarshalAI(flight, nstack) local case=flight.case -- Get old/current stack. - local ostack=flight.flag:Get() + local ostack=flight.flag -- Flight group name. local group=flight.group local groupname=flight.groupname -- Set new stack. - flight.flag:Set(nstack) + flight.flag=nstack -- Current carrier position. local Carrier=self:GetCoordinate() @@ -4875,7 +4940,7 @@ end function AIRBOSS:_GetCharlieTime(flightgroup) -- Get current stack of player. - local stack=flightgroup.flag:Get() + local stack=flightgroup.flag -- Flight is not in marshal stack. if stack<=0 then @@ -4901,7 +4966,7 @@ function AIRBOSS:_GetCharlieTime(flightgroup) local flight=_flight --#AIRBOSS.FlightGroup -- Stack of marshal flight. - local mstack=flight.flag:Get() + local mstack=flight.flag -- Time to get to the marshal stack if not holding already. local Tarrive=0 @@ -4971,7 +5036,7 @@ end function AIRBOSS:_AddMarshalGroup(flight, stack) -- Set flag value. This corresponds to the stack number which starts at 1. - flight.flag:Set(stack) + flight.flag=stack -- Set recovery case. flight.case=self.case @@ -5034,7 +5099,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) local case=flight.case -- Stack of flight. - local stack=flight.flag:Get() + local stack=flight.flag -- Check that stack > 0. if stack<=0 then @@ -5053,7 +5118,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) if (case==1 and mflight.case==1) or (case>1 and mflight.case>1) then -- Get current flag/stack value. - local mstack=mflight.flag:Get() + local mstack=mflight.flag -- Only collapse stacks above the new pattern flight. if mstack>stack then @@ -5077,7 +5142,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) else -- Decrease stack/flag. Human player needs to take care himself. - mflight.flag:Set(newstack) + mflight.flag=newstack -- Angels of new stack. local angels=self:_GetAngels(self:_GetMarshalAltitude(newstack, case)) @@ -5099,7 +5164,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) local sec=_sec --#AIRBOSS.PlayerData -- Also decrease flag for section members of flight. - sec.flag:Set(newstack) + sec.flag=newstack -- Set new time stamp. sec.time=timer.getAbsTime() @@ -5138,7 +5203,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) end -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42). - flight.flag:Set(-1) + flight.flag=-1 -- New time stamp for time in pattern. flight.time=timer.getAbsTime() @@ -5176,7 +5241,7 @@ function AIRBOSS:_GetFreeStack(ai, case, empty) if flight.case==case then -- Get stack of flight. - local n=flight.flag:Get() + local n=flight.flag if n>0 then if flight.ai then @@ -5347,7 +5412,7 @@ function AIRBOSS:_PrintQueue(queue, name) local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) local case=flight.case - local stack=flight.flag:Get() + local stack=flight.flag local fuel=flight.group:GetFuelMin()*100 local ai=tostring(flight.ai) local lead=flight.seclead @@ -5421,8 +5486,7 @@ function AIRBOSS:_CreateFlightGroup(group) 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.flag=-100 flight.ai=not human flight.actype=group:GetTypeName() flight.onboardnumbers=self:_GetOnboardNumbers(group) @@ -5562,7 +5626,7 @@ function AIRBOSS:_InitPlayer(playerData, step) playerData.Tlso=timer.getTime() playerData.Tgroove=nil playerData.wire=nil - playerData.flag:Set(-100) + playerData.flag=-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 @@ -6122,7 +6186,8 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.SPINNING then - -- Might still be better to stay in commencing? + -- Player is spinning. + self:_Spinning(playerData) elseif playerData.step==AIRBOSS.PatternStep.HOLDING then @@ -6263,7 +6328,7 @@ 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 + local rightflag=playerData.flag~=-42 -- Steps that the player could have missed during Case II/III. local step=playerData.step @@ -6309,7 +6374,7 @@ function AIRBOSS:_CheckMissedStepOnEntry(playerData) end -- Set flag value to -42. This is the value to ensure that this routine is not called again! - playerData.flag:Set(-42) + playerData.flag=-42 end end end @@ -6764,6 +6829,39 @@ end -- PATTERN functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Spinning +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Spinning(playerData) + + -- Early break. + local SpinIt={} + SpinIt.name="Spinning" + SpinIt.Xmin=-UTILS.NMToMeters(5) -- Not more than 5 NM behind the boat. + SpinIt.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. + SpinIt.Zmin=-UTILS.NMToMeters(5) -- Not more than 5 NM port. + SpinIt.Zmax= UTILS.NMToMeters(3) -- Not more than 3 NM starboard. + SpinIt.LimitXmin=100 -- 100 meters ahead and a bit starboard. + SpinIt.LimitXmax=nil + SpinIt.LimitZmin=10 + SpinIt.LimitZmax=nil + + -- 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 we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, SpinIt) then + + -- Player is "de-spinned". + --self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.EARLYBREAK) + + -- Remove player from spinning queue. + --self:_RemoveFlightFromQueue(self.Qspinning, playerData) + + end + +end + --- Waiting outside 10 NM zone for free Marshal stack. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. @@ -6802,7 +6900,7 @@ function AIRBOSS:_Holding(playerData) local unit=playerData.unit -- Current stack. - local stack=playerData.flag:Get() + local stack=playerData.flag --------------------------- -- Holding Pattern Check -- @@ -11180,7 +11278,7 @@ function AIRBOSS:_CheckPatternUpdate() -- Update marshal pattern of AI keeping the same stack. if flight.ai then - self:_MarshalAI(flight, flight.flag:Get()) + self:_MarshalAI(flight, flight.flag) end end @@ -12620,6 +12718,92 @@ function AIRBOSS:_RequestMarshal(_unitName) end end +--- Request spinning. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestSpinning(_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="" + if not self:_InQueue(self.Qpattern, playerData.group) then + + -- Player not in pattern queue + text="negative, you have to be in the pattern to spin it!" + + --[[ + elseif playerData.seclead~=playerData.name then + + -- Player is not section lead + text="negative, your section lead has to call spinning." + ]] + + -- Check if player is in the right step. + elseif not (playerData.step==AIRBOSS.PatternStep.BREAKENTRY or + playerData.step==AIRBOSS.PatternStep.EARLYBREAK or + playerData.step==AIRBOSS.PatternStep.LATEBREAK or + playerData.step==AIRBOSS.PatternStep.INITIAL) then + + text="negative, you have to be in the right step to spin it!" + + else + + -- Check if player is in the pattern. + if self:_InQueue(self.Qspinning, playerData.group) then + + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + text="Proceed to early break." + end + + -- Player is "de-spinned". + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.EARLYBREAK) + + -- Remove player from spinning queue + self:_RemoveFlightFromQueue(self.Qspinning, playerData) + + else + + -- Set player step. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.SPINNING) + + -- Add player to spinning queue. + table.insert(self.Qspinning, playerData) + + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + text="Spin it!" + end + + -- Set step for section members. + --[[ + for _,sec in pairs(playerData.section) do + local sectionmember=sec --#AIRBOSS.PlayerData + self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.SPINNING) + if sectionmember.difficulty~=AIRBOSS.Difficulty.HARD then + text="Spin it!" + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end + ]] + + end + + end + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL") + + end + end +end + --- Request to commence landing approach. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. @@ -12641,7 +12825,7 @@ function AIRBOSS:_RequestCommence(_unitName) if _unit:IsInZone(self.zoneCCA) then -- Get stack value. - local stack=playerData.flag:Get() + local stack=playerData.flag -- Number of airborne aircraft currently in pattern. local _,npattern=self:_GetQueueInfo(self.Qpattern) @@ -13240,7 +13424,7 @@ function AIRBOSS:_DisplayQueue(_unitname, qname) local flight=_flight --#AIRBOSS.FlightGroup local charlie=self:_GetCharlieTime(flight) local Charlie=UTILS.SecondsToClock(charlie) - local stack=flight.flag:Get() + local stack=flight.flag local angels=self:_GetAngels(self:_GetMarshalAltitude(stack, flight.case)) local _,nunit,nsec=self:_GetFlightUnits(flight, true) local nick=self:_GetACNickname(flight.actype) @@ -13559,7 +13743,7 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) end -- Stack. - local stack=playerData.flag:Get() + local stack=playerData.flag -- Stack text. local stacktext=nil @@ -13656,7 +13840,7 @@ function AIRBOSS:_MarkMarshalZone(_unitName, flare) if playerData then -- Get player stack and recovery case. - local stack=playerData.flag:Get() + local stack=playerData.flag local case=playerData.case local text="" @@ -13940,7 +14124,8 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) self:I(self.lid..text) -- Header line - local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Mission Time,Wind,Airframe,Modex,Carrier Type,Carrier Name,Theatre,Date\n" + --local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Mission Time,Wind,Airframe,Modex,Carrier Type,Carrier Name,Theatre,Date OS\n" + local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type, Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n" -- Loop over all players. for playername,grades in pairs(self.playerscores) do From 412d9e7a82bb5313add4f9a45b24e4e3ccf31d37 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 11 Feb 2019 12:24:52 +0100 Subject: [PATCH 161/485] AIRBOSS v0.9.7 --- Moose Development/Moose/Ops/Airboss.lua | 632 ++++++++++++-------- Moose Development/Moose/Utilities/Utils.lua | 10 + 2 files changed, 406 insertions(+), 236 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 56de15611..53d87a30c 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -57,11 +57,11 @@ -- -- ## IMPORTANT -- --- Some important restrictions of DCS you should be aware of: +-- Some important restrictions (of DCS) you should be aware of: -- -- * Each player slot (client) should be in a separate group as DCS does only allow for sending messages to groups and not 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. --- * The modex (tail number) of an aircraft should be changed dynamically in the mission by a player. Unfortunately, there is no way to get this information via scripting API functions. +-- * Players are identified by their player name. Hence, ensure that no two player have the same name, e.g. "New Callsign", as this will lead to unexpected results. +-- * The modex (tail number) of an aircraft should **not** be changed dynamically in the mission by a player. Unfortunately, there is no way to get this information via scripting API functions. -- * The A-4E-C mod needs *easy comms* activated to interact with the F10 radio menu. -- -- ## Youtube Videos @@ -70,6 +70,7 @@ -- -- * [[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) +-- * [[MOOSE] Airboss - Groove Test: On-the-fly LSO Grading](https://www.youtube.com/watch?v=Xgs1hwDcPyM) -- -- Lex explaining Boat Ops: -- @@ -84,7 +85,6 @@ -- -- ### 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? A: Go back to start! -- * 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? @@ -199,6 +199,8 @@ -- @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. -- @field #boolean despawnshutdown Despawn group after engine shutdown. +-- @field #number Tbeacon Last time the beacons were refeshed. +-- @field #number dTbeacon Time interval to refresh the beacons. Default 5 minutes. -- @extends Core.Fsm#FSM --- Be the boss! @@ -333,13 +335,14 @@ -- ### Spinning -- -- If the pattern is full, players can go into the spinning pattern. This step is only allowed, if the player is in the pattern and his next step --- is initial, break entry, early/late break. +-- is initial, break entry, early/late break. At this point, the player should climb to 1200 ft a fly on the port side of the boat to go back to the initial again. -- -- If a player is in the spin pattern, flights in the Marshal queue should hold their altitude and are not allowed into the pattern until the spinning aircraft -- proceeds. -- +-- Once the player reaches a point 100 meters behind the boat and at least 1 NM port, his step is set to "Initial" and he can resume the normal pattern approach. -- --- If a player +-- If necessary, the player can call "Spinning" again when in the above mentioned steps. -- -- ### [Reset My Status] -- @@ -395,7 +398,7 @@ -- -- ### LSO Radio Check -- --- LSO will transmit a short message on his radio frequency. See @{#AIRBOSS.SetLSORadio}. +-- LSO will transmit a short message on his radio frequency. See @{#AIRBOSS.SetLSORadio}. Note that in the A-4E you will not hear the message unless you are in the pattern. -- -- ### Marshal Radio Check -- @@ -643,16 +646,18 @@ -- -- 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 so that to total wind above the deck. --- For example, if the is blowing with 5 knots, the carrier will go 15 knots so that it adds up to the specified 20 knots. +-- 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 so that to total wind above the deck +-- corresponds to this wind speed. For example, if the is blowing with 5 knots, the carrier will go 15 knots so that the total velocity adds up to the specified 20 knots for the pilot. -- -- 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 +-- 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. -- +-- Note that the carrier will only head into the wind, if the wind direction is different by more than 5° from the current heading of the carrier (the angled runway, if any, fis taken into account here). +-- -- === -- -- # Persistence of Player Results @@ -991,6 +996,8 @@ AIRBOSS = { Tmessage = nil, soundfolder = nil, despawnshutdown= nil, + dTbeacon = nil, + Tbeacon = nil, } --- Aircraft types capable of landing on carrier (human+AI). @@ -1637,14 +1644,15 @@ AIRBOSS.Difficulty={ -- @field #number wire Wire caught. -- @field #number Tgroove Time in the groove in seconds. -- @field #number case Recovery case. --- @field #string time Mission time. -- @field #string wind Wind speed on deck in knots. --- @field #string airframe Aircraft type name of player. -- @field #string modex Onboard number. +-- @field #string airframe Aircraft type name of player. -- @field #string carriertype Carrier type name. -- @field #string carriername Carrier name/alias. -- @field #string theatre DCS map. --- @field #string date Real live date. Needs **os** to be desanitized. +-- @field #string mitime Mission time in hh:mm:ss+d format +-- @field #string midate Mission date in yyyy/mm/dd format. +-- @field #string osdate Real live date. Needs **os** to be desanitized. --- Checkpoint parameters triggering the next step in the pattern. -- @type AIRBOSS.Checkpoint @@ -1687,7 +1695,6 @@ AIRBOSS.Difficulty={ -- @field #string onboard Onboard number of the aircraft. -- @field #boolean ballcall If true, flight called the ball in the groove. -- @field #boolean recovered If true, element was successfully recovered. --- @field #boolean isseclead If true, element is the section lead. --- Player data table holding all important parameters of each player. -- @type AIRBOSS.PlayerData @@ -1727,7 +1734,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.7w" +AIRBOSS.version="0.9.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1822,6 +1829,10 @@ function AIRBOSS:New(carriername, alias) -- Set some string id for output to DCS.log file. self.lid=string.format("AIRBOSS %s | ", carriername) + + -- Current map. + self.theatre=env.mission.theatre + self:T2(self.lid..string.format("Theatre = %s.", tostring(self.theatre))) -- Get carrier type. self.carriertype=self.carrier:GetTypeName() @@ -1865,6 +1876,9 @@ function AIRBOSS:New(carriername, alias) -- Set TACAN to channel 74X. self:SetTACAN() + + -- Becons are reactivated very 5 min. + self:SetBeaconRefresh() -- Set max aircraft in landing pattern. Default 4. self:SetMaxLandingPattern() @@ -2678,6 +2692,16 @@ function AIRBOSS:SetICLS(channel, morsecode) end +--- Set beacon (TACAN/ICLS) time refresh interfal in case the beacons die. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 300 sec = 5 min. +-- @return #AIRBOSS self +function AIRBOSS:SetBeaconRefresh(interval) + self.dTbeacon=interval or 300 + return self +end + + --- Set LSO radio frequency and modulation. Default frequency is 264 MHz AM. -- @param #AIRBOSS self -- @param #number frequency Frequency in MHz. Default 264 MHz. @@ -2927,6 +2951,25 @@ function AIRBOSS:IsPaused() return self:is("Paused") end +--- Activate TACAN and ICLS beacons. +-- @param #AIRBOSS self +function AIRBOSS:_ActivateBeacons() + self:T(self.lid..string.format("Activating Beacons (TACAN=%s, ICLS=%s)", tostring(self.TACANon), tostring(self.ICLSon))) + + -- Activate TACAN. + if self.TACANon then + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) + end + + -- Activate ICLS. + if self.ICLSon then + self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) + end + + -- Set time stamp. + self.Tbeacon=timer.getTime() +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM event functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2939,21 +2982,10 @@ end 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)) + self:I(self.lid..string.format("Starting AIRBOSS v%s for carrier unit %s of type %s on map %s", AIRBOSS.version, self.carrier:GetName(), self.carriertype, self.theatre)) - -- Activate TACAN. - if self.TACANon then - self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) - end - - -- Current map. - self.theatre=env.mission.theatre - self:T2(self.lid..string.format("Theatre = %s.", tostring(self.theatre))) - - -- Activate ICLS. - if self.ICLSon then - self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) - end + -- Activate TACAN and ICLS if desired. + self:_ActivateBeacons() -- Schedule radio queue checks. self.RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.01) @@ -3073,6 +3105,11 @@ function AIRBOSS:onafterStatus(From, Event, To) self.Tqueue=time end + -- (Re-)activate TACAN and ICLS channels. + if time-self.Tbeacon>self.dTbeacon then + self:_ActivateBeacons() + end + -- Check player status. self:_CheckPlayerStatus() @@ -3145,6 +3182,24 @@ end -- @param #AIRBOSS.PlayerData player Player data. function AIRBOSS:_CheckPlayerPatternDistance(player) + -- Check if player is too close to another aircraft in the pattern. + -- TODO: At which steps is the really necessary. Case II/III? + if player.step==AIRBOSS.PatternStep.INITIAL or + player.step==AIRBOSS.PatternStep.BREAKENTRY or + player.step==AIRBOSS.PatternStep.EARLYBREAK or + player.step==AIRBOSS.PatternStep.LATEBREAK or + player.step==AIRBOSS.PatternStep.ABEAM or + player.step==AIRBOSS.PatternStep.GROOVE_XX or + player.step==AIRBOSS.PatternStep.GROOVE_IM then + + -- Right step but not implemented. + return + + else + -- Wrong step - no check performed. + return + end + -- Nothing to do since we check only in the pattern. if #self.Qpattern==0 then return @@ -3344,16 +3399,33 @@ function AIRBOSS:_CheckRecoveryTimes() -- 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 + -- Check that wind is blowing from a direction > 5° different from the current heading. + local hdg=self:GetHeading() + local wind=self:GetHeadingIntoWind() + local delta=self:_GetDeltaHeading(hdg, wind) + local uturn=delta>5 + + -- Check if wind is actually blowing (0.1 m/s = 0.36 km/h = 0.2 knots) + local _,vwind=self:GetWind() + if vwind<0.1 then + uturn=false + end + + --Debug info + self:T(self.lid..string.format("Heading=%03d°, Wind=%03d° %.1f kts, Delta=%03d° ==> U-turn=%s", hdg, wind,UTILS.MpsToKnots(vwind), delta, tostring(uturn))) - -- Distance in NM to go + 1 NM safety. - local d=UTILS.MetersToNM(s)+1 + + -- Time into the wind + the 5 min early. + local t=nextwindow.STOP-nextwindow.START+300 + local v=UTILS.KnotsToMps(nextwindow.SPEED) + + -- Check that we do not go above max possible speed. + local vmax=self.carrier:GetSpeedMax() + v=math.min(v,vmax) -- Route carrier into the wind. Sets self.turnintowind=true - self:CarrierTurnIntoWind(t, v) + self:CarrierTurnIntoWind(t, v, uturn) + end -- Set current recovery window. @@ -4913,7 +4985,7 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- For CCW pattern: p1 further astern than p2. -- Length of the race track pattern. - local l=UTILS.NMToMeters(7) + local l=UTILS.NMToMeters(10) -- First point of race track pattern. p1=Carrier:Translate(Dist+l, radial) @@ -5055,10 +5127,7 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) -- If the carrier is supposed to turn into the wind, we take the wind coordinate. if self.recoverywindow and self.recoverywindow.WIND then - local _,vwind=self:GetCoordinate():GetWind(50) - if vwind>0.1 then - brc=self:GetBRCintoWind() - end + brc=self:GetBRCintoWind() end -- Get charlie time estimate. @@ -5521,7 +5590,6 @@ function AIRBOSS:_CreateFlightGroup(group) if flight.ai then local onboard=flight.onboardnumbers[flight.seclead] flight.onboard=onboard - flight.elements[1].isseclead=true else flight.onboard=self:_GetOnboardNumberPlayer(group) end @@ -5549,6 +5617,7 @@ function AIRBOSS:_NewPlayer(unitname) if playerunit and playername then + -- Get group. local group=playerunit:GetGroup() -- Player data. @@ -5557,45 +5626,50 @@ function AIRBOSS:_NewPlayer(unitname) -- Create a flight group for the player. playerData=self:_CreateFlightGroup(group) - -- Player unit, client and callsign. - playerData.unit = playerunit - playerData.name = playername - playerData.callsign = playerData.unit:GetCallsign() - playerData.client = CLIENT:FindByName(unitname, nil, true) - playerData.seclead = playername - - -- Number of passes done by player in this slot. - playerData.passes=0 --playerData.passes or 0 + -- Nil check. + if playerData then - -- Debriefing tables. - playerData.lastdebrief=playerData.lastdebrief or {} + -- Player unit, client and callsign. + playerData.unit = playerunit + playerData.name = playername + playerData.callsign = playerData.unit:GetCallsign() + playerData.client = CLIENT:FindByName(unitname, nil, true) + playerData.seclead = playername + + -- Number of passes done by player in this slot. + playerData.passes=0 --playerData.passes or 0 + + -- Debriefing tables. + playerData.lastdebrief=playerData.lastdebrief or {} + + -- Attitude monitor. + playerData.attitudemonitor=false + + -- 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={} - -- Attitude monitor. - playerData.attitudemonitor=false + -- Init stuff for this round. + playerData=self:_InitPlayer(playerData) + + -- 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), string.format("AIRBOSS %s", self.alias), "", 5) - -- 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={} - - -- Init stuff for this round. - playerData=self:_InitPlayer(playerData) - - -- 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), string.format("AIRBOSS %s", self.alias), "", 5) - -- Return player data table. return playerData end @@ -5633,8 +5707,8 @@ function AIRBOSS:_InitPlayer(playerData, step) self:MessageToPlayer(playerData, "Group name contains \"Groove\". Happy groove testing.") playerData.attitudemonitor=true playerData.step=AIRBOSS.PatternStep.FINAL - --table.insert(self.Qpattern, playerData) self:_AddFlightToPatternQueue(playerData) + self.dTstatus=0.1 end return playerData @@ -6083,7 +6157,8 @@ function AIRBOSS:_RemoveFlight(flight, completely) -- Remove flight from all queues. self:_RemoveFlightFromMarshalQueue(flight, true) self:_RemoveFlightFromQueue(self.Qpattern, flight) - self:_RemoveFlightFromQueue(self.Qwaiting, flight) + self:_RemoveFlightFromQueue(self.Qwaiting, flight) + self:_RemoveFlightFromQueue(self.Qspinning, flight) -- Check if player or AI if flight.ai then @@ -6118,6 +6193,8 @@ function AIRBOSS:_RemoveFlight(flight, completely) -- Also set this for the section members as they are in the same boat. for _,sectionmember in pairs(flight.section) do self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.UNDEFINED) + -- Also remove section member in case they are in the spinning queue. + self:_RemoveFlightFromQueue(self.Qspinning, sectionmember) end -- What if flight is member of a section. His status is now undefined. Should he be removed from the section? @@ -6156,18 +6233,9 @@ function AIRBOSS:_CheckPlayerStatus() 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 - 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 + + -- Check distance to other flights. + self:_CheckPlayerPatternDistance(playerData) -- Foul deck check. self:_CheckFoulDeck(playerData) @@ -6379,6 +6447,40 @@ function AIRBOSS:_CheckMissedStepOnEntry(playerData) end end end + +--- Set time in the groove for player. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_SetTimeInGroove(playerData) + + -- Get time in the groove. + local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData + if gdataX0 then + playerData.Tgroove=timer.getTime()-gdataX0.TGroove + else + playerData.Tgroove=9999 + end + +end + +--- Get time in the groove of player. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return #number Player's time in groove in seconds. +function AIRBOSS:_GetTimeInGroove(playerData) + + local Tgroove=999 + + -- Get time in the groove. + local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData + if gdataX0 then + Tgroove=timer.getTime()-gdataX0.TGroove + end + + return Tgroove +end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- EVENT functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -6544,13 +6646,13 @@ function AIRBOSS:OnEventLand(EventData) coord:SmokeGreen() end - -- Get time in the groove. - local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData - if gdataX0 then - playerData.Tgroove=timer.getTime()-gdataX0.TGroove - else - playerData.Tgroove=999 - end + -- Set time in the groove of player. + self:_SetTimeInGroove(playerData) + + -- Debug text. + local text=string.format("Player %s AC type %s landed at dist=%.1f m. Tgroove=%.1f sec.", playerData.name, playerData.actype, dist, self:_GetTimeInGroove(playerData)) + text=text..string.format(" X=%.1f m, Z=%.1f m, rho=%.1f m.", X, Z, rho) + self:T(self.lid..text) -- Check carrier type. if self.carriertype==AIRBOSS.CarrierType.TARAWA then @@ -6563,24 +6665,9 @@ function AIRBOSS:OnEventLand(EventData) else - -- Get wire. We additionally shift the landing coord back because landing event for players is unfortunately delayed. - local wire=self:_GetWire(coord, 75) - - -- 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 - - -- 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.", X, Z, rho) - self:T(self.lid..text) - - -- Unkonwn step until we now more. - playerData.step=AIRBOSS.PatternStep.UNDEFINED - playerData.warning=nil - + -- Next step undefined until we know more. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.UNDEFINED) + -- Call trapped function in 1 second to make sure we did not bolter. SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1) @@ -6693,22 +6780,43 @@ function AIRBOSS:OnEventTakeoff(EventData) self:T3(self.lid.."TAKEOFF: group = "..tostring(EventData.IniGroupName)) self:T3(self.lid.."TAKEOFF: player = "..tostring(_playername)) - if _unit and _playername then + -- Airbase. + local airbase=EventData.Place - -- Debug message. - self:T(self.lid..string.format("Player %s took off!",_playername)) - - -- TODO: Set recoverd status. - - else + -- Airbase name. + local airbasename="unknown" + if airbase then + airbasename=airbase:GetName() + end - -- Debug message. - self:T2(self.lid..string.format("AI unit %s took off!", _unitName)) + -- Check right airbase. + if airbasename==self.carrier:GetName() then + + if _unit and _playername then - -- TODO: Set recoverd status. + -- Debug message. + self:T(self.lid..string.format("Player %s took off at %s!",_playername, airbasename)) + + else - end - + -- Debug message. + self:T2(self.lid..string.format("AI unit %s took off at %s!", _unitName, airbasename)) + + -- Get flight. + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + + if flight then + + -- Set ballcall and recoverd status. + for _,elem in pairs(flight.elements) do + local element=elem --#AIRBOSS.FlightElement + element.ballcall=false + element.recovered=nil + end + end + end + + end end --- Airboss event handler for event crash. @@ -6837,26 +6945,26 @@ function AIRBOSS:_Spinning(playerData) -- Early break. local SpinIt={} SpinIt.name="Spinning" - SpinIt.Xmin=-UTILS.NMToMeters(5) -- Not more than 5 NM behind the boat. + SpinIt.Xmin=-UTILS.NMToMeters(6) -- Not more than 5 NM behind the boat. SpinIt.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. - SpinIt.Zmin=-UTILS.NMToMeters(5) -- Not more than 5 NM port. - SpinIt.Zmax= UTILS.NMToMeters(3) -- Not more than 3 NM starboard. - SpinIt.LimitXmin=100 -- 100 meters ahead and a bit starboard. + SpinIt.Zmin=-UTILS.NMToMeters(6) -- Not more than 5 NM port. + SpinIt.Zmax= UTILS.NMToMeters(2) -- Not more than 3 NM starboard. + SpinIt.LimitXmin=-100 -- 100 meters behind the boat SpinIt.LimitXmax=nil - SpinIt.LimitZmin=10 - SpinIt.LimitZmax=nil - + SpinIt.LimitZmin=-UTILS.NMToMeters(1) -- 1 NM port + SpinIt.LimitZmax=nil + -- 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 we are in front of the boat (diffX > 0). if self:_CheckLimits(X, Z, SpinIt) then - -- Player is "de-spinned". - --self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.EARLYBREAK) + -- Player is "de-spinned". Should go to initial again. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.INITIAL) -- Remove player from spinning queue. - --self:_RemoveFlightFromQueue(self.Qspinning, playerData) + self:_RemoveFlightFromQueue(self.Qspinning, playerData) end @@ -7820,8 +7928,8 @@ function AIRBOSS:_Final(playerData) -- 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 + -- Check if player is in +-4 deg cone and flying towards the runway. + if math.abs(lineup)<=4 then -- Get optimal altitude, distance and speed. local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) @@ -8448,7 +8556,7 @@ function AIRBOSS:_GetWire(Lcoord, dc) wire=99 end - if self.Debug then + if self.Debug and false then -- Wire position coodinates. local wp1=Scoord:Translate(w1, FB) @@ -8800,7 +8908,7 @@ function AIRBOSS:_GetZoneCorridor(case) -- Length of the box in NM. local x=(d+w/2)/math.cos(alpha) - local l=28-x + local l=31-x -- Some math... local y1=d-w2 @@ -9193,6 +9301,7 @@ function AIRBOSS:_AttitudeMonitor(playerData) text=text..string.format("\nDist=%.1f m Alt=%.1f m delta|V|=%.1f km/h", dist, self:_GetAltCarrier(playerData.unit), dv) 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("\nTgroove=%.1f sec", self:_GetTimeInGroove(playerData)) 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) @@ -9400,13 +9509,15 @@ end --- Get wind direction and speed at carrier position. -- @param #AIRBOSS self --- @param #number alt Altitude in meters. Default 50 m. +-- @param #number alt Altitude ASL in meters. Default 50 m. -- @return #number Direction the wind is blowing **from** in degrees. -- @return #number Wind speed in m/s. function AIRBOSS:GetWind(alt) + -- Current position of the carrier local cv=self:GetCoordinate() + -- Wind direction and speed. By default at 50 meters ASL. local Wdir, Wspeed=cv:GetWind(alt or 50) return Wdir, Wspeed @@ -9425,16 +9536,24 @@ function AIRBOSS:GetWindOnDeck(alt) -- Velocity vector of carrier. local vc=self.carrier:GetVelocityVec3() + -- Rotate to angled deck. + vc=UTILS.Rotate2D(vc, self.carrierparam.rwyangle) + -- Wind (from) vector local vw=cv:GetWindWithTurbulenceVec3(alt or 50) -- Carrier velocity has to be negative. If carrier drives in the direction the wind is blowing from, we have less wind in total. local vd=UTILS.VecSubstract(vw, vc) + + local vh=math.deg(math.atan2(vd.z, vd.x)) + if vh<0 then + vh=vh+360 + end -- Strength. local vabs=UTILS.VecNorm(vd) - return vd, vabs + return vh, vabs end @@ -9445,11 +9564,16 @@ end function AIRBOSS:GetHeadingIntoWind(magnetic) -- Get direction the wind is blowing from. This is where we want to go. - local windfrom=self:GetCoordinate():GetWind(50) + local windfrom, vwind=self:GetWind() -- Actually, we want the runway in the wind. local intowind=windfrom-self.carrierparam.rwyangle + -- If no wind, take current heading. + if vwind<0.1 then + intowind=self:GetHeading() + end + -- Magnetic heading. if magnetic then intowind=intowind-self.magvar @@ -9564,6 +9688,28 @@ function AIRBOSS:GetRadial(case, magnetic, offset, inverse) return radial end +--- Get difference between to headings in degrees taking into accound the [0,360) periodocity. +-- @param #AIRBOSS self +-- @param #number hdg1 Heading one. +-- @param #number hdg2 Heading two. +-- @return #number Difference between the two headings in degrees. +function AIRBOSS:_GetDeltaHeading(hdg1, hdg2) + + local V={} --DCS#Vec3 + V.x=math.cos(math.rad(hdg1)) + V.y=0 + V.z=math.sin(math.rad(hdg1)) + + local W={} --DCS#Vec3 + W.x=math.cos(math.rad(hdg2)) + W.y=0 + W.z=math.sin(math.rad(hdg2)) + + local alpha=UTILS.VecAngle(V,W) + + return alpha +end + --- Get relative heading of player wrt carrier. -- 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. @@ -10575,17 +10721,18 @@ function AIRBOSS:_Debrief(playerData) mygrade.finalscore=Points end mygrade.case=playerData.case - mygrade.time=UTILS.SecondsToClock(timer.getAbsTime()) local _,windondeck=self:GetWindOnDeck() - mygrade.wind=tostring(UTILS.Round(windondeck, 1)) - mygrade.airframe=playerData.actype + mygrade.wind=tostring(UTILS.Round(UTILS.MpsToKnots(windondeck), 1)) mygrade.modex=playerData.onboard + mygrade.airframe=playerData.actype mygrade.carriertype=self.carriertype mygrade.carriername=self.alias mygrade.theatre=self.theatre - mygrade.date="n/a" + mygrade.mitime=UTILS.SecondsToClock(timer.getAbsTime()) + mygrade.midate=UTILS.GetDCSMissionDate() + mygrade.osdate="n/a" if os then - mygrade.date=os.date() --os.date("%d.%m.%Y") + mygrade.osdate=os.date() --os.date("%d.%m.%Y") end -- Add LSO grade to player grades table. @@ -11055,17 +11202,13 @@ end -- @param #AIRBOSS self -- @param #number time Time in seconds. -- @param #number vdeck Speed on deck m/s. Carrier will +-- @param #boolean uturn Make U-turn and go back to initial after downwind leg. -- @return #AIRBOSS self -function AIRBOSS:CarrierTurnIntoWind(time, vdeck) +function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) -- Wind speed. - local _,vwind=self:GetCoordinate():GetWind(50) + local _,vwind=self:GetWind() - -- Check that wind is >= 0.1 m/s. - if vwind<0.1 then - return - end - -- Speed of carrier in m/s. local vtot=vdeck-vwind @@ -11092,18 +11235,15 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck) -- Return to coordinate if collision is detected. self.Creturnto=self:GetCoordinate() - - -- 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 + local nextwp=self:_GetNextWaypoint() -- For downwind, we take the velocity at the next WP. local vdownwind=UTILS.MpsToKnots(nextwp:GetVelocity()) -- Let the carrier make a detour from its route but return to its current position. - self:CarrierDetour(pos1, speedknots, true, vdownwind) + self:CarrierDetour(pos1, speedknots, uturn, vdownwind) -- Set switch that we are currently turning into the wind. self.turnintowind=true @@ -11111,6 +11251,21 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck) return self end +--- Get next waypoint of the carrier. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Coordinate of the next waypoint. +-- @return #number Number of waypoint +function AIRBOSS:_GetNextWaypoint() + + -- 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 + + return nextwp,Nnextwp +end + --- Patrol carrier. -- @param #AIRBOSS self -- @return #AIRBOSS self @@ -11323,6 +11478,9 @@ function AIRBOSS._PassingWaypoint(group, airboss, i, final) -- Set current waypoint. airboss.currentwp=i + -- Reactivate beacons. + --airboss:_ActivateBeacons() + -- If final waypoint reached, do route all over again. if i==final and final>1 and airboss.adinfinitum then airboss:_PatrolRoute() @@ -11335,15 +11493,11 @@ end --@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) - - -- Debug message. - local text=string.format("Group %s is resuming route. Next waypoint %d.", group:GetName(), nextwp) + -- Get next waypoint + local nextwp,Nextwp=airboss:_GetNextWaypoint() - -- Debug message. - MESSAGE:New(text,10):ToAllIf(airboss.Debug) - airboss:T(airboss.lid..text) + -- Velocity at that coordinate. + local speedkmh=nextwp.Velocity*3.6 -- Waypoints array. local waypoints={} @@ -11352,16 +11506,24 @@ function AIRBOSS._ResumeRoute(group, airboss, gotocoord) local velocity=group:GetVelocityKMH() -- Current positon as first waypoint. - local wp0=group:GetCoordinate():WaypointGround(velocity) + local wp0=group:GetCoordinate():WaypointGround(speedkmh) table.insert(waypoints, wp0) + -- First goto this coordinate. if gotocoord then - local wp1=gotocoord:WaypointGround(velocity) + local wp1=gotocoord:WaypointGround(speedkmh) table.insert(waypoints, wp1) end + -- Debug message. + local text=string.format("Carrier is resuming route. Next waypoint %d, Speed=%.1f knots.", Nextwp, UTILS.KmphToKnots(speedkmh)) + + -- Debug message. + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:I(airboss.lid..text) + -- Loop over all remaining waypoints. - for i=nextwp, #airboss.waypoints do + for i=Nextwp, #airboss.waypoints do -- Coordinate of the next WP. local coord=airboss.waypoints[i] --Core.Point#COORDINATE @@ -12592,7 +12754,8 @@ function AIRBOSS:_AddF10Commands(_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 - missionCommands.addCommandForGroup(gid, "[Reset My Status]", _rootPath, self._ResetPlayerStatus, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "Spinning", _rootPath, self._RequestSpinning, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "[Reset My Status]", _rootPath, self._ResetPlayerStatus, self, _unitName) -- F7 end else self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) @@ -12627,7 +12790,7 @@ function AIRBOSS:_ResetPlayerStatus(_unitName) self:MessageToPlayer(playerData, text, "AIRBOSS") -- Remove flight from queues. Collapse marshal stack if necessary. - -- TODO: This has not the completely tag. What to do with section members if flight is lead or not? + -- Section members are removed from the Spinning queue. If flight is member, he is removed from the section. self:_RemoveFlight(playerData) -- Initialize player data. @@ -12746,54 +12909,44 @@ function AIRBOSS:_RequestSpinning(_unitName) text="negative, your section lead has to call spinning." ]] + elseif playerData.step==AIRBOSS.PatternStep.SPINNING then + + text="negative, you are already spinning." + -- Check if player is in the right step. elseif not (playerData.step==AIRBOSS.PatternStep.BREAKENTRY or - playerData.step==AIRBOSS.PatternStep.EARLYBREAK or - playerData.step==AIRBOSS.PatternStep.LATEBREAK or - playerData.step==AIRBOSS.PatternStep.INITIAL) then + playerData.step==AIRBOSS.PatternStep.EARLYBREAK or + playerData.step==AIRBOSS.PatternStep.LATEBREAK) then text="negative, you have to be in the right step to spin it!" else - - -- Check if player is in the pattern. - if self:_InQueue(self.Qspinning, playerData.group) then + + -- Set player step. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.SPINNING) - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - text="Proceed to early break." - end + -- Add player to spinning queue. + table.insert(self.Qspinning, playerData) - -- Player is "de-spinned". - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.EARLYBREAK) - - -- Remove player from spinning queue - self:_RemoveFlightFromQueue(self.Qspinning, playerData) - - else - - -- Set player step. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.SPINNING) - - -- Add player to spinning queue. - table.insert(self.Qspinning, playerData) - - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - text="Spin it!" - end - - -- Set step for section members. - --[[ - for _,sec in pairs(playerData.section) do - local sectionmember=sec --#AIRBOSS.PlayerData - self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.SPINNING) - if sectionmember.difficulty~=AIRBOSS.Difficulty.HARD then - text="Spin it!" - self:MessageToPlayer(playerData, text, "MARSHAL") - end - end - ]] - + -- Some advice. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + text="Spin it!" end + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + text=text.." Climb to 1200 feet and proceed to the initial." + end + + -- Set step for section members. + --[[ + for _,sec in pairs(playerData.section) do + local sectionmember=sec --#AIRBOSS.PlayerData + self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.SPINNING) + if sectionmember.difficulty~=AIRBOSS.Difficulty.HARD then + text="Spin it!" + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end + ]] end @@ -13311,7 +13464,7 @@ function AIRBOSS:_DisplayPlayerGrades(_unitName) end -- Time in the groove if any. - if grade.Tgroove and grade.Tgroove<=60 then + if grade.Tgroove and grade.Tgroove<=120 then text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) end end @@ -13487,11 +13640,15 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) icls=string.format("%d (%s)", self.ICLSchannel, self.ICLSmorse) end + -- Wind on flight deck + local wind=UTILS.MpsToKnots(select(2, self:GetWindOnDeck())) + -- 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) + local Nmarshal,nmarshal = self:_GetQueueInfo(self.Qmarshal, playerData.case) + local Npattern,npattern = self:_GetQueueInfo(self.Qpattern) + local Nspinning,nspinning = self:_GetQueueInfo(self.Qspinning) + local Nwaiting,nwaiting = self:_GetQueueInfo(self.Qwaiting) + local Ntotal,ntotal = self:_GetQueueInfo(self.flights) -- Current abs time. local Tabs=timer.getAbsTime() @@ -13540,8 +13697,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) text=text..string.format("Case %d recovery ops\nMarshal radial %03d°\n", self.case, radial) end text=text..string.format("BRC %03d° - FB %03d°\n", self:GetBRC(), self:GetFinalBearing(true)) - --text=text..string.format("FB %03d°\n", self:GetFinalBearing(true)) - text=text..string.format("Speed %d kts\n", carrierspeed) + text=text..string.format("Speed %.1f kts - Wind on deck %.1f kts\n", carrierspeed, wind) text=text..string.format("Tower frequency %.3f MHz\n", self.TowerFreq) text=text..string.format("Marshal radio %.3f MHz\n", self.MarshalFreq) text=text..string.format("LSO radio %.3f MHz\n", self.LSOFreq) @@ -13550,10 +13706,9 @@ 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 (%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 pattern %d (%d) - spinning %d (%d)\n", Npattern, npattern, Nspinning, nspinning) text=text..string.format("# A/C waiting %d (%d)\n", Nwaiting, nwaiting) text=text..string.format(recoverytext) self:T2(self.lid..text) @@ -13590,11 +13745,14 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) -- Get atmospheric data at carrier location. local T=coord:GetTemperature() local P=coord:GetPressure() - local Wd,Ws=coord:GetWind(50) + local Wd,Ws=self:GetWind() -- Get Beaufort wind scale. local Bn,Bd=UTILS.BeaufortScale(Ws) + -- Wind on flight deck + local Wod=UTILS.MpsToKnots(select(2, self:GetWindOnDeck())) + local WD=string.format('%03d°', Wd) local Ts=string.format("%d°C",T) @@ -13607,6 +13765,7 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) 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("Wind on deck %.1f knots\n", Wod) text=text..string.format("QFE %.1f hPa = %s", P, tP) -- More info only reliable if Mission uses static weather. @@ -13778,7 +13937,7 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) 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("Modex: %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("# 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) @@ -14124,7 +14283,6 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) self:I(self.lid..text) -- Header line - --local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Mission Time,Wind,Airframe,Modex,Carrier Type,Carrier Name,Theatre,Date OS\n" local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type, Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n" -- Loop over all players. @@ -14141,7 +14299,7 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) end local Tgroove="n/a" - if grade.Tgroove and grade.Tgroove<=60 and grade.case<3 then + if grade.Tgroove and grade.Tgroove<=120 and grade.case<3 then Tgroove=tostring(UTILS.Round(grade.Tgroove, 1)) end @@ -14152,9 +14310,9 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) -- Compile grade line. --scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d\n", - scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s\n", + scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case, - grade.time, grade.wind, grade.airframe, grade.modex, grade.carriertype, grade.carriername, grade.theatre, grade.date) + grade.wind, grade.modex, grade.airframe, grade.carriertype, grade.carriername, grade.theatre, grade.mitime, grade.midate, grade.osdate) end end @@ -14296,14 +14454,16 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) grade.Tgroove=tonumber(gradedata[8]) end grade.case=tonumber(gradedata[9]) - grade.time=gradedata[10] or "n/a" - grade.wind=gradedata[11] or "n/a" + -- new + grade.wind=gradedata[10] or "n/a" + grade.modex=gradedata[11] or "n/a" grade.airframe=gradedata[12] or "n/a" - grade.modex=gradedata[13] or "n/a" - grade.carriertype=gradedata[14] or "n/a" - grade.carriername=gradedata[15] or "n/a" - grade.theatre=gradedata[16] or "n/a" - grade.date=gradedata[17] or "n/a" + grade.carriertype=gradedata[13] or "n/a" + grade.carriername=gradedata[14] or "n/a" + grade.theatre=gradedata[15] or "n/a" + grade.mitime=gradedata[16] or "n/a" + grade.midate=gradedata[17] or "n/a" + grade.osdate=gradedata[18] or "n/a" -- Init player table if necessary. self.playerscores[playername]=self.playerscores[playername] or {} diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 436c59b4b..f9af6400a 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -864,6 +864,16 @@ function UTILS.GetDCSMap() return env.mission.theatre end +--- Returns the mission date. This is the date the mission started. +-- @return #string Mission date in yyyy/mm/dd format. +function UTILS.GetDCSMissionDate() + local year=tostring(env.mission.date.Year) + local month=tostring(env.mission.date.Month) + local day=tostring(env.mission.date.Day) + return string.format("%s/%s/%s", year, month, day) +end + + --- Returns the magnetic declination of the map. -- Returned values for the current maps are: -- From 59e219e3d6c7b42469ffb6be50aa7fd04d272f43 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 11 Feb 2019 12:31:28 +0100 Subject: [PATCH 162/485] WAREHOUSE v0.6.9 Fixed memory issue. --- Moose Development/Moose/Core/Spawn.lua | 6 +++--- Moose Development/Moose/Functional/Warehouse.lua | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 2bc851087..d65aae7a2 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1615,12 +1615,12 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT -- Get parking data. local parkingdata=SpawnAirbase:GetParkingSpotsTable(termtype) - self:E(string.format("Parking at %s, terminal type %s:", SpawnAirbase:GetName(), tostring(termtype))) + self:T2(string.format("Parking at %s, terminal type %s:", SpawnAirbase:GetName(), tostring(termtype))) for _,_spot in pairs(parkingdata) do - self:E(string.format("%s, Termin Index = %3d, Term Type = %03d, Free = %5s, TOAC = %5s, Term ID0 = %3d, Dist2Rwy = %4d", + self:T2(string.format("%s, Termin Index = %3d, Term Type = %03d, Free = %5s, TOAC = %5s, Term ID0 = %3d, Dist2Rwy = %4d", SpawnAirbase:GetName(), _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy)) end - self:E(string.format("%s at %s: free parking spots = %d - number of units = %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), nfree, nunits)) + self:T(string.format("%s at %s: free parking spots = %d - number of units = %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), nfree, nunits)) -- Set this to true if not enough spots are available for emergency air start. local _notenough=false diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 4064e2f9c..3ce11bb07 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1733,7 +1733,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.8" +WAREHOUSE.version="0.6.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. From 5050371e0d8dc6c83ae6d17c17cf30073d68e702 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 12 Feb 2019 16:26:44 +0100 Subject: [PATCH 163/485] AB v0.9.8w --- Moose Development/Moose/Ops/Airboss.lua | 1328 +++++++++++------------ 1 file changed, 624 insertions(+), 704 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 53d87a30c..f7446f23d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -201,6 +201,8 @@ -- @field #boolean despawnshutdown Despawn group after engine shutdown. -- @field #number Tbeacon Last time the beacons were refeshed. -- @field #number dTbeacon Time interval to refresh the beacons. Default 5 minutes. +-- @field #AIRBOSS.LSOCalls LSOCall Radio voice overs of the LSO. +-- @field #AIRBOSS.MarshalCalls MarshalCall Radio voice over of the Marshal/Airboss. -- @extends Core.Fsm#FSM --- Be the boss! @@ -998,6 +1000,8 @@ AIRBOSS = { despawnshutdown= nil, dTbeacon = nil, Tbeacon = nil, + LSOCall = nil, + MarshalCall = nil, } --- Aircraft types capable of landing on carrier (human+AI). @@ -1164,7 +1168,7 @@ AIRBOSS.GroovePos={ -- @field #string sender Sender of the message (optional). Default radio alias. --- LSO radio calls. --- @type AIRBOSS.LSOCall +-- @type AIRBOSS.LSOCalls -- @field #AIRBOSS.RadioCall RADIOCHECK "Paddles, radio check" call. -- @field #AIRBOSS.RadioCall RIGHTFORLINEUP "Right for line up" call. -- @field #AIRBOSS.RadioCall COMELEFT "Come left" call. @@ -1200,276 +1204,6 @@ AIRBOSS.GroovePos={ -- @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", - suffix="ogg", - loud=false, - subtitle="Paddles, radio check", - duration=1.1, - subduration=5, - }, - RIGHTFORLINEUP={ - file="LSO-RightForLineup", - suffix="ogg", - loud=true, - subtitle="Right for line up", - duration=0.80, - subduration=1, - }, - COMELEFT={ - file="LSO-ComeLeft", - suffix="ogg", - loud=true, - subtitle="Come left", - duration=0.60, - subduration=1, - }, - HIGH={ - file="LSO-High", - suffix="ogg", - loud=true, - subtitle="You're high", - duration=0.65, - subduration=1, - }, - LOW={ - file="LSO-Low", - suffix="ogg", - loud=true, - subtitle="You're low", - duration=0.50, - subduration=1, - }, - POWER={ - file="LSO-Power", - suffix="ogg", - loud=true, - subtitle="Power", - duration=0.50, --0.45 was too short - subduration=1, - }, - SLOW={ - file="LSO-Slow", - suffix="ogg", - loud=true, - subtitle="You're slow", - duration=0.65, - subduration=1, - }, - FAST={ - file="LSO-Fast", - suffix="ogg", - loud=true, - subtitle="You're fast", - duration=0.7, - subduration=1, - }, - CALLTHEBALL={ - file="LSO-CallTheBall", - suffix="ogg", - loud=false, - subtitle="Call the ball", - duration=0.6, - subduration=2, - }, - ROGERBALL={ - file="LSO-RogerBall", - suffix="ogg", - loud=false, - subtitle="Roger ball", - duration=0.7, - subduration=2, - }, - WAVEOFF={ - file="LSO-WaveOff", - suffix="ogg", - loud=false, - subtitle="Wave off", - duration=0.6, - subduration=5, - }, - BOLTER={ - file="LSO-BolterBolter", - suffix="ogg", - loud=false, - subtitle="Bolter, Bolter", - duration=0.75, - subduration=5, - }, - LONGINGROOVE={ - file="LSO-LongInTheGroove", - suffix="ogg", - loud=false, - subtitle="You're long in the groove", - duration=1.2, - subduration=5, - }, - FOULDECK={ - file="LSO-FoulDeck", - suffix="ogg", - loud=false, - subtitle="Foul deck", - duration=0.62, - subduration=5, - }, - DEPARTANDREENTER={ - file="LSO-DepartAndReenter", - suffix="ogg", - loud=false, - subtitle="Depart and re-enter", - duration=1.1, - subduration=5, - }, - PADDLESCONTACT={ - file="LSO-PaddlesContact", - suffix="ogg", - loud=false, - subtitle="Paddles, contact", - duration=1.0, - subduration=5, - }, - WELCOMEABOARD={ - file="LSO-WelcomeAboard", - suffix="ogg", - loud=false, - subtitle="Welcome aboard", - duration=1.0, - subduration=5, - }, - EXPECTHEAVYWAVEOFF={ - file="LSO-ExpectHeavyWaveoff", - suffix="ogg", - loud=false, - subtitle="Expect heavy waveoff", - duration=1.2, - subduration=5, - }, - EXPECTSPOT75={ - file="LSO-ExpectSpot75", - suffix="ogg", - loud=false, - subtitle="Expect spot 7.5", - duration=2.0, - subduration=5, - }, - CLEAREDTOLAND={ - file="LSO-ClearedToLand", - suffix="ogg", - loud=false, - subtitle="Cleared to land", - duration=1.0, - subduration=5, - }, - CHECK={ - file="LSO-Check", - suffix="ogg", - loud=false, - subtitle="Check", - duration=0.45, - subduration=2.5, - }, - STABILIZED={ - file="LSO-Stabilized", - suffix="ogg", - loud=false, - subtitle="Stabilized", - duration=0.9, - subduration=5, - }, - IDLE={ - file="LSO-Idle", - suffix="ogg", - loud=false, - subtitle="Idle", - duration=0.45, - subduration=5, - }, - N0={ - file="LSO-N0", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N1={ - file="LSO-N1", - suffix="ogg", - loud=false, - subtitle="", - duration=0.25, - }, - N2={ - file="LSO-N2", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N3={ - file="LSO-N3", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N4={ - file="LSO-N4", - suffix="ogg", - loud=false, - subtitle="", - duration=0.39, - }, - N5={ - file="LSO-N5", - suffix="ogg", - loud=false, - subtitle="", - duration=0.39, - }, - N6={ - file="LSO-N6", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N7={ - file="LSO-N7", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N8={ - file="LSO-N8", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N9={ - file="LSO-N9", - suffix="ogg", - loud=false, - subtitle="", - 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, - }, -} --- Marshal radio calls. -- @type AIRBOSS.MarshalCall @@ -1488,117 +1222,14 @@ AIRBOSS.LSOCall={ -- @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.1, - subduration=5, - }, - SAYNEEDLES={ - file="MARSHAL-SayNeedles", - suffix="ogg", - loud=false, - subtitle="Say needles", - duration=0.9, - subduration=5, - }, - FLYNEEDLES={ - file="MARSHAL-FlyYourNeedles", - suffix="ogg", - loud=false, - subtitle="Fly your needles", - duration=0.9, - subduration=5, - }, - -- TODO: Other voice overs for marshal. - N0={ - file="LSO-N0", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N1={ - file="LSO-N1", - suffix="ogg", - loud=false, - subtitle="", - duration=0.25, - }, - N2={ - file="LSO-N2", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N3={ - file="LSO-N3", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N4={ - file="LSO-N4", - suffix="ogg", - loud=false, - subtitle="", - duration=0.39, - }, - N5={ - file="LSO-N5", - suffix="ogg", - loud=false, - subtitle="", - duration=0.39, - }, - N6={ - file="LSO-N6", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N7={ - file="LSO-N7", - suffix="ogg", - loud=false, - subtitle="", - duration=0.40, - }, - N8={ - file="LSO-N8", - suffix="ogg", - loud=false, - subtitle="", - duration=0.37, - }, - N9={ - file="LSO-N9", - suffix="ogg", - loud=false, - 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, - }, -} + +--- Message for player. +-- @type AIRBOSS.PlayerMessage +-- @field string message Message text. +-- @field #string sender Sender of the message. +-- @field #string receiver Receiver of the message. +-- @field #number duration +-- @field #boolean clear --- Difficulty level. -- @type AIRBOSS.Difficulty @@ -1722,6 +1353,7 @@ AIRBOSS.Difficulty={ -- @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. +-- @field #table messages Table of messages. -- @extends #AIRBOSS.FlightGroup --- Main group level radio menu: F10 Other/Airboss. @@ -1734,7 +1366,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.7" +AIRBOSS.version="0.9.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -2063,22 +1695,10 @@ function AIRBOSS:New(carriername, alias) SCHEDULER:New(nil, flareme, {}, 1, 3, nil, 180) 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.loud=1 - --self.radiocall[_name]=sound - end - ]] - -- Debug: if false then local text="Playing default sound files:" - for _name,_call in pairs(AIRBOSS.LSOCall) do + for _name,_call in pairs(self.LSOCall) do local call=_call --#AIRBOSS.RadioCall -- Debug text. @@ -3151,7 +2771,7 @@ function AIRBOSS:_CheckAIStatus() 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) + self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, false, 0) -- Pilot: "405, Hornet Ball, 3.2" -- TODO: Voice over. @@ -3162,7 +2782,7 @@ function AIRBOSS:_CheckAIStatus() 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, 6) + self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, false, 6) -- Flight element called the ball. element.ballcall=true @@ -3869,6 +3489,411 @@ function AIRBOSS:_InitTarawa() end + +--- Init voice over radio transmission call. +-- @param #AIRBOSS self +function AIRBOSS:_InitVoiceOvers() + + self.LSOCall={ + RADIOCHECK={ + file="LSO-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Paddles, radio check", + duration=1.1, + subduration=5, + }, + RIGHTFORLINEUP={ + file="LSO-RightForLineup", + suffix="ogg", + loud=true, + subtitle="Right for line up", + duration=0.80, + subduration=1, + }, + COMELEFT={ + file="LSO-ComeLeft", + suffix="ogg", + loud=true, + subtitle="Come left", + duration=0.60, + subduration=1, + }, + HIGH={ + file="LSO-High", + suffix="ogg", + loud=true, + subtitle="You're high", + duration=0.65, + subduration=1, + }, + LOW={ + file="LSO-Low", + suffix="ogg", + loud=true, + subtitle="You're low", + duration=0.50, + subduration=1, + }, + POWER={ + file="LSO-Power", + suffix="ogg", + loud=true, + subtitle="Power", + duration=0.50, --0.45 was too short + subduration=1, + }, + SLOW={ + file="LSO-Slow", + suffix="ogg", + loud=true, + subtitle="You're slow", + duration=0.65, + subduration=1, + }, + FAST={ + file="LSO-Fast", + suffix="ogg", + loud=true, + subtitle="You're fast", + duration=0.7, + subduration=1, + }, + CALLTHEBALL={ + file="LSO-CallTheBall", + suffix="ogg", + loud=false, + subtitle="Call the ball", + duration=0.6, + subduration=2, + }, + ROGERBALL={ + file="LSO-RogerBall", + suffix="ogg", + loud=false, + subtitle="Roger ball", + duration=0.7, + subduration=2, + }, + WAVEOFF={ + file="LSO-WaveOff", + suffix="ogg", + loud=false, + subtitle="Wave off", + duration=0.6, + subduration=5, + }, + BOLTER={ + file="LSO-BolterBolter", + suffix="ogg", + loud=false, + subtitle="Bolter, Bolter", + duration=0.75, + subduration=5, + }, + LONGINGROOVE={ + file="LSO-LongInTheGroove", + suffix="ogg", + loud=false, + subtitle="You're long in the groove", + duration=1.2, + subduration=5, + }, + FOULDECK={ + file="LSO-FoulDeck", + suffix="ogg", + loud=false, + subtitle="Foul deck", + duration=0.62, + subduration=5, + }, + DEPARTANDREENTER={ + file="LSO-DepartAndReenter", + suffix="ogg", + loud=false, + subtitle="Depart and re-enter", + duration=1.1, + subduration=5, + }, + PADDLESCONTACT={ + file="LSO-PaddlesContact", + suffix="ogg", + loud=false, + subtitle="Paddles, contact", + duration=1.0, + subduration=5, + }, + WELCOMEABOARD={ + file="LSO-WelcomeAboard", + suffix="ogg", + loud=false, + subtitle="Welcome aboard", + duration=1.0, + subduration=5, + }, + EXPECTHEAVYWAVEOFF={ + file="LSO-ExpectHeavyWaveoff", + suffix="ogg", + loud=false, + subtitle="Expect heavy waveoff", + duration=1.2, + subduration=5, + }, + EXPECTSPOT75={ + file="LSO-ExpectSpot75", + suffix="ogg", + loud=false, + subtitle="Expect spot 7.5", + duration=2.0, + subduration=5, + }, + CLEAREDTOLAND={ + file="LSO-ClearedToLand", + suffix="ogg", + loud=false, + subtitle="Cleared to land", + duration=1.0, + subduration=5, + }, + CHECK={ + file="LSO-Check", + suffix="ogg", + loud=false, + subtitle="Check", + duration=0.45, + subduration=2.5, + }, + STABILIZED={ + file="LSO-Stabilized", + suffix="ogg", + loud=false, + subtitle="Stabilized", + duration=0.9, + subduration=5, + }, + IDLE={ + file="LSO-Idle", + suffix="ogg", + loud=false, + subtitle="Idle", + duration=0.45, + subduration=5, + }, + N0={ + file="LSO-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="LSO-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="LSO-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="LSO-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="LSO-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="LSO-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N6={ + file="LSO-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="LSO-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="LSO-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="LSO-N9", + suffix="ogg", + loud=false, + subtitle="", + 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, + }, + } + + self.MarshalCall={ + RADIOCHECK={ + file="MARSHAL-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Radio check", + duration=1.1, + subduration=5, + }, + SAYNEEDLES={ + file="MARSHAL-SayNeedles", + suffix="ogg", + loud=false, + subtitle="Say needles", + duration=0.9, + subduration=5, + }, + FLYNEEDLES={ + file="MARSHAL-FlyYourNeedles", + suffix="ogg", + loud=false, + subtitle="Fly your needles", + duration=0.9, + subduration=5, + }, + -- TODO: Other voice overs for marshal. + N0={ + file="LSO-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="LSO-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="LSO-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="LSO-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="LSO-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="LSO-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N6={ + file="LSO-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="LSO-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="LSO-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="LSO-N9", + suffix="ogg", + loud=false, + 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, + }, + } + +end + +--- Init voice over radio transmission call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall radiocall LSO or Marshal radio call object. +-- @param #number duration Duration of the voice over in seconds. +-- @param #string subtitle (Optional) Subtitle to be displayed along with voice over. +-- @param #number subduration (Optional) Duration how long the subtitle is displayed. +-- @param #string filename (Optional) Name of the voice over sound file. +-- @param #string suffix (Optional) Extention of file. Default ".ogg". +function AIRBOSS:SetVoiceOver(radiocall, duration, subtitle, subduration, filename, suffix) + radiocall.duration=duration + radiocall.subtitle=subtitle or radiocall.subtitle + radiocall.file=filename + radiocall.suffix=".ogg" +end + --- Get optimal aircraft AoA parameters.. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -3961,14 +3986,6 @@ function AIRBOSS:_AoAUnit2Deg(playerData, aoaunits) -- A-4E-C source code suggests a imple factor of 1/2 for conversion. degrees=0.5*aoaunits - --[[ - -- 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 @@ -4006,17 +4023,6 @@ function AIRBOSS:_AoADeg2Units(playerData, degrees) -- A-4E source code suggests a simple factor of two as conversion. aoaunits=2*degrees - - --[[ - -- 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 - ]] end @@ -5639,6 +5645,9 @@ function AIRBOSS:_NewPlayer(unitname) -- Number of passes done by player in this slot. playerData.passes=0 --playerData.passes or 0 + -- Messages for player. + playerData.messages={} + -- Debriefing tables. playerData.lastdebrief=playerData.lastdebrief or {} @@ -6223,10 +6232,11 @@ function AIRBOSS:_CheckPlayerStatus() -- Player unit. local unit=playerData.unit - -- Check if unit is alive and in air. - if unit:IsAlive() then + -- Check if unit is alive. + if unit and unit:IsAlive() then -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). + -- TODO: This might cause problems if the CCA is set to be very small! if unit:IsInZone(self.zoneCCA) then -- Display aircraft attitude and other parameters as message text. @@ -6658,7 +6668,7 @@ function AIRBOSS:OnEventLand(EventData) if self.carriertype==AIRBOSS.CarrierType.TARAWA then -- Power "Idle". - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.IDLE, false, 1) + self:RadioTransmission(self.LSORadio, self.LSOCall.IDLE, false, 1) -- Next step debrief. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) @@ -7322,27 +7332,9 @@ function AIRBOSS:_Platform(playerData) -- Check if we are in zone. if inzone then - -- Debug message. - MESSAGE:New("Platform step reached", 5, "DEBUG"):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 - - -- Altitude and speed hint. - local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) - - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end - + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + -- Next step: depends. local nextstep if math.abs(self.holdingoffset)>0 and playerData.case>1 then @@ -7377,21 +7369,16 @@ function AIRBOSS:_ArcInTurn(playerData) local inzone=playerData.unit:IsInZone(self:_GetZoneArcIn(playerData.case)) if inzone then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) - -- Debug message. - MESSAGE:New("Arc Turn In step reached", 5, "DEBUG"):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 - -- Hint speed. - local hint=string.format("%s\n%s", playerData.step, hintSpeed) + -- Hint + -- TODO Add as special case to playerhint function + local hint="" -- Hint turn and set TACAN. if playerData.difficulty==AIRBOSS.Difficulty.EASY then @@ -7425,23 +7412,10 @@ function AIRBOSS:_ArcOutTurn(playerData) -- 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, "DEBUG"):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 + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) -- Next step: local nextstep @@ -7470,25 +7444,16 @@ function AIRBOSS:_DirtyUp(playerData) local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) if inzone then - - -- Debug message. - MESSAGE:New("Dirty up step reached", 5, "DEBUG"):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) + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(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) + -- TODO Add to playerhint function as special case. + local hint="" -- Dirty up. if playerData.difficulty==AIRBOSS.Difficulty.EASY then @@ -7504,8 +7469,8 @@ function AIRBOSS:_DirtyUp(playerData) -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then - local callsay=self:_NewRadioCall(AIRBOSS.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard) - local callfly=self:_NewRadioCall(AIRBOSS.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard) + local callsay=self:_NewRadioCall(self.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard) + local callfly=self:_NewRadioCall(self.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard) self:RadioTransmission(self.MarshalRadio, callsay, false, 40) self:RadioTransmission(self.MarshalRadio, callfly, false, 45) end @@ -7536,24 +7501,13 @@ function AIRBOSS:_Bullseye(playerData) -- 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) - - -- 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) + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) -- Message to player. + -- TODO: Add to playerhint as special case. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - - -- Hint alt and aoa. - local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) - + local hint="" -- Hint follow the needles. if playerData.difficulty==AIRBOSS.Difficulty.EASY then if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then @@ -7566,7 +7520,7 @@ function AIRBOSS:_Bullseye(playerData) -- LSO expect spot 7.5 call if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.EXPECTSPOT75) + self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75) end -- Next step: Groove Call the ball. @@ -7607,6 +7561,92 @@ function AIRBOSS:_BolterPattern(playerData) end end +--- Display hint to player. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number delay Delay before playing sound messages. Default 0 sec. +function AIRBOSS:_PlayerHint(playerData, delay) + + -- No hint for the pros. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + return + end + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt,debriefAlt,callAlt=self:_AltitudeCheck(playerData, alt) + + -- Get speed hint. + local hintSpeed,debriefSpeed,callSpeed=self:_SpeedCheck(playerData, speed) + + -- Get AoA hint. + local hintAoA,debriefAoA,callAoA=self:_AoACheck(playerData, aoa) + + -- Get distance to the boat hint. + local hintDist,debriefDist,callDist=self:_DistCheck(playerData, dist) + + -- Message to player. + local hint="" + if hintAlt then + hint=hint..hintAlt.."\n" + end + if hintSpeed then + hint=hint..hintSpeed.."\n" + end + if hintAoA then + hint=hint..hintAoA.."\n" + end + if hintDist then + hint=hint..hintDist.."\n" + end + + -- Debriefing text. + local debrief="" + if debriefAlt then + debrief=debrief.."\n-"..debriefAlt + end + if debriefSpeed then + debrief=debrief.."\n-"..debriefSpeed + end + if debriefAoA then + debrief=debrief.."\n-"..debriefAoA + end + if debriefDist then + debrief=debrief.."\n-"..debriefDist + end + + -- Add step to debriefing. + if debrief~="" then + self:_AddToDebrief(playerData, debrief) + end + + delay=delay or 0 + if callAlt then + self:Sound2Player(playerData, self.LSORadio, callAlt, false, delay) + delay=delay+callAlt.duration+0.5 + end + if callSpeed then + self:Sound2Player(playerData, self.LSORadio, callSpeed, false, delay) + delay=delay+callSpeed.duration+0.5 + end + if callAoA then + self:Sound2Player(playerData, self.LSORadio, callAoA, false, delay) + delay=delay+callAoA.duration+0.5 + end + if callDist then + self:Sound2Player(playerData, self.LSORadio, callDist, false, delay) + delay=delay+callDist.duration+0.5 + end + + -- Message to player. + if hint~="" then + local text=string.format("%s%s", playerData.step, hint) + self:MessageToPlayer(playerData, hint, "AIRBOSS", "") + end +end + --- Break entry for case I/II recoveries. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -7624,21 +7664,9 @@ function AIRBOSS:_BreakEntry(playerData) -- Check if we are in front of the boat (diffX > 0). if self:_CheckLimits(X, Z, self.BreakEntry) then - -- Get optimal altitude, distance and speed. - local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) - -- Get altitude hint. - local hintAlt=self:_AltitudeCheck(playerData, alt) - - -- 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\n%s", playerData.step, hintAlt, hintSpeed) - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end - -- Next step: Early Break. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.EARLYBREAK) @@ -7670,32 +7698,8 @@ function AIRBOSS:_Break(playerData, part) -- 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 - - -- 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 - if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then - hint=hint.."\nDirty up! Gear down, flaps down. Check hook down." - end - end - ]] - - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end - - -- Debrief - self:_AddToDebrief(playerData, debrief) + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) -- Next step: Late Break or Abeam. local nextstep @@ -7729,8 +7733,8 @@ function AIRBOSS:_CheckForLongDownwind(playerData) 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") - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.DEPARTANDREENTER) + self:RadioTransmission(self.LSORadio, self.LSOCall.DEPARTANDREENTER) playerData.patternwo=true -- Debrief. self:_AddToDebrief(playerData, "Overshoot at wake - Pattern Waveoff!") @@ -7877,28 +7842,10 @@ 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. - 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) - + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + -- Next step: Final. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.FINAL) @@ -7931,25 +7878,9 @@ function AIRBOSS:_Final(playerData) -- Check if player is in +-4 deg cone and flying towards the runway. if math.abs(lineup)<=4 then - -- Get optimal altitude, distance and speed. - local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(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 @@ -8036,13 +7967,13 @@ 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, self.LSOCall.CALLTHEBALL) playerData.Tlso=timer.getTime() -- Pilot "405, Hornet Ball, 3.2". Output should come from pilot. -- LSO "Roger ball" call in three seconds. - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 3) + self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, false, 3) -- Store data. playerData.groove.XX=groovedata @@ -8120,7 +8051,7 @@ function AIRBOSS:_Groove(playerData) if playerData.unit:IsInZone(ZoneALS) and stable then -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.CLEAREDTOLAND) + self:RadioTransmission(self.LSORadio, self.LSOCall.CLEAREDTOLAND) -- Next step: Level cross. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_LC) @@ -8148,7 +8079,7 @@ function AIRBOSS:_Groove(playerData) -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. if playerData.unit:IsInZone(ZoneLS) and stable and playerData.warning==false then - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.STABILIZED) + self:RadioTransmission(self.LSORadio, self.LSOCall.STABILIZED) playerData.warning=true end @@ -8173,7 +8104,7 @@ function AIRBOSS:_Groove(playerData) self:T3(self.lid..string.format("Waveoff distance rho=%.1f m", rho)) -- LSO Wave off! - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WAVEOFF) + self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF) playerData.Tlso=timer.getTime() -- Player was waved off! @@ -8458,8 +8389,8 @@ function AIRBOSS:_CheckFoulDeck(playerData) 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.2) + self:RadioTransmission(self.LSORadio, self.LSOCall.FOULDECK, false, 1) + self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, false, 1.2) -- Player hint for flight students. if playerData.difficulty~=AIRBOSS.Difficulty.HARD then @@ -8638,7 +8569,7 @@ function AIRBOSS:_Trapped(playerData) -- Check if we passed all wires. if wire>4 and v>10 and not playerData.warning then -- Looks like we missed the wires ==> Bolter! - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.BOLTER) + self:RadioTransmission(self.LSORadio, self.LSOCall.BOLTER) playerData.warning=true end @@ -9834,20 +9765,20 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) --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 + self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, true) + advice=advice+self.LSOCall.HIGH.duration elseif glideslopeError>0.8 then -- "You're high." - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.HIGH, false) - advice=advice+AIRBOSS.LSOCall.HIGH.duration + self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, false) + advice=advice+self.LSOCall.HIGH.duration elseif glideslopeError<-0.9 then -- "Power!" - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, true) - advice=advice+AIRBOSS.LSOCall.POWER.duration + self:RadioTransmission(self.LSORadio, self.LSOCall.POWER, true) + advice=advice+self.LSOCall.POWER.duration elseif glideslopeError<-0.6 then -- "Power." - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, false) - advice=advice+AIRBOSS.LSOCall.POWER.duration + self:RadioTransmission(self.LSORadio, self.LSOCall.POWER, false) + advice=advice+self.LSOCall.POWER.duration else -- "Good altitude." end @@ -9857,20 +9788,20 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- TODO: introduce LUE enumerator values. if lineupError<-3 then -- "Come left!" - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.COMELEFT, true) - advice=advice+AIRBOSS.LSOCall.COMELEFT.duration + self:RadioTransmission(self.LSORadio, self.LSOCall.COMELEFT, true) + advice=advice+self.LSOCall.COMELEFT.duration elseif lineupError<-1 then -- "Come left." - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.COMELEFT, false) - advice=advice+AIRBOSS.LSOCall.COMELEFT.duration + self:RadioTransmission(self.LSORadio, self.LSOCall.COMELEFT, false) + advice=advice+self.LSOCall.COMELEFT.duration elseif lineupError>3 then -- "Right for lineup!" - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RIGHTFORLINEUP, true) - advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration + self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, true) + advice=advice+self.LSOCall.RIGHTFORLINEUP.duration elseif lineupError>1 then -- "Right for lineup." - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RIGHTFORLINEUP, false) - advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration + self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, false) + advice=advice+self.LSOCall.RIGHTFORLINEUP.duration else -- "Good lineup." end @@ -9885,26 +9816,26 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then if AOA>acaoa.SLOW then -- "Your're slow!" - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.SLOW, true) - advice=advice+AIRBOSS.LSOCall.SLOW.duration + self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, true) + advice=advice+self.LSOCall.SLOW.duration --S=underline("SLO") elseif AOA>acaoa.Slow then -- "Your're slow." - self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.SLOW, false) - advice=advice+AIRBOSS.LSOCall.SLOW.duration + self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, false) + advice=advice+self.LSOCall.SLOW.duration --S="SLO" elseif AOA>acaoa.OnSpeedMax then -- No call. --S=little("SLO") elseif AOAbadscore then --hint=string.format("You're high.") - radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.HIGH, nil, nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.HIGH, nil, nil, 5) loud=true elseif _error>lowscore then --hint= string.format("You're slightly high.") - radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.HIGH, nil, nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.HIGH, nil, nil, 5) elseif _error<-badscore then --hint=string.format("You're low. ") - radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.LOW, nil, nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.LOW, nil, nil, 5) loud=true elseif _error<-lowscore then --hint=string.format("You're slightly low.") - radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.LOW, nil, nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.LOW, nil, nil, 5) else hint=string.format("Good altitude.") end @@ -10464,11 +10396,9 @@ 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="" @@ -10477,7 +10407,7 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) -- Debrief text. local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(altopt)) - return hint, debrief + return hint, debrief,radiocall end --- Evaluate player's distance to the boat at checkpoint. @@ -10486,6 +10416,7 @@ end -- @param #number optdist Optimal distance in meters. -- @return #string Feedback message text. -- @return #string Debriefing text. +-- @#AIRBOSS.RadioCall Distance radio call. Not implemented yet. function AIRBOSS:_DistanceCheck(playerData, optdist) if optdist==nil then @@ -10528,7 +10459,7 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) -- Debriefing text. local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(optdist)) - return hint, debrief + return hint, debrief, nil end --- Score for correct AoA. @@ -10537,6 +10468,7 @@ end -- @param #number optaoa Optimal AoA. -- @return #string Feedback message text or easy and normal difficulty level or nil for hard. -- @return #string Debriefing text. +-- @return #AIRBOSS.RadioCall Radio call. function AIRBOSS:_AoACheck(playerData, optaoa) if optaoa==nil then @@ -10563,11 +10495,11 @@ function AIRBOSS:_AoACheck(playerData, optaoa) local loud=false if aoa>=aircraftaoa.SLOW then --hint="Your're slow!" - radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.SLOW, nil, nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, nil, nil, 5) loud=true elseif aoa>=aircraftaoa.Slow then --hint="Your're slow." - radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.SLOW, nil, nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, nil, nil, 5) elseif aoa>=aircraftaoa.OnSpeedMax then hint="Your're a little slow." elseif aoa>=aircraftaoa.OnSpeedMin then @@ -10576,10 +10508,10 @@ function AIRBOSS:_AoACheck(playerData, optaoa) hint="You're a little fast." elseif aoa>=aircraftaoa.FAST then --hint="Your're fast." - radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.FAST, nil, nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.FAST, nil, nil, 5) else --hint="You're fast!" - radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.FAST, nil, nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.FAST, nil, nil, 5) loud=true end @@ -10599,7 +10531,7 @@ function AIRBOSS:_AoACheck(playerData, optaoa) -- Debriefing text. local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.", self:_AoADeg2Units(playerData, aoa), _error, self:_AoADeg2Units(playerData, optaoa)) - return hint, debrief + return hint, debrief,radiocall end --- Evaluate player's speed. @@ -10608,6 +10540,7 @@ end -- @param #number speedopt Optimal speed in m/s. -- @return #string Feedback text. -- @return #string Debriefing text. +-- @return #AIRBOSS.RadioCall Radio call. function AIRBOSS:_SpeedCheck(playerData, speedopt) if speedopt==nil then @@ -10630,17 +10563,17 @@ function AIRBOSS:_SpeedCheck(playerData, speedopt) local loud=false if _error>badscore then --hint=string.format("You're fast.") - radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.FAST, "AIRBOSS", nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "AIRBOSS", nil, 5) loud=true elseif _error>lowscore then --hint= string.format("You're slightly fast.") - radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.FAST, "AIRBOSS", nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "AIRBOSS", nil, 5) elseif _error<-badscore then --hint=string.format("You're slow.") - radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.SLOW, "AIRBOSS", nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "AIRBOSS", nil, 5) elseif _error<-lowscore then --hint=string.format("You're slightly slow.") - radiocall=self:_NewRadioCall(AIRBOSS.LSOCall.SLOW, "AIRBOSS", nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "AIRBOSS", nil, 5) loud=true else hint=string.format("Good speed.") @@ -10661,7 +10594,7 @@ function AIRBOSS:_SpeedCheck(playerData, speedopt) -- Debrief text. local debrief=string.format("Speed %d knots = %d%% deviation from %d knots.", UTILS.MpsToKnots(speed), _error, UTILS.MpsToKnots(speedopt)) - return hint, debrief + return hint, debrief, radiocall end --- Append text to debriefing. @@ -10846,7 +10779,7 @@ function AIRBOSS:_Debrief(playerData) else -- Welcome aboard! - self:Sound2Player(playerData, self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) + self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) -- Airboss talkto! local text=string.format("the deck was fouled but landed anyway. Airboss wants to talk to you!") @@ -10869,7 +10802,7 @@ function AIRBOSS:_Debrief(playerData) else -- Welcome aboard! - self:Sound2Player(playerData, self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) + self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) -- Airboss talkto! local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") @@ -10899,7 +10832,7 @@ function AIRBOSS:_Debrief(playerData) if not playerData.unit:InAir() then -- Welcome aboard! - self:Sound2Player(playerData, self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) + self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) end @@ -12108,7 +12041,7 @@ end -- @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 + if call.file==self.MarshalCall.NOISE.file or call.file==self.LSOCall.NOISE.file then return true else return false @@ -12344,8 +12277,7 @@ 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:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) +function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay) if playerData and message and message~="" then @@ -12366,10 +12298,10 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration if delay and delay>0 then -- Delayed call. - SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear, 0, soundoff}, delay) + SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) else - if receiver==playerData.onboard and not soundoff then + if receiver==playerData.onboard then -- Sound only to player group. if sender and (sender=="LSO" or sender=="MARSHAL" or sender=="AIRBOSS") then @@ -12378,7 +12310,7 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration local wait=self:_Number2Sound(playerData, sender, receiver) -- Play click sound to end message. - local filename=self:_RadioFilename(AIRBOSS.MarshalCall.CLICK) + local filename=self:_RadioFilename(self.MarshalCall.CLICK) USERSOUND:New(filename):ToGroup(playerData.group, wait) end @@ -12387,7 +12319,7 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration -- Text message to player client. if playerData.client then MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) - end + end end @@ -12407,7 +12339,7 @@ end function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay) -- Create new (fake) radio call to show the subtitile. - local call=self:_NewRadioCall(AIRBOSS.LSOCall.NOISE, sender or "LSO", message, duration, receiver, sender) + local call=self:_NewRadioCall(self.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) @@ -12426,7 +12358,7 @@ end function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay) -- Create new (fake) radio call to show the subtitile. - local call=self:_NewRadioCall(AIRBOSS.MarshalCall.NOISE, sender or "MARSHAL", message, duration, receiver, sender) + local call=self:_NewRadioCall(self.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) @@ -12934,19 +12866,7 @@ function AIRBOSS:_RequestSpinning(_unitName) end if playerData.difficulty==AIRBOSS.Difficulty.EASY then text=text.." Climb to 1200 feet and proceed to the initial." - end - - -- Set step for section members. - --[[ - for _,sec in pairs(playerData.section) do - local sectionmember=sec --#AIRBOSS.PlayerData - self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.SPINNING) - if sectionmember.difficulty~=AIRBOSS.Difficulty.HARD then - text="Spin it!" - self:MessageToPlayer(playerData, text, "MARSHAL") - end end - ]] end @@ -14196,7 +14116,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, self.LSOCall.RADIOCHECK) end end end @@ -14215,7 +14135,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.MarshalRadio, AIRBOSS.MarshalCall.RADIOCHECK) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RADIOCHECK) end end end From aba0c19215b69f03d5580ff8dc575f39738bf2e6 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 13 Feb 2019 00:15:31 +0100 Subject: [PATCH 164/485] AB v0.9.8wip --- Moose Development/Moose/Ops/Airboss.lua | 143 +++++++++++------------- 1 file changed, 66 insertions(+), 77 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index f7446f23d..a94006562 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -331,7 +331,7 @@ -- -- ### 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. +-- If a recovery tanker 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 itself at its home base. Once the re-fueling is complete, the player has to re-register to the marshal stack. -- -- ### Spinning @@ -1587,6 +1587,9 @@ function AIRBOSS:New(carriername, alias) return nil end + -- Init voice over files. + self:_InitVoiceOvers() + ------------------- -- Debug Section -- ------------------- @@ -7372,28 +7375,6 @@ function AIRBOSS:_ArcInTurn(playerData) -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) - - -- Message to player. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - - -- Hint - -- TODO Add as special case to playerhint function - local hint="" - - -- 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) - local turn="right" - if self.holdingoffset<0 then - turn="left" - end - hint=hint..string.format("\nTurn %s and select TACAN %03d°.", turn, radial) - end - - -- Message to player. - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end -- Next step: Arc Out Turn. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.ARCOUT) @@ -7447,25 +7428,6 @@ function AIRBOSS:_DirtyUp(playerData) -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) - - -- Message to player. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - - -- Hint alt and speed. - -- TODO Add to playerhint function as special case. - local hint="" - - -- Dirty up. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - hint=hint.."\nFAF! Checks completed. Nozzles 50°." - else - hint=hint.."\nDirty up! Hook, gear and flaps down." - end - end - - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then @@ -7504,20 +7466,6 @@ function AIRBOSS:_Bullseye(playerData) -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) - -- Message to player. - -- TODO: Add to playerhint as special case. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then - local hint="" - -- Hint follow the needles. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then - hint=hint..string.format("Intercept glideslope and follow the needles.") - end - end - - self:MessageToPlayer(playerData, hint, "MARSHAL", "") - end - -- LSO expect spot 7.5 call if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75) @@ -7585,21 +7533,21 @@ function AIRBOSS:_PlayerHint(playerData, delay) local hintAoA,debriefAoA,callAoA=self:_AoACheck(playerData, aoa) -- Get distance to the boat hint. - local hintDist,debriefDist,callDist=self:_DistCheck(playerData, dist) + local hintDist,debriefDist,callDist=self:_DistanceCheck(playerData, dist) -- Message to player. local hint="" if hintAlt then - hint=hint..hintAlt.."\n" + hint=hint.."\n"..hintAlt end if hintSpeed then - hint=hint..hintSpeed.."\n" + hint=hint.."\n"..hintSpeed end if hintAoA then - hint=hint..hintAoA.."\n" + hint=hint.."\n"..hintAoA end if hintDist then - hint=hint..hintDist.."\n" + hint=hint.."\n"..hintDist end -- Debriefing text. @@ -7640,6 +7588,43 @@ function AIRBOSS:_PlayerHint(playerData, delay) delay=delay+callDist.duration+0.5 end + -- ARC IN info. + if playerData.step==AIRBOSS.PatternStep.ARCIN then + + -- 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) + local turn="right" + if self.holdingoffset<0 then + turn="left" + end + hint=hint..string.format("\nTurn %s and select TACAN %03d°.", turn, radial) + end + + end + + -- DIRTUP additonal info. + if playerData.step==AIRBOSS.PatternStep.DIRTYUP then + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + hint=hint.."\nFAF! Checks completed. Nozzles 50°." + else + hint=hint.."\nDirty up! Hook, gear and flaps down." + end + end + end + + -- BULLSEYE additonal info. + if playerData.step==AIRBOSS.PatternStep.BULLSEYE then + -- Hint follow the needles. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then + hint=hint..string.format("\nIntercept glideslope and follow the needles.") + end + end + end + -- Message to player. if hint~="" then local text=string.format("%s%s", playerData.step, hint) @@ -7776,7 +7761,7 @@ function AIRBOSS:_Abeam(playerData) end -- Hint for player about altitude, AoA etc. - self:_PlayerHint(playerData) + self:_PlayerHint(playerData, 3) -- Next step: ninety. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.NINETY) @@ -8398,10 +8383,14 @@ function AIRBOSS:_CheckFoulDeck(playerData) self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) end - -- Set player parameters for foul deck + -- Set player parameters for foul deck. playerData.fouldeckwo=true + + -- Debrief. playerData.step=AIRBOSS.PatternStep.DEBRIEF playerData.warning=nil + + -- Pass would be invalid if the player lands. playerData.valid=false -- Send a message to the player that blocks the runway. @@ -12019,18 +12008,18 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) 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) + if call~=self[caller].CLICK and + call~=self[caller].N0 and + call~=self[caller].N1 and + call~=self[caller].N2 and + call~=self[caller].N3 and + call~=self[caller].N4 and + call~=self[caller].N5 and + call~=self[caller].N6 and + call~=self[caller].N7 and + call~=self[caller].N8 and + call~=self[caller].N9 then + self:RadioTransmission(radio, self[caller].CLICK, false, delay) end end @@ -12497,7 +12486,7 @@ function AIRBOSS:_Number2Sound(playerData, sender, number, delay) local N=string.format("N%s", n) -- Radio call. - local call=AIRBOSS[Sender][N] --#AIRBOSS.RadioCall + local call=self[Sender][N] --#AIRBOSS.RadioCall -- Create file name. --local filename=string.format("%s.%s", call.file, call.suffix) @@ -12555,7 +12544,7 @@ function AIRBOSS:_Number2Radio(radio, number, delay) local N=string.format("N%s", n) -- Radio call. - local call=AIRBOSS[Sender][N] --#AIRBOSS.RadioCall + local call=self[Sender][N] --#AIRBOSS.RadioCall -- Transmit. self:RadioTransmission(radio, call, false, delay) From b5b6d398aef741cbe8f023e55e7c86dd6ab7666a Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 13 Feb 2019 16:46:15 +0100 Subject: [PATCH 165/485] AB 0.9.8w --- Moose Development/Moose/Functional/RAT.lua | 21 +++- Moose Development/Moose/Ops/Airboss.lua | 106 +++++++++++++++++---- 2 files changed, 106 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index dc5563546..5a3d9919a 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -150,6 +150,7 @@ -- @field #number parkingscanradius Radius in meters until which parking spots are scanned for obstacles like other units, statics or scenery. -- @field #boolean parkingscanscenery If true, area around parking spots is scanned for scenery objects. Default is false. -- @field #boolean parkingverysafe If true, parking spots are considered as non-free until a possible aircraft has left and taken off. Default false. +-- @field #boolean despawnair If true, aircraft are despawned when they reach their destination zone. Default. -- @extends Core.Spawn#SPAWN --- Implements an easy to use way to randomly fill your map with AI aircraft. @@ -428,6 +429,7 @@ RAT={ parkingscanradius=40, -- Scan radius. parkingscanscenery=false, -- Scan parking spots for scenery obstacles. parkingverysafe=false, -- Very safe option. + despawnair=true, } ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -546,7 +548,7 @@ RAT.id="RAT | " --- RAT version. -- @list version RAT.version={ - version = "2.3.5", + version = "2.3.6", print = true, } @@ -1098,6 +1100,14 @@ function RAT:SetParkingSpotSafeOFF() return self end +--- Aircraft that reach their destination zone are not despawned. They will probably go the the nearest airbase and try to land. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetDespawnAirOFF() + self.despawnair=false + return self +end + --- Set takeoff type. Starting cold at airport, starting hot at airport, starting at runway, starting in the air. -- Default is "takeoff-coldorhot". So there is a 50% chance that the aircraft starts with cold engines and 50% that it starts with hot engines. -- @param #RAT self @@ -3604,13 +3614,18 @@ function RAT:Status(message, forID) local text=string.format("Flight %s will be despawned NOW!", self.alias) self:T(RAT.id..text) - -- Despawn old group. + + -- Respawn group if (not self.norespawn) and (not self.respawn_after_takeoff) then local idx=self:GetSpawnIndexFromGroup(group) local coord=group:GetCoordinate() self:_Respawn(idx, coord, 0) end - self:_Despawn(group, 0) + + -- Despawn old group. + if self.despawnair then + self:_Despawn(group, 0) + end end diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index a94006562..fde40b486 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1206,7 +1206,7 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall NOISE Static noise sound. --- Marshal radio calls. --- @type AIRBOSS.MarshalCall +-- @type AIRBOSS.MarshalCalls -- @field #AIRBOSS.RadioCall RADIOCHECK "Radio check" call. -- @field #AIRBOSS.RadioCall SAYNEEDLES "Say needles" call. -- @field #AIRBOSS.RadioCall FLYNEEDLES "Fly your needles" call. @@ -1222,6 +1222,15 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall N9 "Nine" call. -- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. -- @field #AIRBOSS.RadioCall NOISE Static noise sound. +-- @field #AIRBOSS.RadioCall CASE "Case" call. +-- @field #AIRBOSS.RadioCall EXPECTED "Expected" call. +-- @field #AIRBOSS.RadioCall BRC "BRC" call. +-- @field #AIRBOSS.RadioCall CHARLIETIME "Charlie Time" call. +-- @field #AIRBOSS.RadioCall REPORTSEEME "Report see me" call. +-- @field #AIRBOSS.RadioCall HOLDAT "Hold at" call. +-- @field #AIRBOSS.RadioCall ANGELS "Angels" call. +-- @field #AIRBOSS.RadioCall ALTIMETER "Altimeter" call. +-- @field #AIRBOSS.RadioCall POINT "Point" call. --- Message for player. -- @type AIRBOSS.PlayerMessage @@ -3488,8 +3497,8 @@ function AIRBOSS:_InitTarawa() 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.5) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 - self.BreakLate.LimitZmax= nil - + self.BreakLate.LimitZmax= nil + end @@ -3793,72 +3802,71 @@ function AIRBOSS:_InitVoiceOvers() duration=0.9, subduration=5, }, - -- TODO: Other voice overs for marshal. N0={ - file="LSO-N0", + file="MARSHAL-N0", suffix="ogg", loud=false, subtitle="", duration=0.40, }, N1={ - file="LSO-N1", + file="MARSHAL-N1", suffix="ogg", loud=false, subtitle="", duration=0.25, }, N2={ - file="LSO-N2", + file="MARSHAL-N2", suffix="ogg", loud=false, subtitle="", duration=0.37, }, N3={ - file="LSO-N3", + file="MARSHAL-N3", suffix="ogg", loud=false, subtitle="", duration=0.37, }, N4={ - file="LSO-N4", + file="MARSHAL-N4", suffix="ogg", loud=false, subtitle="", duration=0.39, }, N5={ - file="LSO-N5", + file="MARSHAL-N5", suffix="ogg", loud=false, subtitle="", duration=0.39, }, N6={ - file="LSO-N6", + file="MARSHAL-N6", suffix="ogg", loud=false, subtitle="", duration=0.40, }, N7={ - file="LSO-N7", + file="MARSHAL-N7", suffix="ogg", loud=false, subtitle="", duration=0.40, }, N8={ - file="LSO-N8", + file="MARSHAL-N8", suffix="ogg", loud=false, subtitle="", duration=0.37, }, N9={ - file="LSO-N9", + file="MARSHAL-N9", suffix="ogg", loud=false, subtitle="", @@ -5146,11 +5154,14 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) local Ccharlie=UTILS.SecondsToClock(flight.Tcharlie) -- Marshal message. - 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) + --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. - self:MessageToMarshal(text, "MARSHAL", flight.onboard) + --self:MessageToMarshal(text, "MARSHAL", flight.onboard) + + -- Combined marshal call. + self:_MarshalCallArrived(flight.onboard, flight.case, brc, alt, Ccharlie, P) -- Hint about TACAN bearing. if self.TACANon and (not flight.ai) and flight.difficulty==AIRBOSS.Difficulty.EASY then @@ -5895,7 +5906,7 @@ function AIRBOSS:_CheckSectionRecovered(flight) -- Check all elements of the lead flight group. for _,_element in pairs(lead.elements) do - local element=_element --#AIROBSS.FlightElement + local element=_element --#AIRBOSS.FlightElement if not element.recovered then return false end @@ -12557,6 +12568,65 @@ function AIRBOSS:_Number2Radio(radio, number, delay) return wait end +--- Compile a radio call when Marshal tells a flight the holding alitude. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #number case Recovery case. +-- @param #number brc Base recovery course. +-- @param #number altitude Holding alitude. +-- @param #string charlie Charlie Time estimate. +-- @param #string qfe Alitmeter inHg. +function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) + + -- Split strings etc. + local angels=self:_GetAngels(altitude) + local QFE=UTILS.Split(tostring(qfe), ".") + local CT=UTILS.Split(select(1, UTILS.Split(alitmeter, "+")), ":") + + -- Subtitle text. + local text=string.format("Case %d, expected BRC %03d°, hold at angels %d. Expected Charlie Time %s.\n", case, brc, angels, charlie) + text=text..string.format("Altimeter %.2f. Report see me.", qfe) + + -- Create new call to display complete subtitle. + local casecall=self:_NewRadioCall(self.MarshalCall.CASE, "MARSHAL", text, self.Tmessage, modex) + + -- Case.. + self:RadioTransmission(self.MarshalRadio, casecall) + -- X.. + self:_Number2Radio(self.MarshalRadio, tostring(case)) + -- expected.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED) + -- BRC.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.BRC) + -- XYZ.. + self:_Number2Radio(self.MarshalRadio, string.format("%d03", brc)) + -- hold at.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOLDAT) + -- angels.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ANGELS) + -- X.. + self:_Number2Radio(self.MarshalRadio, tostring(angels)) + -- Expected.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED) + -- Charlie time.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CHARLIETIME) + -- XY.. (hours) + self:_Number2Radio(self.MarshalRadio, CT[1]) + -- XY.. (minutes) + self:_Number2Radio(self.MarshalRadio, CT[2]) + -- Altimeter.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ALTIMETER) + -- XY.. + self:_Number2Radio(self.MarshalRadio, QFE[1]) + -- Point.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.POINT) + -- XY.. + self:_Number2Radio(self.MarshalRadio, QFE[2]) + -- Report see me. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.REPORTSEEME) + +end + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- RADIO MENU Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- From cff9557217e63aa5aa2115e43fc2c2da2698f404 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 14 Feb 2019 00:53:22 +0100 Subject: [PATCH 166/485] AB v0.9.8 wip --- Moose Development/Moose/Ops/Airboss.lua | 188 +++++++++++++++++++++--- 1 file changed, 164 insertions(+), 24 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index fde40b486..20374dd40 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -203,6 +203,7 @@ -- @field #number dTbeacon Time interval to refresh the beacons. Default 5 minutes. -- @field #AIRBOSS.LSOCalls LSOCall Radio voice overs of the LSO. -- @field #AIRBOSS.MarshalCalls MarshalCall Radio voice over of the Marshal/Airboss. +-- @field #number lowfuelAI Low fuel threshold for AI groups in percent. -- @extends Core.Fsm#FSM --- Be the boss! @@ -1002,6 +1003,7 @@ AIRBOSS = { Tbeacon = nil, LSOCall = nil, MarshalCall = nil, + lowfuelAI = nil, } --- Aircraft types capable of landing on carrier (human+AI). @@ -2756,6 +2758,23 @@ end -- @param #AIRBOSS self function AIRBOSS:_CheckAIStatus() + -- Loop over all flights in Marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Only AI! + if flight.ai then + + -- TODO: Check that aircraft can be refueled. + local fuel=flight.group:GetFuelMin()*100 + + if self.lowfuelAI and fuelmath.abs(gd.LUE) then - self:T(self.lid..string.format("Got bigger Linue up error at %s: LUE %.3f>%.3f.", gs, lineupError, gd.LUE)) + self:T(self.lid..string.format("Got bigger Lineup error at %s: LUE %.3f>%.3f.", gs, lineupError, gd.LUE)) gd.LUE=lineupError end @@ -11508,6 +11607,36 @@ function AIRBOSS._ReachedHoldingZone(group, airboss, flight) end end +--- Function called when a group should be send to the Marshal stack. If stack is full, it is send to wait. +--@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._TaskFunctionMarshalAI(group, airboss, flight) + + -- Debug message. + local text=string.format("Flight %s is send to marshal.", group:GetName()) + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Get the next free stack for current recovery case. + local stack=airboss:_GetFreeStack(flight.ai) + + if stack then + + -- Send AI to marshal stack. + airboss:_MarshalAI(flight, stack) + + else + + -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available. + if not airboss:_InQueue(airboss.Qwaiting, flight.group) then + airboss:_WaitAI(flight) + end + + end + +end + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- MISC functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -11981,6 +12110,10 @@ end function AIRBOSS:RadioTransmission(radio, call, loud, delay) self:F2({radio=radio, call=call, loud=loud, delay=delay}) + if radio==nil or call==nil then + return + end + -- Create a new radio transmission item. local transmission={} --#AIRBOSS.Radioitem @@ -12577,18 +12710,24 @@ end -- @param #string charlie Charlie Time estimate. -- @param #string qfe Alitmeter inHg. function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) + self:E({modex=modex,case=case,brc=brc,altitude=altitude,charlie=charlie,qfe=qfe}) -- Split strings etc. local angels=self:_GetAngels(altitude) - local QFE=UTILS.Split(tostring(qfe), ".") - local CT=UTILS.Split(select(1, UTILS.Split(alitmeter, "+")), ":") + local QFE=UTILS.Split(tostring(UTILS.Round(qfe,2)), ".") + local clock=UTILS.Split(charlie, "+") + self:E({clock=clock}) + local CT=UTILS.Split(clock[1], ":") -- Subtitle text. local text=string.format("Case %d, expected BRC %03d°, hold at angels %d. Expected Charlie Time %s.\n", case, brc, angels, charlie) text=text..string.format("Altimeter %.2f. Report see me.", qfe) -- Create new call to display complete subtitle. - local casecall=self:_NewRadioCall(self.MarshalCall.CASE, "MARSHAL", text, self.Tmessage, modex) + --local casecall=self:_NewRadioCall(self.MarshalCall.CASE, "MARSHAL", text, self.Tmessage, modex) + + -- Until we have a case sound we take the noise for testing + local casecall=self:_NewRadioCall(self.MarshalCall.NOISE, "MARSHAL", text, self.Tmessage, modex) -- Case.. self:RadioTransmission(self.MarshalRadio, casecall) @@ -12599,7 +12738,7 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) -- BRC.. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.BRC) -- XYZ.. - self:_Number2Radio(self.MarshalRadio, string.format("%d03", brc)) + self:_Number2Radio(self.MarshalRadio, string.format("%03d", brc)) -- hold at.. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOLDAT) -- angels.. @@ -12624,7 +12763,8 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) self:_Number2Radio(self.MarshalRadio, QFE[2]) -- Report see me. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.REPORTSEEME) - + -- Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CLICK) end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- From 9df35840fe07b2b78bf3ada8fb9f5926d28d4910 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 14 Feb 2019 16:54:59 +0100 Subject: [PATCH 167/485] AB v0.9.8w --- Moose Development/Moose/Ops/Airboss.lua | 304 +++++++++++++++++++++--- 1 file changed, 265 insertions(+), 39 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 20374dd40..b61af90df 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -189,6 +189,8 @@ -- @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 #string radiorelayLSO Name of the aircraft acting as sender for broadcasting LSO radio messages from the carrier. DCS shortcoming workaround. +-- @field #string radiorelayMSH Name of the aircraft acting as sender for broadcasting Marhsal 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. @@ -204,6 +206,7 @@ -- @field #AIRBOSS.LSOCalls LSOCall Radio voice overs of the LSO. -- @field #AIRBOSS.MarshalCalls MarshalCall Radio voice over of the Marshal/Airboss. -- @field #number lowfuelAI Low fuel threshold for AI groups in percent. +-- @field #boolean emergency If true (default), allow emergency landings, i.e. bypass any pattern and go for final approach. -- @extends Core.Fsm#FSM --- Be the boss! @@ -299,13 +302,14 @@ -- -- The general structure -- --- * **F1 Help...**: Help submenu, see below. --- * **F2 Kneeboard...**: Kneeboard submenu, see below. Carrier information, weather report, player status. +-- * **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** -- * **F6 Spinning** --- * **F7 [Reset My Status]** +-- * **F7 Emergency Landing** +-- * **F8 [Reset My Status]** -- -- ### Request Marshal -- @@ -347,6 +351,12 @@ -- -- If necessary, the player can call "Spinning" again when in the above mentioned steps. -- +-- ### Emergency Landing +-- +-- Request an emergency landing, i.e. bypass all pattern steps and go directly to the final approach. +-- +-- All section members are supposed to follow. Player (or section lead) is removed from all other queues and automatically added to the landing pattern queue. +-- -- ### [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. @@ -785,7 +795,34 @@ -- -- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles/") -- --- ## Remarks +-- ## How To Use Your Own Voice Overs +-- +-- If you have a set of AIRBOSS sound files recorded or got it from elsewhere it is possible to use those instead of the default ones. +-- I recommend to use exactly the same file names as the original sound files have. +-- +-- However, the **timing is critical**! As sometimes sounds are played directly after one another, e.g. by saying the modex but also on other occations, the airboss +-- script has a radio queue implemented (actually two - one for the LSO and one for the Marshal/Airboss radio). +-- By this it is automatically taken care that played messages are not overlapping and played over each other. The disadvantage is, that the script needs to know +-- the exact duration of *each* voice over. For the default sounds this is hard coded in the source code. For your own files, you need to give that bit of information +-- to the script via the @{#AIRBOSS.SetVoiceOver}(**radiocall**, **duration**, **subtitle**, **subduration**, **filename**, **suffix**) function. Only the first two +-- parameters **radiocall** and **duration** are usually important to adjust here. +-- +-- For example, if you want to change the LSO "Call the Ball" and "Roger Ball" calls: +-- +-- airbossStennis:SetVoiceOver(airbossStennis.LSOCall.CALLTHEBALL, 0.6) +-- airbossStennis:SetVoiceOver(airbossStennis.LSOCall.ROGERBALL, 0.7) +-- +-- Again, changing the file name, subtitle, subtitle duration is not required if you name the file exactly like the original one, which is this case would be "LSO-RogerBall.ogg". +-- +-- ## Carrier Specific Voice Overs +-- +-- It is possible to use diffent sound files for different carriers. If you have set up two (or more) AIRBOSS objects at different carriers - say Stennis and Tarawa - each +-- carrier would use the files in the specified directory, e.g. +-- +-- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles Stenis/") +-- airbossTarawa:SetSoundfilesFolder("Airboss Soundfiles Tarawa/") +-- +-- ## The Radio Transmission Dilemma -- -- DCS offers two (actually three) ways to send radio messages. Each one has its advantages and disadvantages and it is important to understand the differences. -- @@ -794,8 +831,8 @@ -- *In principle*, the best way to transmit messages is via the [TransmitMessage](https://wiki.hoggitworld.com/view/DCS_command_transmitMessage) command. -- This method has the advantage that subtitles can be used and these subtitles are only displayed to the players who dialed in the same radio frequency as -- used for the transmission. --- However, this method unfortunately only works if the sending unit is an **aircraft**. There it is not usable by the AIRBOSS per se as the transmission comes from --- a naval unit (the carrier). +-- However, this method unfortunately only works if the sending unit is an **aircraft**. Therefore, it is not usable by the AIRBOSS per se as the transmission comes from +-- a naval unit (i.e. the carrier). -- -- As a workaround, you can put an aircraft, e.g. a Helicopter on the deck of the carrier or another ship of the strike group. The aircraft should be set to -- uncontrolled and maybe even to immortal. With the @{#AIRBOSS.SetRadioUnit}(*unitname*) function you can use this unit as "radio repeater". @@ -809,7 +846,7 @@ -- -- Another way to broadcast messages is via the [radio transmission trigger](https://wiki.hoggitworld.com/view/DCS_func_radioTransmission). This method can be used for all -- units (land, air, naval). However, messages cannot be subtitled. Therefore, subtitles are displayed to the players via normal textout messages. --- The disadvantage is that is is impossible to know which players have the right radio frequencies dialed in. There subtitles of the Marshal radio are displayed to all players +-- The disadvantage is that is is impossible to know which players have the right radio frequencies dialed in. Therefore, subtitles of the Marshal radio calls are displayed to all players -- inside the CCA. Subtitles on the LSO radio frequency are displayed to all players in the pattern. -- -- ### Sound to User @@ -990,6 +1027,8 @@ AIRBOSS = { windowcount = 0, LSOdT = nil, senderac = nil, + radiorelayLSO = nil, + radiorelayMSH = nil, turnintowind = nil, detour = nil, squadsetAI = nil, @@ -1004,6 +1043,7 @@ AIRBOSS = { LSOCall = nil, MarshalCall = nil, lowfuelAI = nil, + emergency = nil, } --- Aircraft types capable of landing on carrier (human+AI). @@ -1098,6 +1138,7 @@ AIRBOSS.CarrierType={ -- @field #string GROOVE_LC "Groove Level Cross". -- @field #string GROOVE_IW "Groove In the Wires". -- @field #string BOLTER "Bolter Pattern". +-- @field #string EMERGENCY "Emergency Landing". -- @field #string DEBRIEF "Debrief". AIRBOSS.PatternStep={ UNDEFINED="Undefined", @@ -1127,6 +1168,7 @@ AIRBOSS.PatternStep={ GROOVE_AL="Groove Abeam Landing Spot", GROOVE_LC="Groove Level Cross", BOLTER="Bolter Pattern", + EMERGENCY="Emergency Landing", DEBRIEF="Debrief", } @@ -1541,6 +1583,9 @@ function AIRBOSS:New(carriername, alias) -- Airboss is a nice guy. self:SetAirbossNiceGuy() + -- Allow emergency landings. + self:SetEmergencyLandings() + -- No despawn after engine shutdown by default. self:SetDespawnOnEngineShutdown(false) @@ -2158,6 +2203,20 @@ function AIRBOSS:SetAirbossNiceGuy(switch) return self end +--- Allow emergency landings, i.e. bypassing any pattern and go directly to final approach. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, emergency landings are okay. +-- @return #AIRBOSS self +function AIRBOSS:SetEmergencyLandings(switch) + if switch==true or switch==nil then + self.emergency=true + else + self.emergency=false + end + return self +end + + --- Despawn AI groups after they they shut down their engines -- @param #AIRBOSS self -- @param #boolean switch If true or nil, AI groups are despawned. @@ -2393,6 +2452,25 @@ function AIRBOSS:SetRadioUnitName(unitname) return self end +--- Set unit acting as radio relay for the LSO radio. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #AIRBOSS self +function AIRBOSS:SetRadioRelayLSO(unitname) + self.radiorelayLSO=unitname + return self +end + +--- Set unit acting as radio relay for the Marshal radio. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #AIRBOSS self +function AIRBOSS:SetRadioRelayMarshal(unitname) + self.radiorelayMarshal=unitname + 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 @@ -3071,7 +3149,7 @@ function AIRBOSS:_CheckRecoveryTimes() local v=UTILS.KnotsToMps(nextwindow.SPEED) -- Check that we do not go above max possible speed. - local vmax=self.carrier:GetSpeedMax() + local vmax=self.carrier:GetSpeedMax() v=math.min(v,vmax) -- Route carrier into the wind. Sets self.turnintowind=true @@ -3106,15 +3184,16 @@ end --@param #AIRBOSS self --@param #AIRBOSS.FlightGroup flight --@return #AIRBOSS.FlightGroup The leader of the section. Could be the flight itself. +--@return #boolean If true, flight is lead. function AIRBOSS:_GetFlightLead(flight) if flight.name~=flight.seclead then -- Section lead of flight. local lead=self.players[flight.seclead] - return lead + return lead,false else -- Flight without section or section lead. - return flight + return flight,true end end @@ -4967,7 +5046,18 @@ function AIRBOSS:_RefuelAI(flight) else + ------------------------------ + -- Guide AI to divert field -- + ------------------------------ + + -- Closest Airfield of the coaliton. local divertfield=self:GetCoordinate():GetClosestAirbase(Airbase.Category.AIRDROME, self:GetCoalition()) + + -- Coordinate. + local divertcoord=divertfield:GetCoordinate() + + -- Landing waypoint. + wp[#wp+1]=divertcoord:WaypointAirLanding(UTILS.KnotsToKmph(200), divertfield, {}, "Divert Field") end @@ -5385,7 +5475,6 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) self:T(self.lid..string.format("Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal)) -- Add flight to pattern queue. - --table.insert(self.Qpattern, flight) self:_AddFlightToPatternQueue(flight) end @@ -6049,6 +6138,11 @@ function AIRBOSS:_AddFlightToPatternQueue(flight) -- Add flight to table. table.insert(self.Qpattern, flight) + -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42). + flight.flag=-1 + -- New time stamp for time in pattern. + flight.time=timer.getAbsTime() + -- Init recovered switch. flight.recovered=false for _,elem in pairs(flight.elements) do @@ -6057,6 +6151,9 @@ function AIRBOSS:_AddFlightToPatternQueue(flight) -- Set recovered for all section members. for _,sec in pairs(flight.section) do + -- Set flag and timestamp for section members + sec.flag=-1 + sec.time=timer.getAbsTime() for _,elem in pairs(sec.elements) do elem.recoverd=false end @@ -6463,6 +6560,11 @@ function AIRBOSS:_CheckPlayerStatus() -- CASE I/II: In the wake. self:_Wake(playerData) + elseif playerData.step==AIRBOSS.PatternStep.EMERGENCY then + + -- Emergency landing. Player pos is not checked. + self:_Final(playerData, true) + elseif playerData.step==AIRBOSS.PatternStep.FINAL then -- CASE I/II: Turn to final and enter the groove. @@ -7950,15 +8052,17 @@ end --- Turn to final. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Final(playerData) - +-- @param #boolean nocheck If true, player is not checked to be in the right position. +function AIRBOSS:_Final(playerData, nocheck) -- 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.Final) then - self:_AbortPattern(playerData, X, Z, self.Final, true) - return + -- In front of carrier or more than 4 km behind carrier. + if not nocheck then + if self:_CheckAbort(X, Z, self.Final) then + self:_AbortPattern(playerData, X, Z, self.Final, true) + return + end end -- Relative heading 0=fly parallel +-90=fly perpendicular @@ -7970,8 +8074,15 @@ function AIRBOSS:_Final(playerData) -- Player's angle of bank. local roll=playerData.unit:GetRoll() + -- Get groove zone. + local zone=self:_GetZoneGroove() + + -- Check if player is in zone. + local inzone=playerData.unit:IsInZone(zone) + -- Check if player is in +-4 deg cone and flying towards the runway. - if math.abs(lineup)<=4 then + --if math.abs(lineup)<=4 then + if inzone then -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) @@ -7994,7 +8105,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 @@ -8730,6 +8841,7 @@ end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ZONE functions ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Get Initial zone for Case I or II. -- @param #AIRBOSS self -- @param #number case Recovery Case. @@ -8742,8 +8854,7 @@ function AIRBOSS:_GetZoneInitial(case) -- Carrier coordinate. local cv=self:GetCoordinate() - -- Zone and vec2 array. - local zone + -- Vec2 array. local vec2 if case==1 then @@ -8775,11 +8886,43 @@ function AIRBOSS:_GetZoneInitial(case) end -- Polygon zone. - local zone=ZONE_POLYGON_BASE:New("CASE I/II initial.", vec2) + local zone=ZONE_POLYGON_BASE:New("Zone CASE I/II Initial", vec2) return zone end +--- Get groove zone. +-- @param #AIRBOSS self +-- @param #number l Length of the groove in NM. Default 1.5 NM. +-- @param #number w Width of the groove in NM. Default +-- @return Core.Zone#ZONE_POLYGON_BASE Initial zone. +function AIRBOSS:_GetZoneGroove(l, w) + + l=l or 1.5 + w=w or 0.5 + + -- Get radial, i.e. inverse of BRC. + local fbi=self:GetRadial(1, false, false) + + -- Stern coordinate. + local st=self:_GetSternCoord() + + -- Zone points. + local c1=st:Translate(self.carrierparam.totwidthstarboard, fbi-90) + local c2=st:Translate(UTILS.NMToMeters(0.10), fbi-90):Translate(UTILS.NMToMeters(0.3), fbi) + local c3=st:Translate(UTILS.NMToMeters(w/2), fbi-90):Translate(UTILS.NMToMeters(l), fbi) + local c4=st:Translate(UTILS.NMToMeters(w/2), fbi+90):Translate(UTILS.NMToMeters(l), fbi) + local c5=st:Translate(UTILS.NMToMeters(0.10), fbi+90):Translate(UTILS.NMToMeters(0.3), fbi) + local c6=st:Translate(self.carrierparam.totwidthport, fbi+90) + + -- Vec2 array. + local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2()} + + -- Polygon zone. + local zone=ZONE_POLYGON_BASE:New("Zone Groove", 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 @@ -11195,13 +11338,18 @@ function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed) -- Speed in km/h. local speedkmh=UTILS.KnotsToKmph(speed) + local cspeedkmh=self.carrier:GetVelocityKMH() local uspeedkmh=UTILS.KnotsToKmph(uspeed) -- Waypoint table. local wp={} + -- Pos1 is a bit into. + local pos1=pos0:Translate(500, pos0:HeadingTo(coord)) + -- Create from/to waypoints. - table.insert(wp, pos0:WaypointGround(speedkmh)) + table.insert(wp, pos0:WaypointGround(cspeedkmh)) + table.insert(wp, pos1:WaypointGround(cspeedkmh)) table.insert(wp, coord:WaypointGround(speedkmh)) -- If enabled, go back to where you came from. @@ -11241,8 +11389,8 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) -- Wind speed. local _,vwind=self:GetWind() - -- Speed of carrier in m/s. - local vtot=vdeck-vwind + -- Speed of carrier in m/s but at least 2 knots. + local vtot=math.max(vdeck-vwind, UTILS.KnotsToMps(2)) -- Distance to travel local dist=vtot*time @@ -12197,7 +12345,7 @@ function AIRBOSS:Broadcast(radio, call, loud) ---------------------------- -- Get unit sending the transmission. - local sender=self:_GetRadioSender() + local sender=self:_GetRadioSender(radio) -- Construct file name and subtitle. local filename=self:_RadioFilename(call, loud) @@ -12535,14 +12683,31 @@ 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 +-- @param #AIRBOSS.Radio radio Airboss radio data. -- @return Wrapper.Unit#UNIT Sending aircraft unit or nil if was not setup, is not an aircraft or is not alive. -function AIRBOSS:_GetRadioSender() +function AIRBOSS:_GetRadioSender(radio) -- Check if we have a sending aircraft. local sender=nil --Wrapper.Unit#UNIT + + -- Try the general default. if self.senderac then sender=UNIT:FindByName(self.senderac) end + + -- Try the specific marshal unit. + if radio.alias=="Marshal" then + if self.radiorelayMSH then + sender=UNIT:FindByName(self.radiorelayMSH) + end + end + + -- Try the specific LSO unit. + if radio.alias=="LSO" then + if self.radiorelayLSO then + sender=UNIT:FindByName(self.radiorelayLSO) + end + end -- Check that sender is alive and an aircraft. if sender and sender:IsAlive() and sender:IsAir() then @@ -12885,8 +13050,9 @@ function AIRBOSS:_AddF10Commands(_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 - missionCommands.addCommandForGroup(gid, "Spinning", _rootPath, self._RequestSpinning, self, _unitName) -- F6 - missionCommands.addCommandForGroup(gid, "[Reset My Status]", _rootPath, self._ResetPlayerStatus, self, _unitName) -- F7 + missionCommands.addCommandForGroup(gid, "Spinning", _rootPath, self._RequestSpinning, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "Emergency Landing", _rootPath, self._RequestEmergency, self, _unitName) -- F7 + missionCommands.addCommandForGroup(gid, "[Reset My Status]", _rootPath, self._ResetPlayerStatus, self, _unitName) -- F8 end else self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) @@ -13012,6 +13178,71 @@ function AIRBOSS:_RequestMarshal(_unitName) end end +--- Request emergency landing. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestEmergency(_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="" + if not self.emergency then + + -- Mission designer did not allow emergency landing. + text="negative, no emergency landings on my carrier. We are currently busy. See how you get along!" + + else + + -- Cleared. + text="roger, you can bypass the pattern and are cleared for final approach!" + + -- Now, if player is in the marshal or waiting queue he will be removed. But the new leader should stay in or not. + local lead=self:_GetFlightLead(playerData) + + -- Set set for lead. + self:_SetPlayerStep(lead, AIRBOSS.PatternStep.EMERGENCY) + + -- Also set emergency landing for all members. + for _,sec in pairs(lead.section) do + local sectionmember=sec --#AIRBOSS.PlayerData + self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.EMERGENCY) + + -- Remove flight from spinning queue just in case (everone can spin on his own). + self:_RemoveFlightFromQueue(self.Qspinning, sectionmember) + end + + + -- Remove flight from waiting queue just in case. + self:_RemoveFlightFromQueue(self.Qwaiting, lead) + + if self:_InQueue(self.Qmarshal, lead.group) then + -- Remove flight from Marshal queue and add to pattern. + self:_RemoveFlightFromMarshalQueue(lead) + else + -- Add flight to pattern if he was not. + if not self:_InQueue(self.Qpattern, lead.group) then + self:_AddFlightToPatternQueue(lead) + end + end + + end + + -- Send message. + self:MessageToPlayer(playerData, text, "AIRBOSS") + + end + + end +end + --- Request spinning. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. @@ -13030,25 +13261,20 @@ function AIRBOSS:_RequestSpinning(_unitName) local text="" if not self:_InQueue(self.Qpattern, playerData.group) then - -- Player not in pattern queue + -- Player not in pattern queue. text="negative, you have to be in the pattern to spin it!" - - --[[ - elseif playerData.seclead~=playerData.name then - - -- Player is not section lead - text="negative, your section lead has to call spinning." - ]] - + elseif playerData.step==AIRBOSS.PatternStep.SPINNING then + -- Player is already spinning. text="negative, you are already spinning." -- Check if player is in the right step. elseif not (playerData.step==AIRBOSS.PatternStep.BREAKENTRY or playerData.step==AIRBOSS.PatternStep.EARLYBREAK or playerData.step==AIRBOSS.PatternStep.LATEBREAK) then - + + -- Player is not in the right step. text="negative, you have to be in the right step to spin it!" else From 5ee0ae44cdf5ac0b5f04a024b01a1d650290d11c Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 15 Feb 2019 01:23:42 +0100 Subject: [PATCH 168/485] Update Airboss.lua --- Moose Development/Moose/Ops/Airboss.lua | 183 ++++++++++++++++++++---- 1 file changed, 152 insertions(+), 31 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index b61af90df..70d22b19a 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -207,6 +207,8 @@ -- @field #AIRBOSS.MarshalCalls MarshalCall Radio voice over of the Marshal/Airboss. -- @field #number lowfuelAI Low fuel threshold for AI groups in percent. -- @field #boolean emergency If true (default), allow emergency landings, i.e. bypass any pattern and go for final approach. +-- @field #boolean respawnAI If true, respawn AI flights as they enter the CCA to detach and airfields from the mission plan. Default false. +-- @field #boolean turning If true, carrier is currently turning. -- @extends Core.Fsm#FSM --- Be the boss! @@ -1044,6 +1046,7 @@ AIRBOSS = { MarshalCall = nil, lowfuelAI = nil, emergency = nil, + respawnAI = nil, } --- Aircraft types capable of landing on carrier (human+AI). @@ -1589,6 +1592,9 @@ function AIRBOSS:New(carriername, alias) -- No despawn after engine shutdown by default. self:SetDespawnOnEngineShutdown(false) + -- No respawning of AI groups when entering the CCA. + self:SetRespawnAI(false) + -- Mission uses static weather by default. self:SetStaticWeather() @@ -1658,6 +1664,8 @@ function AIRBOSS:New(carriername, alias) BASE:TraceLevel(1) self.dTstatus=0.1 end + + --self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red, 5) -- Smoke zones. if self.Debug and false then @@ -2217,7 +2225,7 @@ function AIRBOSS:SetEmergencyLandings(switch) end ---- Despawn AI groups after they they shut down their engines +--- Despawn AI groups after they they shut down their engines. -- @param #AIRBOSS self -- @param #boolean switch If true or nil, AI groups are despawned. -- @return #AIRBOSS self @@ -2230,6 +2238,19 @@ function AIRBOSS:SetDespawnOnEngineShutdown(switch) return self end +--- Respawn AI groups once they reach the CCA. Clears any attached airbases and allows making them land on the carrier via script. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, AI groups are respawned. +-- @return #AIRBOSS self +function AIRBOSS:SetRespawnAI(switch) + if switch==true or switch==nil then + self.respawnAI=true + else + self.respawnAI=false + end + 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. @@ -3970,6 +3991,69 @@ function AIRBOSS:_InitVoiceOvers() subtitle="", duration=0.40, --0.38 too short }, + CASE={ + file="MARSHAL-Case", + suffix="ogg", + loud=false, + subtitle="", + duration=0.45, + }, + EXPECTED={ + file="MARSHAL-Expected", + suffix="ogg", + loud=false, + subtitle="", + duration=0.60, + }, + BRC={ + file="MARSHAL-BRC", + suffix="ogg", + loud=false, + subtitle="", + duration=0.68, + }, + HOLDAT={ + file="MARSHAL-HoldAt", + suffix="ogg", + loud=false, + subtitle="", + duration=0.43, + }, + ANGELS={ + file="MARSHAL-Angels", + suffix="ogg", + loud=false, + subtitle="", + duration=0.68, + }, + EXPECTED={ + file="MARSHAL-Expected", + suffix="ogg", + loud=false, + subtitle="", + duration=0.72, + }, + ALTIMETER={ + file="MARSHAL-Altimeter", + suffix="ogg", + loud=false, + subtitle="", + duration=0.73, + }, + POINT={ + file="MARSHAL-Point", + suffix="ogg", + loud=false, + subtitle="", + duration=0.42, + }, + REPORTSEEME={ + file="MARSHAL-ReportSeeMe", + suffix="ogg", + loud=false, + subtitle="", + duration=1.05, + }, CLICK={ file="AIRBOSS-RadioClick", suffix="ogg", @@ -4640,13 +4724,13 @@ function AIRBOSS:_ScanCarrierZone() if stack then -- Send AI to marshal stack. We respawn the group to clean possible departure and destination airbases. - self:_MarshalAI(knownflight, stack, true) + self:_MarshalAI(knownflight, stack, self.respawnAI) 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, true) -- Group is respawned to clear any attached airfields. + self:_WaitAI(knownflight, self.respawnAI) -- Group is respawned to clear any attached airfields. end end @@ -8893,13 +8977,13 @@ end --- Get groove zone. -- @param #AIRBOSS self --- @param #number l Length of the groove in NM. Default 1.5 NM. --- @param #number w Width of the groove in NM. Default +-- @param #number l Length of the groove in NM. Default 2.0 NM. +-- @param #number w Width of the groove in NM. Default 0.3 NM. -- @return Core.Zone#ZONE_POLYGON_BASE Initial zone. function AIRBOSS:_GetZoneGroove(l, w) - l=l or 1.5 - w=w or 0.5 + l=l or 2.0 + w=w or 0.3 -- Get radial, i.e. inverse of BRC. local fbi=self:GetRadial(1, false, false) @@ -8907,6 +8991,8 @@ function AIRBOSS:_GetZoneGroove(l, w) -- Stern coordinate. local st=self:_GetSternCoord() + -- TODO: optimize for Tarawa. shift port. + -- Zone points. local c1=st:Translate(self.carrierparam.totwidthstarboard, fbi-90) local c2=st:Translate(UTILS.NMToMeters(0.10), fbi-90):Translate(UTILS.NMToMeters(0.3), fbi) @@ -11579,7 +11665,25 @@ function AIRBOSS:_CheckPatternUpdate() self.Corientlast=vNew -- Carrier is turning when its heading changed by at least one degree since last check. - local turning=deltaLast>=1 + local turning=math.abs(deltaLast)>=1 + + -- Starting to turn. + if turning and not self.turning then + -- Turning! + self.turning=true + + -- Get heading. + local hdg + if self.turnintowind then + hdg=select(1,self:GetWind()) + else + hdg=self:GetCoordinate():HeadingTo(self:_GetNextWaypoint()) + end + + -- Inform everyone. + local text=string.format("staring turn to heading %03d°.", hdg) + self:MessageToMarshal(text, "AIRBOSS", "99") + end -- No update if carrier is turning! if turning then @@ -11624,12 +11728,13 @@ function AIRBOSS:_CheckPatternUpdate() local FB=self:GetFinalBearing(true) local text=string.format("new final bearing %03d°.", FB) self:MessageToMarshal(text, "AIRBOSS", "99") + self.turning=false end -- Reset parameters for next update check. self.Corientation=vNew self.Cposition=pos - self.Tpupdate=timer.getTime() + self.Tpupdate=timer.getTime() end end @@ -12310,7 +12415,16 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) call~=self[caller].N6 and call~=self[caller].N7 and call~=self[caller].N8 and - call~=self[caller].N9 then + call~=self[caller].N9 and + call~=self[caller].EXPECTED and + call~=self[caller].BRC and + call~=self[caller].ALTIMETER and + call~=self[caller].POINT and + call~=self[caller].HOLDAT and + call~=self[caller].CASE and + call~=self[caller].POINT and + call~=self[caller].ANGELS and + call~=self[caller].CHARLIETIME then self:RadioTransmission(radio, self[caller].CLICK, false, delay) end end @@ -12889,47 +13003,54 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) text=text..string.format("Altimeter %.2f. Report see me.", qfe) -- Create new call to display complete subtitle. - --local casecall=self:_NewRadioCall(self.MarshalCall.CASE, "MARSHAL", text, self.Tmessage, modex) + local casecall=self:_NewRadioCall(self.MarshalCall.CASE, "MARSHAL", text, self.Tmessage, modex) - -- Until we have a case sound we take the noise for testing - local casecall=self:_NewRadioCall(self.MarshalCall.NOISE, "MARSHAL", text, self.Tmessage, modex) + + local delay=0 -- Case.. - self:RadioTransmission(self.MarshalRadio, casecall) + self:RadioTransmission(self.MarshalRadio, casecall, nil, delay) -- X.. - self:_Number2Radio(self.MarshalRadio, tostring(case)) + self:_Number2Radio(self.MarshalRadio, tostring(case), nil, delay) + delay=delay+0.5 -- expected.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, delay) -- BRC.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.BRC) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.BRC, nil, delay) -- XYZ.. - self:_Number2Radio(self.MarshalRadio, string.format("%03d", brc)) + self:_Number2Radio(self.MarshalRadio, string.format("%03d", brc), nil, delay) + delay=delay+0.5 -- hold at.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOLDAT) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOLDAT, nil, delay) + delay=delay+0.1 -- angels.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ANGELS) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ANGELS, nil, delay) -- X.. - self:_Number2Radio(self.MarshalRadio, tostring(angels)) + self:_Number2Radio(self.MarshalRadio, tostring(angels), nil, delay) + delay=delay+0.5 -- Expected.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, delay) -- Charlie time.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CHARLIETIME) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CHARLIETIME, nil, delay) -- XY.. (hours) - self:_Number2Radio(self.MarshalRadio, CT[1]) + self:_Number2Radio(self.MarshalRadio, CT[1], nil, delay) -- XY.. (minutes) - self:_Number2Radio(self.MarshalRadio, CT[2]) + self:_Number2Radio(self.MarshalRadio, CT[2], nil, delay) + delay=delay+0.5 -- Altimeter.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ALTIMETER) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ALTIMETER, nil, delay) -- XY.. - self:_Number2Radio(self.MarshalRadio, QFE[1]) + self:_Number2Radio(self.MarshalRadio, QFE[1], nil, delay) -- Point.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.POINT) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.POINT, nil, delay) -- XY.. - self:_Number2Radio(self.MarshalRadio, QFE[2]) + self:_Number2Radio(self.MarshalRadio, QFE[2], nil, delay) + delay=delay+0.5 -- Report see me. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.REPORTSEEME) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.REPORTSEEME, nil, delay) + delay=delay+0.2 -- Click! - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CLICK) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CLICK, nil, delay) end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- From df43ff08415d9881e3b62807feb26a85335469f1 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 15 Feb 2019 17:00:22 +0100 Subject: [PATCH 169/485] AB v0.9.8w --- Moose Development/Moose/Ops/Airboss.lua | 880 +++++++++++++----------- 1 file changed, 492 insertions(+), 388 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 70d22b19a..8951c67f6 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -209,6 +209,8 @@ -- @field #boolean emergency If true (default), allow emergency landings, i.e. bypass any pattern and go for final approach. -- @field #boolean respawnAI If true, respawn AI flights as they enter the CCA to detach and airfields from the mission plan. Default false. -- @field #boolean turning If true, carrier is currently turning. +-- @field #AIRBOSS.GLE gle Glidesope error thresholds. +-- @field #AIRBOSS.LUE lue Lineup error thresholds. -- @extends Core.Fsm#FSM --- Be the boss! @@ -1047,6 +1049,8 @@ AIRBOSS = { lowfuelAI = nil, emergency = nil, respawnAI = nil, + gle = {}, + lue = {}, } --- Aircraft types capable of landing on carrier (human+AI). @@ -1112,6 +1116,25 @@ AIRBOSS.CarrierType={ -- @field #number FAST Really fast AoA threshold. -- @field #number SLOW Really slow AoA threshold. +--- Glideslope error thresholds in degrees. +-- @type AIRBOSS.GLE +-- @field #number _max Max _OK_ value. Default 0.4 deg. +-- @field #number _min Min _OK_ value. Default -0.3 deg. +-- @field #number High (H) threshold. Default 0.8 deg. +-- @field #number Low (L) threshold. Default -0.6 deg. +-- @field #number HIGH H threshold. Default 1.5 deg. +-- @field #number LOW L threshold. Default -0.9 deg. + +--- Lineup error thresholds in degrees. +-- @type AIRBOSS.LUE +-- @field #number _max Max _OK_ value. Default 0.5 deg. +-- @field #number _min Min _OK_ value. Default -0.5 deg. +-- @field #number Left (LUR) threshold. Default -1.0 deg. +-- @field #number Right (LUL) threshold. Default 1.0 deg. +-- @field #number LEFT LUR threshold. Default -3.0 deg. +-- @field #number RIGHT LUL threshold. Default 3.0 deg. + + --- Pattern steps. -- @type AIRBOSS.PatternStep -- @field #string UNDEFINED "Undefined". @@ -1279,13 +1302,6 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall ALTIMETER "Altimeter" call. -- @field #AIRBOSS.RadioCall POINT "Point" call. ---- Message for player. --- @type AIRBOSS.PlayerMessage --- @field string message Message text. --- @field #string sender Sender of the message. --- @field #string receiver Receiver of the message. --- @field #number duration --- @field #boolean clear --- Difficulty level. -- @type AIRBOSS.Difficulty @@ -1409,7 +1425,7 @@ AIRBOSS.Difficulty={ -- @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. --- @field #table messages Table of messages. +-- @field #boolean showhints If true, show step hints. -- @extends #AIRBOSS.FlightGroup --- Main group level radio menu: F10 Other/Airboss. @@ -1609,6 +1625,12 @@ function AIRBOSS:New(carriername, alias) -- Default player skill EASY. self:SetDefaultPlayerSkill(AIRBOSS.Difficulty.EASY) + + -- Default glideslope error thresholds. + self:SetGlideslopeErrorThresholds() + + -- Default lineup error thresholds. + --self:SetLine -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() @@ -2295,6 +2317,45 @@ function AIRBOSS:SetDefaultMessageDuration(duration) return self end + +--- Set glideslope error thresholds. +-- @param #AIRBOSS self +-- @param #number _max +-- @param #number _min +-- @param #number High +-- @param #number HIGH +-- @param #number Low +-- @param #number LOW +-- @return #AIRBOSS self +function AIRBOSS:SetGlideslopeErrorThresholds(_max,_min, High, HIGH, Low, LOW) + self.gle._max=_max or 0.4 + self.gle.High=High or 0.8 + self.gle.HIGH=HIGH or 1.5 + self.gle._min=_min or -0.3 + self.gle.Low=Low or -0.6 + self.gle.LOW=LOW or -0.9 + return self +end + +--- Set lineup error thresholds. +-- @param #AIRBOSS self +-- @param #number _max +-- @param #number _min +-- @param #number Left +-- @param #number LEFT +-- @param #number Right +-- @param #number RIGHT +-- @return #AIRBOSS self +function AIRBOSS:SetLineupErrorThresholds(_max,_min, Left, LEFT, Right, RIGHT) + self.lue._max=_max or 0.5 + self.lue._min=_min or -0.5 + self.lue.Left=Left or -1.0 + self.lue.LEFT=LEFT or -3.0 + self.lue.Right=Right or 1.0 + self.lue.RIGHT=RIGHT or 3.0 + 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 @@ -4842,7 +4903,7 @@ function AIRBOSS:_MarshalPlayer(playerData, stack) for _,_flight in pairs(playerData.section) do local flight=_flight --#AIRBOSS.PlayerData - -- TODO: Inform player? Should be done by lead via radio? + -- XXX: Inform player? Should be done by lead via radio? -- Set step. self:_SetPlayerStep(flight, AIRBOSS.PatternStep.HOLDING) @@ -4928,7 +4989,7 @@ function AIRBOSS:_WaitAI(flight, respawn) if respawn then -- This should clear the landing waypoints. - -- TODO: This resets the weapons and the fuel state. But not the units fortunately. + -- Note: This resets the weapons and the fuel state. But not the units fortunately. -- Get group template. local Template=group:GetTemplate() @@ -5078,7 +5139,7 @@ function AIRBOSS:_MarshalAI(flight, nstack, respawn) if respawn then -- This should clear the landing waypoints. - -- TODO: This resets the weapons and the fuel state. But not the units fortunately. + -- Note: This resets the weapons and the fuel state. But not the units fortunately. -- Get group template. local Template=group:GetTemplate() @@ -5943,11 +6004,20 @@ function AIRBOSS:_NewPlayer(unitname) -- Set difficulty level. playerData.difficulty=playerData.difficulty or self.defaultskill - -- Subtitles of player + -- Subtitles of player. if playerData.subtitles==nil then playerData.subtitles=true end + -- Show step hints. + if playerData.showhints==nil then + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + playerData.showhints=false + else + playerData.showhints=true + end + end + -- Points rewarded. playerData.points={} @@ -6190,7 +6260,7 @@ function AIRBOSS:_CheckSectionRecovered(flight) -- Check all elements of the secmember flight group. for _,_element in pairs(sectionmember.elements) do - local element=_element --#AIROBSS.FlightElement + local element=_element --#AIRBOSS.FlightElement if not element.recovered then return false end @@ -7389,7 +7459,6 @@ function AIRBOSS:_Holding(playerData) self:T3("Player is still in the holding zone. Good job.") else -- Player left the holding zone. - 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 @@ -7428,12 +7497,11 @@ function AIRBOSS:_Holding(playerData) -- Player left holding zone if inholdingzone then -- Player is back in the holding zone. - 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:T2("Player still outside the holding zone. What are you doing man?!") + self:T3("Player still outside the holding zone. What are you doing man?!") end elseif playerData.holding==nil then @@ -7444,9 +7512,6 @@ function AIRBOSS:_Holding(playerData) -- Player arrived in holding zone. playerData.holding=true - -- Debug output. - self:T("Player entered the holding zone for the first time.") - -- Inform player. text=text..string.format("You arrived at the holding zone.") @@ -7463,20 +7528,18 @@ function AIRBOSS:_Holding(playerData) playerData.warning=true 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.") + self:T3("Waiting for player to arrive in the holding zone.") end end - -- Send message. - self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) + -- Send message. + if playerData.showhints then + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end @@ -7523,13 +7586,12 @@ function AIRBOSS:_Commencing(playerData, zonecheck) text=text.."Proceed to initial." else text=text.."Descent to platform." - if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.showhints then text=text.." VSI 4000 ft/min until you reach 5000 ft." end end -- Message to player. - --self:MessageToPlayer(playerData, text, "MARSHAL", nil, 3) self:MessageToPlayer(playerData, text, "MARSHAL") end @@ -7557,7 +7619,7 @@ 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. +-- @return #boolean True if player is in the initial zone. function AIRBOSS:_Initial(playerData) -- Check if player is in initial zone and entering the CASE I pattern. @@ -7570,7 +7632,7 @@ function AIRBOSS:_Initial(playerData) if inzone and math.abs(relheading)<60 then -- Send message for normal and easy difficulty. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + if playerData.showhints then -- Inform player. local hint=string.format("Initial") @@ -7805,128 +7867,7 @@ function AIRBOSS:_BolterPattern(playerData) end end ---- Display hint to player. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. --- @param #number delay Delay before playing sound messages. Default 0 sec. -function AIRBOSS:_PlayerHint(playerData, delay) - -- No hint for the pros. - if playerData.difficulty==AIRBOSS.Difficulty.HARD then - return - end - - -- Get optimal altitude, distance and speed. - local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) - - -- Get altitude hint. - local hintAlt,debriefAlt,callAlt=self:_AltitudeCheck(playerData, alt) - - -- Get speed hint. - local hintSpeed,debriefSpeed,callSpeed=self:_SpeedCheck(playerData, speed) - - -- Get AoA hint. - local hintAoA,debriefAoA,callAoA=self:_AoACheck(playerData, aoa) - - -- Get distance to the boat hint. - local hintDist,debriefDist,callDist=self:_DistanceCheck(playerData, dist) - - -- Message to player. - local hint="" - if hintAlt then - hint=hint.."\n"..hintAlt - end - if hintSpeed then - hint=hint.."\n"..hintSpeed - end - if hintAoA then - hint=hint.."\n"..hintAoA - end - if hintDist then - hint=hint.."\n"..hintDist - end - - -- Debriefing text. - local debrief="" - if debriefAlt then - debrief=debrief.."\n-"..debriefAlt - end - if debriefSpeed then - debrief=debrief.."\n-"..debriefSpeed - end - if debriefAoA then - debrief=debrief.."\n-"..debriefAoA - end - if debriefDist then - debrief=debrief.."\n-"..debriefDist - end - - -- Add step to debriefing. - if debrief~="" then - self:_AddToDebrief(playerData, debrief) - end - - delay=delay or 0 - if callAlt then - self:Sound2Player(playerData, self.LSORadio, callAlt, false, delay) - delay=delay+callAlt.duration+0.5 - end - if callSpeed then - self:Sound2Player(playerData, self.LSORadio, callSpeed, false, delay) - delay=delay+callSpeed.duration+0.5 - end - if callAoA then - self:Sound2Player(playerData, self.LSORadio, callAoA, false, delay) - delay=delay+callAoA.duration+0.5 - end - if callDist then - self:Sound2Player(playerData, self.LSORadio, callDist, false, delay) - delay=delay+callDist.duration+0.5 - end - - -- ARC IN info. - if playerData.step==AIRBOSS.PatternStep.ARCIN then - - -- 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) - local turn="right" - if self.holdingoffset<0 then - turn="left" - end - hint=hint..string.format("\nTurn %s and select TACAN %03d°.", turn, radial) - end - - end - - -- DIRTUP additonal info. - if playerData.step==AIRBOSS.PatternStep.DIRTYUP then - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - hint=hint.."\nFAF! Checks completed. Nozzles 50°." - else - hint=hint.."\nDirty up! Hook, gear and flaps down." - end - end - end - - -- BULLSEYE additonal info. - if playerData.step==AIRBOSS.PatternStep.BULLSEYE then - -- Hint follow the needles. - if playerData.difficulty==AIRBOSS.Difficulty.EASY then - if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then - hint=hint..string.format("\nIntercept glideslope and follow the needles.") - end - end - end - - -- Message to player. - if hint~="" then - local text=string.format("%s%s", playerData.step, hint) - self:MessageToPlayer(playerData, hint, "AIRBOSS", "") - end -end --- Break entry for case I/II recoveries. -- @param #AIRBOSS self @@ -8683,7 +8624,7 @@ function AIRBOSS:_CheckFoulDeck(playerData) self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, false, 1.2) -- Player hint for flight students. - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + if playerData.showhints then local text=string.format("overfly landing area and enter bolter pattern.") self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) end @@ -10091,19 +10032,20 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Glideslope high/low calls. --TODO: introduce GSE enumerator values. - if glideslopeError>1.5 then + if glideslopeError>self.gle.HIGH then --1.5 then -- "You're high!" self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, true) advice=advice+self.LSOCall.HIGH.duration - elseif glideslopeError>0.8 then + elseif glideslopeError>self.gle.High then --0.8 then -- "You're high." self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, false) advice=advice+self.LSOCall.HIGH.duration - elseif glideslopeError<-0.9 then + elseif glideslopeError3 then + elseif lineupError>self.lue.RIGHT then --3 then -- "Right for lineup!" self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, true) advice=advice+self.LSOCall.RIGHTFORLINEUP.duration - elseif lineupError>1 then + elseif lineupError>self.lue.Right then -- 1 then -- "Right for lineup." self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, false) advice=advice+self.LSOCall.RIGHTFORLINEUP.duration @@ -10369,7 +10311,7 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) elseif AOA FB for Case II or BRC for Case III. + local radial=self:GetRadial(playerData.case, true, false, true) + local turn="right" + if self.holdingoffset<0 then + turn="left" + end + hint=hint..string.format("\nTurn %s and select TACAN %03d°.", turn, radial) + end + + end + + -- DIRTUP additonal info. + if playerData.step==AIRBOSS.PatternStep.DIRTYUP then + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + hint=hint.."\nFAF! Checks completed. Nozzles 50°." + else + hint=hint.."\nDirty up! Hook, gear and flaps down." + end + end + end + + -- BULLSEYE additonal info. + if playerData.step==AIRBOSS.PatternStep.BULLSEYE then + -- Hint follow the needles. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then + hint=hint..string.format("\nIntercept glideslope and follow the needles.") + end + end + end + + -- Message to player. + if hint~="" then + local text=string.format("%s%s", playerData.step, hint) + self:MessageToPlayer(playerData, hint, "AIRBOSS", "") + end +end + + +--- 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. +function AIRBOSS:_StepHint(playerData, step) + + -- Set step. + step=step or playerData.step + + -- Message is only for "Flight Students". + if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.showhints 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 %d 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 %d knots", UTILS.MpsToKnots(speed)) + end + + -- Distance to the boat. + if dist then + hint=hint..string.format("\nDistance to the boat %.1f NM", UTILS.MetersToNM(dist)) + end + + if step==AIRBOSS.PatternStep.ABEAM then + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + hint=hint.."\nNozzles 50°-60°. Antiskid OFF. Lights OFF." + else + hint=hint.."\nDirty up! Gear DOWN, flaps DOWN. Check hook down." + end + 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:%s", step, hint) + + -- Send hint to player. + self:MessageToPlayer(playerData, text, "AIRBOSS", "", nil, false, 1) + + end + + end end @@ -10701,23 +10799,20 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) local radiocall=nil --#AIRBOSS.RadioCall local hint="" - local loud=false if _error>badscore then --hint=string.format("You're high.") - radiocall=self:_NewRadioCall(self.LSOCall.HIGH, nil, nil, 5) - loud=true + radiocall=self:_NewRadioCall(self.LSOCall.HIGH, "Paddles", "") elseif _error>lowscore then --hint= string.format("You're slightly high.") - radiocall=self:_NewRadioCall(self.LSOCall.HIGH, nil, nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.HIGH, "Paddles", "") elseif _error<-badscore then --hint=string.format("You're low. ") - radiocall=self:_NewRadioCall(self.LSOCall.LOW, nil, nil, 5) - loud=true + radiocall=self:_NewRadioCall(self.LSOCall.LOW, "Paddles", "") elseif _error<-lowscore then --hint=string.format("You're slightly low.") - radiocall=self:_NewRadioCall(self.LSOCall.LOW, nil, nil, 5) + radiocall=self:_NewRadioCall(self.LSOCall.LOW, "Paddles", "") else - hint=string.format("Good altitude.") + hint=string.format("Good altitude. ") end -- Extend or decrease depending on skill. @@ -10738,6 +10833,133 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) return hint, debrief,radiocall end +--- Score for correct AoA. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number optaoa Optimal AoA. +-- @return #string Feedback message text or easy and normal difficulty level or nil for hard. +-- @return #string Debriefing text. +-- @return #AIRBOSS.RadioCall Radio call. +function AIRBOSS:_AoACheck(playerData, optaoa) + + 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-optaoa)/optaoa*100 + + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall + + -- Rate aoa. + local hint="" + if aoa>=aircraftaoa.SLOW then + --hint="Your're slow!" + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "Paddles", "") + elseif aoa>=aircraftaoa.Slow then + --hint="Your're slow." + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "Paddles", "") + 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(self.LSOCall.FAST, "Paddles", "") + else + --hint="You're fast!" + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "Paddles", "") + 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)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep is short normally. + hint="" + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debriefing text. + local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.", self:_AoADeg2Units(playerData, aoa), _error, self:_AoADeg2Units(playerData, optaoa)) + + return hint, debrief,radiocall +end + +--- Evaluate player's speed. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number speedopt Optimal speed in m/s. +-- @return #string Feedback text. +-- @return #string Debriefing text. +-- @return #AIRBOSS.RadioCall Radio call. +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 + + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall + + local hint="" + if _error>badscore then + --hint=string.format("You're fast.") + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "AIRBOSS", "") + elseif _error>lowscore then + --hint= string.format("You're slightly fast.") + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "AIRBOSS", "") + elseif _error<-badscore then + --hint=string.format("You're slow.") + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "AIRBOSS", "") + elseif _error<-lowscore then + --hint=string.format("You're slightly slow.") + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "AIRBOSS", "") + 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 speed is %d knots.", UTILS.MpsToKnots(speedopt)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep is short normally. + hint="" + 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.", UTILS.MpsToKnots(speed), _error, UTILS.MpsToKnots(speedopt)) + + return hint, debrief, radiocall +end + --- Evaluate player's distance to the boat at checkpoint. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. @@ -10781,7 +11003,6 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) -- We keep it short normally. elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then -- No hint at all for the pros. - hint="" end -- Debriefing text. @@ -10790,140 +11011,9 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) return hint, debrief, nil end ---- Score for correct AoA. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data. --- @param #number optaoa Optimal AoA. --- @return #string Feedback message text or easy and normal difficulty level or nil for hard. --- @return #string Debriefing text. --- @return #AIRBOSS.RadioCall Radio call. -function AIRBOSS:_AoACheck(playerData, optaoa) - - 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-optaoa)/optaoa*100 - - -- 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!" - radiocall=self:_NewRadioCall(self.LSOCall.SLOW, nil, nil, 5) - loud=true - elseif aoa>=aircraftaoa.Slow then - --hint="Your're slow." - radiocall=self:_NewRadioCall(self.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(self.LSOCall.FAST, nil, nil, 5) - else - --hint="You're fast!" - radiocall=self:_NewRadioCall(self.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="" - end - - -- Debriefing text. - local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.", self:_AoADeg2Units(playerData, aoa), _error, self:_AoADeg2Units(playerData, optaoa)) - - return hint, debrief,radiocall -end - ---- Evaluate player's speed. --- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player data table. --- @param #number speedopt Optimal speed in m/s. --- @return #string Feedback text. --- @return #string Debriefing text. --- @return #AIRBOSS.RadioCall Radio call. -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 - - -- 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.") - radiocall=self:_NewRadioCall(self.LSOCall.FAST, "AIRBOSS", nil, 5) - loud=true - elseif _error>lowscore then - --hint= string.format("You're slightly fast.") - radiocall=self:_NewRadioCall(self.LSOCall.FAST, "AIRBOSS", nil, 5) - elseif _error<-badscore then - --hint=string.format("You're slow.") - radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "AIRBOSS", nil, 5) - elseif _error<-lowscore then - --hint=string.format("You're slightly slow.") - radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "AIRBOSS", nil, 5) - loud=true - 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 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="" - end - - -- Debrief text. - local debrief=string.format("Speed %d knots = %d%% deviation from %d knots.", UTILS.MpsToKnots(speed), _error, UTILS.MpsToKnots(speedopt)) - - return hint, debrief, radiocall -end +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DEBRIEFING +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Append text to debriefing. -- @param #AIRBOSS self @@ -11201,66 +11291,6 @@ function AIRBOSS:_Debrief(playerData) end end ---- 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. -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 %d 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 %d knots", UTILS.MpsToKnots(speed)) - end - - -- Distance to the boat. - if dist then - hint=hint..string.format("\nDistance to the boat %.1f NM", UTILS.MetersToNM(dist)) - end - - if step==AIRBOSS.PatternStep.ABEAM then - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - hint=hint.."\nNozzles 50°-60°. Antiskid OFF. Lights OFF." - else - hint=hint.."\nDirty up! Gear DOWN, flaps DOWN. Check hook down." - end - 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:%s", step, hint) - - -- Send hint to player. - self:MessageToPlayer(playerData, text, "AIRBOSS", "", nil, false, 1) - - end - - end -end - ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- CARRIER ROUTING Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -11989,6 +12019,34 @@ function AIRBOSS:_GetTowerFrequency() end end +--- 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 Error margin for still being okay. +-- @return #number Error margin for really sucking. +function AIRBOSS:_GetGoodBadScore(playerData) + + local lowscore + local badscore + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + lowscore=10 + badscore=20 + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + lowscore=5 + badscore=10 + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + lowscore=2.5 + badscore=5 + end + + return lowscore, badscore +end + --- Check if aircraft is capable of landing on this aircraft carrier. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) @@ -12363,6 +12421,7 @@ end function AIRBOSS:RadioTransmission(radio, call, loud, delay) self:F2({radio=radio, call=call, loud=loud, delay=delay}) + -- Nil check. if radio==nil or call==nil then return end @@ -13012,6 +13071,7 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) self:RadioTransmission(self.MarshalRadio, casecall, nil, delay) -- X.. self:_Number2Radio(self.MarshalRadio, tostring(case), nil, delay) + delay=delay+0.5 -- expected.. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, delay) @@ -13019,6 +13079,7 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) self:RadioTransmission(self.MarshalRadio, self.MarshalCall.BRC, nil, delay) -- XYZ.. self:_Number2Radio(self.MarshalRadio, string.format("%03d", brc), nil, delay) + delay=delay+0.5 -- hold at.. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOLDAT, nil, delay) @@ -13027,6 +13088,7 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ANGELS, nil, delay) -- X.. self:_Number2Radio(self.MarshalRadio, tostring(angels), nil, delay) + delay=delay+0.5 -- Expected.. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, delay) @@ -13036,6 +13098,7 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) self:_Number2Radio(self.MarshalRadio, CT[1], nil, delay) -- XY.. (minutes) self:_Number2Radio(self.MarshalRadio, CT[2], nil, delay) + delay=delay+0.5 -- Altimeter.. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ALTIMETER, nil, delay) @@ -13045,6 +13108,7 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) self:RadioTransmission(self.MarshalRadio, self.MarshalCall.POINT, nil, delay) -- XY.. self:_Number2Radio(self.MarshalRadio, QFE[2], nil, delay) + delay=delay+0.5 -- Report see me. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.REPORTSEEME, nil, delay) @@ -13140,6 +13204,7 @@ function AIRBOSS:_AddF10Commands(_unitName) 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 + missionCommands.addCommandForGroup(gid, "Hints On/Off", _skillPath, self._SetHintsOnOff, self, playername) -- F4 -- F10/Airboss//F1 Help/ missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F3 missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._DisplayAttitude, self, _unitName) -- F4 @@ -13985,7 +14050,7 @@ function AIRBOSS:_DisplayDebriefing(_unitName) for _,_data in pairs(playerData.lastdebrief) do local step=_data.step local comment=_data.hint - text=text..string.format("* %s:\n",step) + text=text..string.format("* %s:",step) text=text..string.format("%s\n", comment) end else @@ -14286,6 +14351,45 @@ function AIRBOSS:_SetDifficulty(playername, difficulty) else self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) end + + -- Set hints as well. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + playerData.showhints=false + else + playerData.showhints=true + end + +end + +--- Turn player's aircraft attitude display on or off. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_SetHintsOnOff(_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.showhints=not playerData.showhints + + -- Inform player. + local text="" + if playerData.showhints==true then + text=string.format("hints are now ON.") + elseif playerData.showhints==false then + text=string.format("hints are now OFF.") + end + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + + end + end end --- Turn player's aircraft attitude display on or off. From 60b6d2ade03a3fdc0ab761a08442db6e50886515 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 17 Feb 2019 23:16:07 +0100 Subject: [PATCH 170/485] Update Airboss.lua --- Moose Development/Moose/Ops/Airboss.lua | 975 ++++++++++++++++-------- 1 file changed, 644 insertions(+), 331 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 8951c67f6..b79a9c42a 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -157,7 +157,9 @@ -- @field #table Qwaiting Queue of aircraft groups waiting outside 10 NM zone for the next free Marshal stack. -- @field #table Qspinning Queue of aircraft currently spinning. -- @field #table RQMarshal Radio queue of marshal. +-- @field #number TQMarshal Abs mission time, the last transmission ended. -- @field #table RQLSO Radio queue of LSO. +-- @field #number TQLSO Abs mission time, the last transmission ended. -- @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. @@ -580,7 +582,7 @@ -- -- * 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". +-- * 2.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 Points **CUT**: "Cut pass", when player was waved off but landed anyway. -- -- ## Foul Deck Waveoff @@ -1000,6 +1002,8 @@ AIRBOSS = { Qspinning = {}, RQMarshal = {}, RQLSO = {}, + TQMarshal = 0, + TQLSO = 0, Nmaxpattern = nil, Nmaxmarshal = nil, NmaxSection = nil, @@ -1277,9 +1281,17 @@ AIRBOSS.GroovePos={ --- Marshal radio calls. -- @type AIRBOSS.MarshalCalls --- @field #AIRBOSS.RadioCall RADIOCHECK "Radio check" call. --- @field #AIRBOSS.RadioCall SAYNEEDLES "Say needles" call. +-- @field #AIRBOSS.RadioCall ALTIMETER "Altimeter" call. +-- @field #AIRBOSS.RadioCall BRC "BRC" call. +-- @field #AIRBOSS.RadioCall CARRIERTURNTOHEADING "Turn to heading" call. +-- @field #AIRBOSS.RadioCall CASE "Case" call. +-- @field #AIRBOSS.RadioCall CHARLIETIME "Charlie Time" call. +-- @field #AIRBOSS.RadioCall CLEAREDFORCASE "You're cleared for case" call. +-- @field #AIRBOSS.RadioCall DEGREES "Degrees" call. +-- @field #AIRBOSS.RadioCall EXPECTED "Expected" call. -- @field #AIRBOSS.RadioCall FLYNEEDLES "Fly your needles" call. +-- @field #AIRBOSS.RadioCall HOLDATANGELS "Hold at angels" call. +-- @field #AIRBOSS.RadioCall MARSHALRADIAL "Marshal radial" call. -- @field #AIRBOSS.RadioCall N0 "Zero" call. -- @field #AIRBOSS.RadioCall N1 "One" call. -- @field #AIRBOSS.RadioCall N2 "Two" call. @@ -1290,17 +1302,21 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall N7 "Seven" call. -- @field #AIRBOSS.RadioCall N8 "Eight" call. -- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall NEWFB "New final bearing" call. +-- @field #AIRBOSS.RadioCall OBS "Obs" call. +-- @field #AIRBOSS.RadioCall POINT "Point" call. +-- @field #AIRBOSS.RadioCall RADIOCHECK "Radio check" call. +-- @field #AIRBOSS.RadioCall RECOVERY "Recovery" call. +-- @field #AIRBOSS.RadioCall RECOVERYPAUSEDNOTICE "Recovery paused until further notice" call. +-- @field #AIRBOSS.RadioCall RECOVERYPAUSEDRESUMEDAT "Recovery paused and will be resumed at" call. +-- @field #AIRBOSS.RadioCall RESUMERECOVERY "Recovery paused until further notice" call. +-- @field #AIRBOSS.RadioCall REPORTSEEME "Report see me" call. +-- @field #AIRBOSS.RadioCall SAYNEEDLES "Say needles" call. +-- @field #AIRBOSS.RadioCall STACKFULL "Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions" call. +-- @field #AIRBOSS.RadioCall STARTINGRECOVERY "Starting aircraft recovery" call. -- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. -- @field #AIRBOSS.RadioCall NOISE Static noise sound. --- @field #AIRBOSS.RadioCall CASE "Case" call. --- @field #AIRBOSS.RadioCall EXPECTED "Expected" call. --- @field #AIRBOSS.RadioCall BRC "BRC" call. --- @field #AIRBOSS.RadioCall CHARLIETIME "Charlie Time" call. --- @field #AIRBOSS.RadioCall REPORTSEEME "Report see me" call. --- @field #AIRBOSS.RadioCall HOLDAT "Hold at" call. --- @field #AIRBOSS.RadioCall ANGELS "Angels" call. --- @field #AIRBOSS.RadioCall ALTIMETER "Altimeter" call. --- @field #AIRBOSS.RadioCall POINT "Point" call. + --- Difficulty level. @@ -1630,7 +1646,7 @@ function AIRBOSS:New(carriername, alias) self:SetGlideslopeErrorThresholds() -- Default lineup error thresholds. - --self:SetLine + self:SetLineupErrorThresholds() -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() @@ -1679,8 +1695,8 @@ function AIRBOSS:New(carriername, alias) ------------------- -- Debug trace. - if false then - self.Debug=true + if true then + --self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) @@ -1783,28 +1799,7 @@ function AIRBOSS:New(carriername, alias) -- Flare points every 3 seconds for 3 minutes. SCHEDULER:New(nil, flareme, {}, 1, 3, nil, 180) end - - -- Debug: - if false then - local text="Playing default sound files:" - for _name,_call in pairs(self.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 --- ----------------------- @@ -2561,6 +2556,76 @@ function AIRBOSS:SetUserSoundRadio() return self end +--- Test LSO radio sounds. +-- @param #AIRBOSS self +-- @param #number delay Delay in seconds be sound check starts. +-- @return #AIRBOSS self +function AIRBOSS:SoundCheckLSO(delay) + + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(nil, AIRBOSS.SoundCheckLSO, {self}, delay) + else + + + local text="Playing LSO sound files:" + + for _name,_call in pairs(self.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) + + -- Also play the loud version. + if call.loud then + self:RadioTransmission(self.LSORadio, call, true) + end + end + + -- Debug message. + self:I(self.lid..text) + + end +end + +--- Test Marshal radio sounds. +-- @param #AIRBOSS self +-- @param #number delay Delay in seconds be sound check starts. +-- @return #AIRBOSS self +function AIRBOSS:SoundCheckMarshal(delay) + + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(nil, AIRBOSS.SoundCheckMarshal, {self}, delay) + else + + + local text="Playing Marshal sound files:" + + for _name,_call in pairs(self.MarshalCall) 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.MarshalRadio, call, false) + + -- Also play the loud version. + if call.loud then + self:RadioTransmission(self.MarshalRadio, call, true) + end + end + + -- Debug message. + self:I(self.lid..text) + + end +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. @@ -2962,7 +3027,7 @@ function AIRBOSS:_CheckAIStatus() if lineup<2 and distance<=0.75 and alt<500 and not element.ballcall then -- Paddles: Call the ball! - self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, false, 0) + self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, false, 0, nil, true) -- Pilot: "405, Hornet Ball, 3.2" -- TODO: Voice over. @@ -2972,8 +3037,8 @@ function AIRBOSS:_CheckAIStatus() -- 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, self.LSOCall.ROGERBALL, false, 6) + -- Paddles: Roger ball after 6 seconds. + self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, false, 6, nil, true) -- Flight element called the ball. element.ballcall=true @@ -3370,19 +3435,10 @@ function AIRBOSS:onafterRecoveryStart(From, Event, To, Case, Offset) -- Input or default value. Offset=Offset or self.defaultoffset - - -- Debug output. - local text=string.format("Starting aircraft recovery Case %d ops.", Case) - if Case>1 then - 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 inside CCA. - self:MessageToMarshal(text, "AIRBOSS", "99") + -- Radio message: "99, starting aircraft recovery case X ops. (Marshal radial XYZ degrees)" + self:_MarshalCallRecoveryStart(Case) + -- Switch to case. self:RecoveryCase(Case, Offset) end @@ -3430,19 +3486,26 @@ function AIRBOSS:onafterRecoveryPause(From, Event, To, duration) 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. + -- Time to resume. local clock=UTILS.SecondsToClock(timer.getAbsTime()+duration) - text=string.format("aircraft recovery is paused and will be resumed at %s.", clock) + + -- Marshal call: "99, aircraft recovery paused and will be resume at XX:YY." + self:_MarshalCallRecoveryPausedResumedAt(clock) + else + + local text=string.format("aircraft recovery is paused until further notice.") + + -- Marshal call: "99, aircraft recovery paused until further notice." + self:_MarshalCallRecoveryPausedNotice() + end - -- Message to Marshal. - self:MessageToMarshal(text, "AIRBOSS", "99") end --- On after "RecoveryUnpause" event. Recovery of aircraft is resumed. @@ -3453,12 +3516,10 @@ end function AIRBOSS:onafterRecoveryUnpause(From, Event, To) -- Debug output. self:T(self.lid..string.format("Unpausing aircraft recovery.")) + + -- Resume recovery. + self:_MarshalCallRecoveryResume() - -- Message text. - local text=string.format("resuming aircraft recovery.") - - -- Message to Marshal. - self:MessageToMarshal(text, "AIRBOSS", "99") end @@ -3686,7 +3747,52 @@ end -- @param #AIRBOSS self function AIRBOSS:_InitVoiceOvers() + --------------- + -- LSO Radio -- + --------------- + + -- LSO Radio Calls. self.LSOCall={ + BOLTER={ + file="LSO-BolterBolter", + suffix="ogg", + loud=false, + subtitle="Bolter, Bolter", + duration=0.75, + subduration=5, + }, + CALLTHEBALL={ + file="LSO-CallTheBall", + suffix="ogg", + loud=false, + subtitle="Call the ball", + duration=0.6, + subduration=2, + }, + CHECK={ + file="LSO-Check", + suffix="ogg", + loud=false, + subtitle="Check", + duration=0.45, + subduration=2.5, + }, + CLEAREDTOLAND={ + file="LSO-ClearedToLand", + suffix="ogg", + loud=false, + subtitle="Cleared to land", + duration=1.0, + subduration=5, + }, + COMELEFT={ + file="LSO-ComeLeft", + suffix="ogg", + loud=true, + subtitle="Come left", + duration=0.60, + subduration=1, + }, RADIOCHECK={ file="LSO-RadioCheck", suffix="ogg", @@ -3703,14 +3809,6 @@ function AIRBOSS:_InitVoiceOvers() duration=0.80, subduration=1, }, - COMELEFT={ - file="LSO-ComeLeft", - suffix="ogg", - loud=true, - subtitle="Come left", - duration=0.60, - subduration=1, - }, HIGH={ file="LSO-High", suffix="ogg", @@ -3751,14 +3849,6 @@ function AIRBOSS:_InitVoiceOvers() duration=0.7, subduration=1, }, - CALLTHEBALL={ - file="LSO-CallTheBall", - suffix="ogg", - loud=false, - subtitle="Call the ball", - duration=0.6, - subduration=2, - }, ROGERBALL={ file="LSO-RogerBall", suffix="ogg", @@ -3774,14 +3864,6 @@ function AIRBOSS:_InitVoiceOvers() subtitle="Wave off", duration=0.6, subduration=5, - }, - BOLTER={ - file="LSO-BolterBolter", - suffix="ogg", - loud=false, - subtitle="Bolter, Bolter", - duration=0.75, - subduration=5, }, LONGINGROOVE={ file="LSO-LongInTheGroove", @@ -3839,22 +3921,6 @@ function AIRBOSS:_InitVoiceOvers() duration=2.0, subduration=5, }, - CLEAREDTOLAND={ - file="LSO-ClearedToLand", - suffix="ogg", - loud=false, - subtitle="Cleared to land", - duration=1.0, - subduration=5, - }, - CHECK={ - file="LSO-Check", - suffix="ogg", - loud=false, - subtitle="Check", - duration=0.45, - subduration=2.5, - }, STABILIZED={ file="LSO-Stabilized", suffix="ogg", @@ -3957,22 +4023,68 @@ function AIRBOSS:_InitVoiceOvers() }, } + ------------------- + -- MARSHAL Radio -- + ------------------- + + -- MARSHAL Radio Calls. self.MarshalCall={ - RADIOCHECK={ - file="MARSHAL-RadioCheck", + ALTIMETER={ + file="MARSHAL-Altimeter", suffix="ogg", loud=false, - subtitle="Radio check", - duration=1.1, + subtitle="", + duration=0.85, + }, + BRC={ + file="MARSHAL-BRC", + suffix="ogg", + loud=false, + subtitle="", + duration=0.80, + }, + CARRIERTURNTOHEADING={ + file="MARSHAL-CarrierTurnToHeading", + suffix="ogg", + loud=false, + subtitle="", + duration=1.75, subduration=5, }, - SAYNEEDLES={ - file="MARSHAL-SayNeedles", + CASE={ + file="MARSHAL-Case", suffix="ogg", loud=false, - subtitle="Say needles", - duration=0.9, - subduration=5, + subtitle="", + duration=0.40, + }, + CHARLIETIME={ + file="MARSHAL-CharlieTime", + suffix="ogg", + loud=false, + subtitle="", + duration=0.90, + }, + CLEAREDFORCASE={ + file="MARSHAL-ClearedForCase", + suffix="ogg", + loud=false, + subtitle="", + duration=1.25, + }, + DEGREES={ + file="MARSHAL-Degrees", + suffix="ogg", + loud=false, + subtitle="", + duration=0.60, + }, + EXPECTED={ + file="MARSHAL-Expected", + suffix="ogg", + loud=false, + subtitle="", + duration=0.55, }, FLYNEEDLES={ file="MARSHAL-FlyYourNeedles", @@ -3982,6 +4094,27 @@ function AIRBOSS:_InitVoiceOvers() duration=0.9, subduration=5, }, + HOLDATANGELS={ + file="MARSHAL-HoldAtAngels", + suffix="ogg", + loud=false, + subtitle="", + duration=1.10, + }, + MARSHALRADIAL={ + file="MARSHAL-MarshalRadial", + suffix="ogg", + loud=false, + subtitle="", + duration=1.10, + }, + NEWFB={ + file="MARSHAL-NewFB", + suffix="ogg", + loud=false, + subtitle="", + duration=1.35, + }, N0={ file="MARSHAL-N0", suffix="ogg", @@ -4052,69 +4185,91 @@ function AIRBOSS:_InitVoiceOvers() subtitle="", duration=0.40, --0.38 too short }, - CASE={ - file="MARSHAL-Case", + OPS={ + file="MARSHAL-Ops", suffix="ogg", loud=false, subtitle="", - duration=0.45, + duration=0.48, }, - EXPECTED={ - file="MARSHAL-Expected", - suffix="ogg", - loud=false, - subtitle="", - duration=0.60, - }, - BRC={ - file="MARSHAL-BRC", - suffix="ogg", - loud=false, - subtitle="", - duration=0.68, - }, - HOLDAT={ - file="MARSHAL-HoldAt", - suffix="ogg", - loud=false, - subtitle="", - duration=0.43, - }, - ANGELS={ - file="MARSHAL-Angels", - suffix="ogg", - loud=false, - subtitle="", - duration=0.68, - }, - EXPECTED={ - file="MARSHAL-Expected", - suffix="ogg", - loud=false, - subtitle="", - duration=0.72, - }, - ALTIMETER={ - file="MARSHAL-Altimeter", - suffix="ogg", - loud=false, - subtitle="", - duration=0.73, - }, POINT={ file="MARSHAL-Point", suffix="ogg", loud=false, subtitle="", - duration=0.42, + duration=0.33, }, + RADIOCHECK={ + file="MARSHAL-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Radio check", + duration=1.20, + subduration=5, + }, + RECOVERY={ + file="MARSHAL-Recovery", + suffix="ogg", + loud=false, + subtitle="", + duration=0.70, + subduration=5, + }, + RECOVERYPAUSEDNOTICE={ + file="MARSHAL-RecoveryPausedNotice", + suffix="ogg", + loud=false, + subtitle="aircraft recovery paused until further notice", + duration=2.90, + subduration=5, + }, + RECOVERYPAUSEDRESUMEDAT={ + file="MARSHAL-RecoveryPausedResumed", + suffix="ogg", + loud=false, + subtitle="", + duration=3.40, + subduration=5, + }, REPORTSEEME={ file="MARSHAL-ReportSeeMe", suffix="ogg", loud=false, subtitle="", - duration=1.05, - }, + duration=0.95, + }, + RESUMERECOVERY={ + file="MARSHAL-ResumeRecovery", + suffix="ogg", + loud=false, + subtitle="resuming aircraft recovery", + duration=1.75, + subduraction=5, + }, + SAYNEEDLES={ + file="MARSHAL-SayNeedles", + suffix="ogg", + loud=false, + subtitle="Say needles", + duration=0.90, + subduration=5, + }, + STACKFULL={ + file="MARSHAL-StackFull", + suffix="ogg", + loud=false, + subtitle="Marshal Stack is currently full. Hold outside 10 NM zone and wait for further instuctions", + duration=6.35, + subduration=10, + }, + STARTINGRECOVERY={ + file="MARSHAL-StartingRecovery", + suffix="ogg", + loud=false, + subtitle="", + duration=2.65, + subduration=5, + }, CLICK={ file="AIRBOSS-RadioClick", suffix="ogg", @@ -4640,12 +4795,9 @@ function AIRBOSS:_ClearForLanding(flight) 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, nil, false, 2) - + -- Cleared for Case X recovery. + self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) + end --- Set player step. Any warning is erased and next step hint shown. @@ -4844,19 +4996,9 @@ function AIRBOSS:_WaitPlayer(playerData) -- 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:MessageToMarshal(text, "AIRBOSS", playerData.onboard) + -- Radio message: Stack is full. + self:_MarshalCallStackFull(playerData.onboard, nwaiting) -- Add player flight to waiting queue. table.insert(self.Qwaiting, playerData) @@ -5471,7 +5613,8 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) -- Stack altitude. - local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) + --local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) + local alt=self:_GetMarshalAltitude(stack, flight.case) -- Current BRC. local brc=self:GetBRC() @@ -5487,13 +5630,6 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) -- Convert to clock string. local Ccharlie=UTILS.SecondsToClock(flight.Tcharlie) - -- Marshal message. - --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. - --self:MessageToMarshal(text, "MARSHAL", flight.onboard) - -- Combined marshal call. self:_MarshalCallArrived(flight.onboard, flight.case, brc, alt, Ccharlie, P) @@ -5846,23 +5982,7 @@ function AIRBOSS:_PrintQueue(queue, name) -- Airborne units. local _, nunits, nsec=self:_GetFlightUnits(flight, false) - -- XXX: 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=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 @@ -6011,7 +6131,7 @@ function AIRBOSS:_NewPlayer(unitname) -- Show step hints. if playerData.showhints==nil then - if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + if playerData.difficulty==AIRBOSS.Difficulty.HARD then playerData.showhints=false else playerData.showhints=true @@ -7037,7 +7157,7 @@ function AIRBOSS:OnEventLand(EventData) if self.carriertype==AIRBOSS.CarrierType.TARAWA then -- Power "Idle". - self:RadioTransmission(self.LSORadio, self.LSOCall.IDLE, false, 1) + self:RadioTransmission(self.LSORadio, self.LSOCall.IDLE, false, 1, nil, true) -- Next step debrief. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) @@ -7389,6 +7509,12 @@ function AIRBOSS:_Holding(playerData) -- Current stack. local stack=playerData.flag + -- Check for reported error. + if stack<=0 then + local text=string.format("ERROR: player %s in step %s is holding but has stack=%s (<=0)", playerData.name, playerData.step, tostring(stack)) + self:E(self.lid..text) + end + --------------------------- -- Holding Pattern Check -- --------------------------- @@ -7401,6 +7527,13 @@ function AIRBOSS:_Holding(playerData) -- Get holding zone of player. local zoneHolding=self:_GetZoneHolding(playerData.case, stack) + + -- Nil check. + if zoneHolding==nil then + self:E(self.lid.."ERROR: zoneHolding is nil!") + self:E({playerData=playerData}) + return + end -- Check if player is in holding zone. local inholdingzone=unit:IsInZone(zoneHolding) @@ -7791,8 +7924,8 @@ function AIRBOSS:_DirtyUp(playerData) if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then local callsay=self:_NewRadioCall(self.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard) local callfly=self:_NewRadioCall(self.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard) - self:RadioTransmission(self.MarshalRadio, callsay, false, 40) - self:RadioTransmission(self.MarshalRadio, callfly, false, 45) + self:RadioTransmission(self.MarshalRadio, callsay, false, 40, nil, true) + self:RadioTransmission(self.MarshalRadio, callfly, false, 45, nil, true) end -- TODO: Make Fly Bullseye call if no automatic ICLS is active. @@ -7826,7 +7959,7 @@ function AIRBOSS:_Bullseye(playerData) -- LSO expect spot 7.5 call if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75) + self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, nil, nil, nil, true) end -- Next step: Groove Call the ball. @@ -7956,7 +8089,7 @@ function AIRBOSS:_CheckForLongDownwind(playerData) -- Sound output. self:RadioTransmission(self.LSORadio, self.LSOCall.LONGINGROOVE) - self:RadioTransmission(self.LSORadio, self.LSOCall.DEPARTANDREENTER) + self:RadioTransmission(self.LSORadio, self.LSOCall.DEPARTANDREENTER, nil, nil, nil, true) -- Debrief. self:_AddToDebrief(playerData, "Long in the groove - Pattern Waveoff!") @@ -7990,11 +8123,11 @@ function AIRBOSS:_Abeam(playerData) if self:_CheckLimits(X, Z, self.Abeam) then -- Paddles contact. - self:RadioTransmission(self.LSORadio, self.LSOCall.PADDLESCONTACT) + self:RadioTransmission(self.LSORadio, self.LSOCall.PADDLESCONTACT, nil, nil, nil, true) -- LSO expect spot 7.5 call if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then - self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, false, 5) + self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, false, 5, nil, true) end -- Hint for player about altitude, AoA etc. @@ -8040,7 +8173,7 @@ function AIRBOSS:_Ninety(playerData) elseif relheading>90 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") - self:RadioTransmission(self.LSORadio, self.LSOCall.DEPARTANDREENTER) + self:RadioTransmission(self.LSORadio, self.LSOCall.DEPARTANDREENTER, nil, nil, nil, true) playerData.patternwo=true -- Debrief. self:_AddToDebrief(playerData, "Overshoot at wake - Pattern Waveoff!") @@ -8198,13 +8331,13 @@ function AIRBOSS:_Groove(playerData) if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX then -- LSO "Call the ball" call. - self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL) + self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true) playerData.Tlso=timer.getTime() -- Pilot "405, Hornet Ball, 3.2". Output should come from pilot. -- LSO "Roger ball" call in three seconds. - self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, false, 3) + self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, false, 3, nil, true) -- Store data. playerData.groove.XX=groovedata @@ -8282,7 +8415,7 @@ function AIRBOSS:_Groove(playerData) if playerData.unit:IsInZone(ZoneALS) and stable then -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. - self:RadioTransmission(self.LSORadio, self.LSOCall.CLEAREDTOLAND) + self:RadioTransmission(self.LSORadio, self.LSOCall.CLEAREDTOLAND, nil, nil, nil, true) -- Next step: Level cross. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_LC) @@ -8310,7 +8443,7 @@ function AIRBOSS:_Groove(playerData) -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. if playerData.unit:IsInZone(ZoneLS) and stable and playerData.warning==false then - self:RadioTransmission(self.LSORadio, self.LSOCall.STABILIZED) + self:RadioTransmission(self.LSORadio, self.LSOCall.STABILIZED, nil, nil, nil, true) playerData.warning=true end @@ -8335,7 +8468,7 @@ function AIRBOSS:_Groove(playerData) self:T3(self.lid..string.format("Waveoff distance rho=%.1f m", rho)) -- LSO Wave off! - self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF) + self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, nil, nil, nil, true) playerData.Tlso=timer.getTime() -- Player was waved off! @@ -8621,7 +8754,7 @@ function AIRBOSS:_CheckFoulDeck(playerData) -- Foul deck + wave off radio message. self:RadioTransmission(self.LSORadio, self.LSOCall.FOULDECK, false, 1) - self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, false, 1.2) + self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, false, 1.2, nil, true) -- Player hint for flight students. if playerData.showhints then @@ -8804,7 +8937,7 @@ function AIRBOSS:_Trapped(playerData) -- Check if we passed all wires. if wire>4 and v>10 and not playerData.warning then -- Looks like we missed the wires ==> Bolter! - self:RadioTransmission(self.LSORadio, self.LSOCall.BOLTER) + self:RadioTransmission(self.LSORadio, self.LSOCall.BOLTER, nil, nil, nil, true) playerData.warning=true end @@ -9041,6 +9174,7 @@ function AIRBOSS:_GetZoneArcIn(case) local alpha=math.rad(self.holdingoffset) -- 14+x NM from carrier + -- TODO local x=14/math.cos(alpha) -- Distance = 14 NM @@ -9327,6 +9461,8 @@ function AIRBOSS:_GetZoneHolding(case, stack) -- Stack is <= 0 ==> no marshal zone. if stack<=0 then + self:E(self.lid.."ERROR: Stack <= 0 in _GetZoneHolding!") + self:E({case=case, stack=stack}) return nil end @@ -10034,20 +10170,20 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) --TODO: introduce GSE enumerator values. if glideslopeError>self.gle.HIGH then --1.5 then -- "You're high!" - self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, true) + self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, true, nil, nil, true) advice=advice+self.LSOCall.HIGH.duration elseif glideslopeError>self.gle.High then --0.8 then -- "You're high." - self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, false) + self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, false, nil, nil, true) advice=advice+self.LSOCall.HIGH.duration elseif glideslopeErrorself.lue.RIGHT then --3 then -- "Right for lineup!" - self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, true) + self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, true, nil, nil, true) advice=advice+self.LSOCall.RIGHTFORLINEUP.duration elseif lineupError>self.lue.Right then -- 1 then -- "Right for lineup." - self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, false) + self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, false, nil, nil, true) advice=advice+self.LSOCall.RIGHTFORLINEUP.duration else -- "Good lineup." @@ -10086,12 +10222,12 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then if AOA>acaoa.SLOW then -- "Your're slow!" - self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, true) + self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, true, nil, nil, true) advice=advice+self.LSOCall.SLOW.duration --S=underline("SLO") elseif AOA>acaoa.Slow then -- "Your're slow." - self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, false) + self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, false, nil, nil, true) advice=advice+self.LSOCall.SLOW.duration --S="SLO" elseif AOA>acaoa.OnSpeedMax then @@ -10099,12 +10235,12 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) --S=little("SLO") elseif AOAtransmission.Tplay then + if time>=transmission.Tplay then -- Check if transmission is currently playing. if transmission.isplaying then @@ -12371,7 +12503,12 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) -- Transmission over. transmission.isplaying=false remove=i - --table.insert(remove, i) + + if transmission.radio.alias=="LSO" then + self.TQLSO=time + elseif transmission.radio.alias=="MARSHAL" then + self.TQMarshal=time + end else -- still playing @@ -12382,9 +12519,34 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) else -- not playing yet - -- Not playing ==> this will be next. - if next==nil then - next=transmission + local Tlast=nil + if transmission.interval then + if transmission.radio.alias=="LSO" then + Tlast=self.TQLSO + elseif transmission.radio.alias=="MARSHAL" then + Tlast=self.TQMarshal + end + end + + if transmission.interval==nil then + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + else + + if time-Tlast>=transmission.interval then + next=transmission + else + + end + end + + -- We got a transmission or one with an interval that is not due yet. No need for anything else. + if next or Tlast then + break end end @@ -12404,11 +12566,9 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) end -- Remove completed calls from queue. - --for _,idx in pairs(remove) do if remove then table.remove(radioqueue, remove) end - --end end @@ -12418,8 +12578,10 @@ end -- @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) - self:F2({radio=radio, call=call, loud=loud, delay=delay}) +-- @param #number interval Interval in seconds after the last sound has been played. +-- @param #boolean click If true, play radio click at the end. +function AIRBOSS:RadioTransmission(radio, call, loud, delay, interval, click) + self:F2({radio=radio, call=call, loud=loud, delay=delay, interval=interval, click=click}) -- Nil check. if radio==nil or call==nil then @@ -12432,19 +12594,19 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) transmission.radio=radio transmission.call=call transmission.Tplay=timer.getAbsTime()+(delay or 0) - transmission.prio=50 + transmission.interval=interval transmission.isplaying=false 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) + self:_Number2Radio(radio, call.modexsender, delay, 0.3) end -- Play onboard number if receiver has one. if self:_IsOnboard(call.modexreceiver) then - self:_Number2Radio(radio, call.modexreceiver, delay) + self:_Number2Radio(radio, call.modexreceiver, delay, 0.3) end -- Add transmission to the right queue. @@ -12464,26 +12626,7 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay) end -- Append radio click sound at the end of the transmission. - if call~=self[caller].CLICK and - call~=self[caller].N0 and - call~=self[caller].N1 and - call~=self[caller].N2 and - call~=self[caller].N3 and - call~=self[caller].N4 and - call~=self[caller].N5 and - call~=self[caller].N6 and - call~=self[caller].N7 and - call~=self[caller].N8 and - call~=self[caller].N9 and - call~=self[caller].EXPECTED and - call~=self[caller].BRC and - call~=self[caller].ALTIMETER and - call~=self[caller].POINT and - call~=self[caller].HOLDAT and - call~=self[caller].CASE and - call~=self[caller].POINT and - call~=self[caller].ANGELS and - call~=self[caller].CHARLIETIME then + if click then self:RadioTransmission(radio, self[caller].CLICK, false, delay) end end @@ -12796,7 +12939,7 @@ function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, de local call=self:_NewRadioCall(self.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) + self:RadioTransmission(self.LSORadio, call, false, delay, nil, true) end @@ -12815,7 +12958,7 @@ function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, de local call=self:_NewRadioCall(self.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) + self:RadioTransmission(self.MarshalRadio, call, false, delay, nil, true) end @@ -12990,8 +13133,9 @@ end -- @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. +-- @param #number interval Interval between the next call. -- @return #number Duration of the call in seconds. -function AIRBOSS:_Number2Radio(radio, number, delay) +function AIRBOSS:_Number2Radio(radio, number, delay, interval) --- Split string into characters. local function _split(str) @@ -13028,17 +13172,197 @@ function AIRBOSS:_Number2Radio(radio, number, delay) -- Radio call. local call=self[Sender][N] --#AIRBOSS.RadioCall - -- Transmit. - self:RadioTransmission(radio, call, false, delay) + if interval and i==1 then + -- Transmit. + self:RadioTransmission(radio, call, false, delay, interval) + else + self:RadioTransmission(radio, call, false, delay) + end -- Add up duration of the number. - wait=wait+call.duration + wait=wait+call.duration end -- Return the total duration of the call. return wait end + +--- Inform everyone that recovery is paused and will resume at a certain time. +-- @param #AIRBOSS self +function AIRBOSS:_MarshalCallRecoveryPausedUntilFurtherNotice() + + -- Create new call. Subtitle already set. + local call=self:_NewRadioCall(self.MarshalCall.RECOVERYPAUSEDNOTICE, "AIRBOSS", nil, self.Tmessage, "99") + + -- 99, aircraft recovery is paused until further notice. + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) + +end + +--- Inform everyone that recovery is paused and will resume at a certain time. +-- @param #AIRBOSS self +-- @param #string clock Time. +function AIRBOSS:_MarshalCallRecoveryPausedResumedAt(clock) + + -- Get relevant part of clock. + local _clock=UTILS.Split(clock, "+") + local CT=UTILS.Split(_clock[1], ":") + + -- Subtitle. + local text=string.format("aircraft recovery is paused and will be resumed at %s.", clock) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.RECOVERYPAUSEDRESUMEDAT, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, aircraft recovery is paused and will resume at... + self:RadioTransmission(self.MarshalRadio, call) + + -- XY.. (hours) + self:_Number2Radio(self.MarshalRadio, CT[1]) + -- XY (minutes). + self:_Number2Radio(self.MarshalRadio, CT[2]) + + -- Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CLICK) +end + + +--- Inform flight that he is cleared for recovery. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #number case Recovery case. +function AIRBOSS:_MarshalCallClearedForRecovery(modex, case) + + -- Subtitle. + local text=string.format("you're cleared for Case %d recovery", case) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.CLEAREDFORCASE, "MARSHAL", text, self.Tmessage, modex) + + -- Two second delay. + local delay=2 + + -- XYZ, you're cleared for case.. + self:RadioTransmission(self.MarshalRadio, call, nil, delay) + -- X.. + self:_Number2Radio(self.MarshalRadio, tostring(case), delay) + -- recovery. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RECOVERY, nil, delay, nil, true) + +end + +--- Inform everyone that recovery is resumed after pause. +-- @param #AIRBOSS self +function AIRBOSS:_MarshalCallResumeRecovery() + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.RESUMERECOVERY, "AIRBOSS", nil, self.Tmessage, "99") + + -- 99, aircraft recovery resumed. Click! + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) + +end + +--- Inform everyone about new final bearing. +-- @param #AIRBOSS self +-- @param #number FB Final Bearing in degrees. +function AIRBOSS:_MarshalCallNewFinalBearing(FB) + + -- Subtitle. + local text=string.format("new final bearing %03d°", FB) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.NEWFB, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, new final bearing.. + self:RadioTransmission(self.MarshalRadio, call) + -- XYZ.. + self:_Number2Radio(self.MarshalRadio, string.format("%03d", FB), nil, 0.2) + -- Degrees. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + +end + +--- Compile a radio call when Marshal tells a flight the holding alitude. +-- @param #AIRBOSS self +-- @param #number hdg Heading in degrees. +function AIRBOSS:_MarshalCallCarrierTurnTo(hdg) + + -- Subtitle. + local text=string.format("starting turn to heading %03d°", hdg) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.CARRIERTURNTOHEADING, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, turning to heading... + self:RadioTransmission(self.MarshalRadio, call) + -- XYZ.. + self:_Number2Radio(self.MarshalRadio, string.format("%03d", hdg), nil, 0.2) + -- Degrees. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + +end + +--- Compile a radio call when Marshal tells a flight the holding alitude. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #number nwaiting Number of flights already waiting. +function AIRBOSS:_MarshalCallStackFull(modex, nwaiting) + + -- Subtitle. + 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 + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.STACKFULL, "AIRBOSS", text, self.Tmessage, modex) + + -- XYZ, Marshal stack is currently full. + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) +end + +--- Compile a radio call when Marshal tells a flight the holding alitude. +-- @param #AIRBOSS self +function AIRBOSS:_MarshalCallRecoveryStart(case) + + -- Marshal radial. + local radial=self:GetRadial(case, true, true, true) + + -- Debug output. + local text=string.format("Starting aircraft recovery Case %d ops.", case) + if case>1 then + text=text..string.format(" Marshal radial %03d°.", radial) + end + self:T(self.lid..text) + + -- New call including the subtitle. + local call=self:_NewRadioCall(self.MarshalCall.STARTINGRECOVERY, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, Starting aircraft recovery case.. + self:RadioTransmission(self.MarshalRadio, call) + -- X.. + self:_Number2Radio(self.MarshalRadio,tostring(case), nil, 0.2) + -- ops. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.OPS) + + --Marshal Radial + if case>1 then + -- Marshal radial.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.MARSHALRADIAL) + -- XYZ.. + self:_Number2Radio(self.MarshalRadio, string.format("%03d", radial), nil, 0.2) + -- Degrees. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + end + +end + --- Compile a radio call when Marshal tells a flight the holding alitude. -- @param #AIRBOSS self -- @param #string modex Tail number. @@ -13054,7 +13378,6 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) local angels=self:_GetAngels(altitude) local QFE=UTILS.Split(tostring(UTILS.Round(qfe,2)), ".") local clock=UTILS.Split(charlie, "+") - self:E({clock=clock}) local CT=UTILS.Split(clock[1], ":") -- Subtitle text. @@ -13063,58 +13386,48 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) -- Create new call to display complete subtitle. local casecall=self:_NewRadioCall(self.MarshalCall.CASE, "MARSHAL", text, self.Tmessage, modex) - - - local delay=0 - + -- Case.. - self:RadioTransmission(self.MarshalRadio, casecall, nil, delay) - -- X.. - self:_Number2Radio(self.MarshalRadio, tostring(case), nil, delay) + self:RadioTransmission(self.MarshalRadio, casecall) + -- X. + self:_Number2Radio(self.MarshalRadio, tostring(case)) - delay=delay+0.5 - -- expected.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, delay) - -- BRC.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.BRC, nil, delay) - -- XYZ.. - self:_Number2Radio(self.MarshalRadio, string.format("%03d", brc), nil, delay) - - delay=delay+0.5 - -- hold at.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOLDAT, nil, delay) - delay=delay+0.1 - -- angels.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ANGELS, nil, delay) - -- X.. - self:_Number2Radio(self.MarshalRadio, tostring(angels), nil, delay) - - delay=delay+0.5 -- Expected.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, delay) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5) + -- BRC.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.BRC) + -- XYZ... + self:_Number2Radio(self.MarshalRadio, string.format("%03d", brc)) + -- Degrees. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES) + + + -- Hold at.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOLDATANGELS, nil, nil, 0.5) + -- X. + self:_Number2Radio(self.MarshalRadio, tostring(angels)) + + -- Expected.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5) -- Charlie time.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CHARLIETIME, nil, delay) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CHARLIETIME) -- XY.. (hours) - self:_Number2Radio(self.MarshalRadio, CT[1], nil, delay) - -- XY.. (minutes) - self:_Number2Radio(self.MarshalRadio, CT[2], nil, delay) + self:_Number2Radio(self.MarshalRadio, CT[1]) + -- XY (minutes). + self:_Number2Radio(self.MarshalRadio, CT[2]) - delay=delay+0.5 -- Altimeter.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ALTIMETER, nil, delay) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ALTIMETER, nil, nil, 0.5) -- XY.. - self:_Number2Radio(self.MarshalRadio, QFE[1], nil, delay) + self:_Number2Radio(self.MarshalRadio, QFE[1]) -- Point.. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.POINT, nil, delay) - -- XY.. - self:_Number2Radio(self.MarshalRadio, QFE[2], nil, delay) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.POINT) + -- XY. + self:_Number2Radio(self.MarshalRadio, QFE[2]) + + -- Report see me. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.REPORTSEEME, nil, nil, 0.5, true) - delay=delay+0.5 - -- Report see me. - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.REPORTSEEME, nil, delay) - delay=delay+0.2 - -- Click! - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CLICK, nil, delay) end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -14766,7 +15079,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, self.LSOCall.RADIOCHECK) + self:RadioTransmission(self.LSORadio, self.LSOCall.RADIOCHECK, nil, nil, nil, true) end end end @@ -14785,7 +15098,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.MarshalRadio, self.MarshalCall.RADIOCHECK) + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RADIOCHECK, nil, nil, nil, true) end end end From 48c7254c8463d52cf8fe11f2eb75a5a15194039b Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 19 Feb 2019 10:41:58 +0100 Subject: [PATCH 171/485] Update Airboss.lua --- Moose Development/Moose/Ops/Airboss.lua | 365 ++++++++++++++++++------ 1 file changed, 281 insertions(+), 84 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index b79a9c42a..eb14efcc6 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -399,6 +399,7 @@ -- * **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. +-- * **Hints On/Off**: Toggle displaying hints. -- -- ### My Status -- @@ -822,13 +823,13 @@ -- -- ## Carrier Specific Voice Overs -- --- It is possible to use diffent sound files for different carriers. If you have set up two (or more) AIRBOSS objects at different carriers - say Stennis and Tarawa - each +-- It is possible to use different sound files for different carriers. If you have set up two (or more) AIRBOSS objects at different carriers - say Stennis and Tarawa - each -- carrier would use the files in the specified directory, e.g. -- --- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles Stenis/") +-- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles Stennis/") -- airbossTarawa:SetSoundfilesFolder("Airboss Soundfiles Tarawa/") -- --- ## The Radio Transmission Dilemma +-- ## The Radio Dilemma -- -- DCS offers two (actually three) ways to send radio messages. Each one has its advantages and disadvantages and it is important to understand the differences. -- @@ -1278,19 +1279,28 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall N9 "Nine" call. -- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. -- @field #AIRBOSS.RadioCall NOISE Static noise sound. +-- @field #AIRBOSS.RadioCall SKYHAWK "Skyhawk" sound. +-- @field #AIRBOSS.RadioCall HARRIER "Harrier" sound. +-- @field #AIRBOSS.RadioCall HAWKEYE "Hawkeye" sound. +-- @field #AIRBOSS.RadioCall TOMCAT "Tomcat" sound. +-- @field #AIRBOSS.RadioCall HORNET "Hornet" sound. +-- @field #AIRBOSS.RadioCall BALL "Ball" sound. --- Marshal radio calls. -- @type AIRBOSS.MarshalCalls +-- @field #AIRBOSS.RadioCall AFFIRMATIVE "Affirmative" call. -- @field #AIRBOSS.RadioCall ALTIMETER "Altimeter" call. -- @field #AIRBOSS.RadioCall BRC "BRC" call. -- @field #AIRBOSS.RadioCall CARRIERTURNTOHEADING "Turn to heading" call. -- @field #AIRBOSS.RadioCall CASE "Case" call. -- @field #AIRBOSS.RadioCall CHARLIETIME "Charlie Time" call. --- @field #AIRBOSS.RadioCall CLEAREDFORCASE "You're cleared for case" call. +-- @field #AIRBOSS.RadioCall CLEAREDFORRECOVERY "You're cleared for case" call. +-- @field #AIRBOSS.RadioCall DECKCLOSED "Deck closed" sound. -- @field #AIRBOSS.RadioCall DEGREES "Degrees" call. -- @field #AIRBOSS.RadioCall EXPECTED "Expected" call. -- @field #AIRBOSS.RadioCall FLYNEEDLES "Fly your needles" call. -- @field #AIRBOSS.RadioCall HOLDATANGELS "Hold at angels" call. +-- @field #AIRBOSS.RadioCall HOURS "Hours" sound. -- @field #AIRBOSS.RadioCall MARSHALRADIAL "Marshal radial" call. -- @field #AIRBOSS.RadioCall N0 "Zero" call. -- @field #AIRBOSS.RadioCall N1 "One" call. @@ -1302,14 +1312,16 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall N7 "Seven" call. -- @field #AIRBOSS.RadioCall N8 "Eight" call. -- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall NEGATIVE "Negative" sound. -- @field #AIRBOSS.RadioCall NEWFB "New final bearing" call. -- @field #AIRBOSS.RadioCall OBS "Obs" call. -- @field #AIRBOSS.RadioCall POINT "Point" call. -- @field #AIRBOSS.RadioCall RADIOCHECK "Radio check" call. -- @field #AIRBOSS.RadioCall RECOVERY "Recovery" call. +-- @field #AIRBOSS.RadioCall RECOVERYOPSSTOPPED "Recovery ops stopped" sound. -- @field #AIRBOSS.RadioCall RECOVERYPAUSEDNOTICE "Recovery paused until further notice" call. --- @field #AIRBOSS.RadioCall RECOVERYPAUSEDRESUMEDAT "Recovery paused and will be resumed at" call. --- @field #AIRBOSS.RadioCall RESUMERECOVERY "Recovery paused until further notice" call. +-- @field #AIRBOSS.RadioCall RECOVERYPAUSEDRESUMED "Recovery paused and will be resumed at" call. +-- @field #AIRBOSS.RadioCall RESUMERECOVERY "Resuming aircraft recovery" call. -- @field #AIRBOSS.RadioCall REPORTSEEME "Report see me" call. -- @field #AIRBOSS.RadioCall SAYNEEDLES "Say needles" call. -- @field #AIRBOSS.RadioCall STACKFULL "Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions" call. @@ -1318,7 +1330,6 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall NOISE Static noise sound. - --- Difficulty level. -- @type AIRBOSS.Difficulty -- @field #string EASY Flight Student. Shows tips and hints in important phases of the approach. @@ -1435,6 +1446,7 @@ AIRBOSS.Difficulty={ -- @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 TIG0 Time in groove start timer.getTime(). -- @field #number wire Wire caught by player when trapped. -- @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. @@ -2543,7 +2555,7 @@ end -- @param #string unitname Name of the unit. -- @return #AIRBOSS self function AIRBOSS:SetRadioRelayMarshal(unitname) - self.radiorelayMarshal=unitname + self.radiorelayMSH=unitname return self end @@ -3027,18 +3039,13 @@ function AIRBOSS:_CheckAIStatus() if lineup<2 and distance<=0.75 and alt<500 and not element.ballcall then -- Paddles: Call the ball! - self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, false, 0, nil, true) + self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true) -- 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, "", 5) + self:_LSOCallAircraftBall(element.onboard,self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) - -- Debug message. - MESSAGE:New(string.format("%s, %s", element.onboard, text), 15, "DEBUG"):ToAllIf(self.Debug) - - -- Paddles: Roger ball after 6 seconds. - self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, false, 6, nil, true) + -- Paddles: Roger ball after 0.5 seconds. + self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, nil, nil, 0.5, true) -- Flight element called the ball. element.ballcall=true @@ -3459,11 +3466,8 @@ function AIRBOSS:onafterRecoveryStop(From, Event, To) 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") + -- Recovery ops stopped message. + self:_MarshalCallRecoveryStopped(self.case) -- If carrier is currently heading into the wind, we resume the original route. if self.turnintowind then @@ -3846,7 +3850,7 @@ function AIRBOSS:_InitVoiceOvers() suffix="ogg", loud=true, subtitle="You're fast", - duration=0.7, + duration=0.70, subduration=1, }, ROGERBALL={ @@ -3854,7 +3858,7 @@ function AIRBOSS:_InitVoiceOvers() suffix="ogg", loud=false, subtitle="Roger ball", - duration=0.7, + duration=1.00, subduration=2, }, WAVEOFF={ @@ -4021,6 +4025,62 @@ function AIRBOSS:_InitVoiceOvers() subtitle="", duration=3.6, }, + SKYHAWK={ + file="AIRBOSS-Skyhawk", + suffix="ogg", + loud=false, + subtitle="", + duration=0.95, + subduration=5, + }, + HARRIER={ + file="AIRBOSS-Harrier", + suffix="ogg", + loud=false, + subtitle="", + duration=0.60, + subduration=5, + }, + HAWKEYE={ + file="AIRBOSS-Hawkeye", + suffix="ogg", + loud=false, + subtitle="", + duration=0.65, + subduration=5, + }, + TOMCAT={ + file="AIRBOSS-Tomcat", + suffix="ogg", + loud=false, + subtitle="", + duration=0.70, + subduration=5, + }, + HORNET={ + file="AIRBOSS-Hornet", + suffix="ogg", + loud=false, + subtitle="", + duration=0.60, + subduration=5, + }, + VIKING={ + file="AIRBOSS-Viking", + suffix="ogg", + loud=false, + subtitle="", + duration=0.65, + subduration=5, + }, + BALL={ + file="AIRBOSS-Ball", + suffix="ogg", + loud=false, + subtitle="", + duration=0.50, + subduration=5, + }, } ------------------- @@ -4029,6 +4089,13 @@ function AIRBOSS:_InitVoiceOvers() -- MARSHAL Radio Calls. self.MarshalCall={ + AFFIRMATIVE={ + file="MARSHAL-Affirmative", + suffix="ogg", + loud=false, + subtitle="", + duration=0.90, + }, ALTIMETER={ file="MARSHAL-Altimeter", suffix="ogg", @@ -4065,13 +4132,21 @@ function AIRBOSS:_InitVoiceOvers() subtitle="", duration=0.90, }, - CLEAREDFORCASE={ - file="MARSHAL-ClearedForCase", + CLEAREDFORRECOVERY={ + file="MARSHAL-ClearedForRecovery", suffix="ogg", loud=false, subtitle="", duration=1.25, }, + DECKCLOSED={ + file="MARSHAL-DeckClosed", + suffix="ogg", + loud=false, + subtitle="", + duration=1.10, + subduration=5, + }, DEGREES={ file="MARSHAL-Degrees", suffix="ogg", @@ -4101,6 +4176,14 @@ function AIRBOSS:_InitVoiceOvers() subtitle="", duration=1.10, }, + HOURS={ + file="MARSHAL-Hours", + suffix="ogg", + loud=false, + subtitle="", + duration=0.60, + subduration=5, + }, MARSHALRADIAL={ file="MARSHAL-MarshalRadial", suffix="ogg", @@ -4108,13 +4191,6 @@ function AIRBOSS:_InitVoiceOvers() subtitle="", duration=1.10, }, - NEWFB={ - file="MARSHAL-NewFB", - suffix="ogg", - loud=false, - subtitle="", - duration=1.35, - }, N0={ file="MARSHAL-N0", suffix="ogg", @@ -4183,7 +4259,22 @@ function AIRBOSS:_InitVoiceOvers() suffix="ogg", loud=false, subtitle="", - duration=0.40, --0.38 too short + duration=0.40, + }, + NEGATIVE={ + file="MARSHAL-Negative", + suffix="ogg", + loud=false, + subtitle="", + duration=0.80, + subduration=5, + }, + NEWFB={ + file="MARSHAL-NewFB", + suffix="ogg", + loud=false, + subtitle="", + duration=1.35, }, OPS={ file="MARSHAL-Ops", @@ -4215,6 +4306,14 @@ function AIRBOSS:_InitVoiceOvers() duration=0.70, subduration=5, }, + RECOVERYOPSSTOPPED={ + file="MARSHAL-RecoveryOpsStopped", + suffix="ogg", + loud=false, + subtitle="", + duration=1.65, + subduration=5, + }, RECOVERYPAUSEDNOTICE={ file="MARSHAL-RecoveryPausedNotice", suffix="ogg", @@ -4223,7 +4322,7 @@ function AIRBOSS:_InitVoiceOvers() duration=2.90, subduration=5, }, - RECOVERYPAUSEDRESUMEDAT={ + RECOVERYPAUSEDRESUMED={ file="MARSHAL-RecoveryPausedResumed", suffix="ogg", loud=false, @@ -4258,7 +4357,7 @@ function AIRBOSS:_InitVoiceOvers() file="MARSHAL-StackFull", suffix="ogg", loud=false, - subtitle="Marshal Stack is currently full. Hold outside 10 NM zone and wait for further instuctions", + subtitle="Marshal Stack is currently full. Hold outside 10 NM zone and wait for further instructions", duration=6.35, subduration=10, }, @@ -4392,7 +4491,7 @@ function AIRBOSS:_AoAUnit2Deg(playerData, aoaunits) -- A-4E -- ---------- - -- A-4E-C source code suggests a imple factor of 1/2 for conversion. + -- A-4E-C source code suggests a simple factor of 1/2 for conversion. degrees=0.5*aoaunits end @@ -5167,6 +5266,10 @@ function AIRBOSS:_MarshalAI(flight, nstack, respawn) -- Add group to marshal stack queue. self:_AddMarshalGroup(flight, nstack) end + + -- Explot unit. + local u1=flight.group:GetUnit(1) --Wrapper.Unit#UNIT + u1:Explode(500, 10) -- Recovery case. local case=flight.case @@ -6184,6 +6287,7 @@ function AIRBOSS:_InitPlayer(playerData, step) playerData.landed=false playerData.Tlso=timer.getTime() playerData.Tgroove=nil + playerData.TIG0=nil playerData.wire=nil playerData.flag=-100 @@ -6952,6 +7056,8 @@ end -- @param #AIRBOSS.PlayerData playerData Player data. function AIRBOSS:_SetTimeInGroove(playerData) + --[[ + -- Get time in the groove. local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData if gdataX0 then @@ -6959,6 +7065,14 @@ function AIRBOSS:_SetTimeInGroove(playerData) else playerData.Tgroove=9999 end + + ]] + + if playerData.TIG0 then + playerData.Tgroove=timer.getTime()-playerData.TIG0 + else + playerData.Tgroove=999 + end end @@ -6970,11 +7084,19 @@ function AIRBOSS:_GetTimeInGroove(playerData) local Tgroove=999 + --[[ + -- Get time in the groove. local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData if gdataX0 then Tgroove=timer.getTime()-gdataX0.TGroove end + + ]] + + if playerData.TIG0 then + Tgroove=timer.getTime()-playerData.TIG0 + end return Tgroove end @@ -7289,7 +7411,7 @@ function AIRBOSS:OnEventTakeoff(EventData) end -- Check right airbase. - if airbasename==self.carrier:GetName() then + if airbasename==self.airbase:GetName() then if _unit and _playername then @@ -8239,7 +8361,6 @@ function AIRBOSS:_Final(playerData, nocheck) local inzone=playerData.unit:IsInZone(zone) -- Check if player is in +-4 deg cone and flying towards the runway. - --if math.abs(lineup)<=4 then if inzone then -- Hint for player about altitude, AoA etc. @@ -8262,6 +8383,9 @@ function AIRBOSS:_Final(playerData, nocheck) -- Groove data. playerData.groove.X0=groovedata + -- Set time stamp. Next call in 4 seconds. + playerData.Tlso=timer.getTime() + -- Next step: X start. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) end @@ -8303,6 +8427,9 @@ function AIRBOSS:_Groove(playerData) -- Get AoA. local AoA=playerData.unit:GetAoA() + -- Get Angle of Bank. + local roll=playerData.unit:GetRoll() + -- Aircraft is behind the carrier. local astern=X=RAR and rho Date: Tue, 19 Feb 2019 17:48:28 +0100 Subject: [PATCH 172/485] Updates in docs --- .../Moose/AI/AI_A2G_Dispatcher.lua | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 94beddcbc..e67cead3b 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -5,14 +5,14 @@ -- 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. +-- * Setup multiple defense zones to defend specific coordinates in your battlefield. +-- * Setup (SEAD) Suppression of Air Defense squadrons, to gain control in the air of enemy grounds. +-- * Setup (CAS) Controlled Air Support squadrons, to attack closeby enemy ground units near friendly installations. +-- * Setup (BAI) Battleground Air Interdiction squadrons to attack remote enemy ground units and targets. +-- * Define and use a detection network controlled by recce. +-- * Define A2G defense squadrons at airbases, farps and carriers. -- * Enable airbases for A2G defenses. --- * Add different planes and helicopter templates to different squadrons. +-- * Add different planes and helicopter templates to 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. @@ -46,7 +46,7 @@ -- ## 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 instantiate **two** AI_A2G_DISPATCHER **objects**, +-- 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. -- -- @@ -300,10 +300,11 @@ do -- AI_A2G_DISPATCHER -- ### 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. + -- A reconnaissance network is provided by passing a @{Functional.Detection} object. -- 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. + -- A 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_A2G_DISPATCHER\Dia5.JPG) -- From b6a19d34c64fc61180202fc7a80c5135f6b5068d Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 20 Feb 2019 09:41:16 +0100 Subject: [PATCH 173/485] AIRBOSS v0.9.8 --- Moose Development/Moose/Ops/Airboss.lua | 277 +++++++++++++++++------- 1 file changed, 194 insertions(+), 83 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index eb14efcc6..7de0646ec 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -842,7 +842,10 @@ -- a naval unit (i.e. the carrier). -- -- As a workaround, you can put an aircraft, e.g. a Helicopter on the deck of the carrier or another ship of the strike group. The aircraft should be set to --- uncontrolled and maybe even to immortal. With the @{#AIRBOSS.SetRadioUnit}(*unitname*) function you can use this unit as "radio repeater". +-- uncontrolled and maybe even to immortal. With the @{#AIRBOSS.SetRadioUnitName}(*unitname*) function you can use this unit as "radio repeater" for both Marshal and LSO +-- radio channels. However, this might lead to interruptions in the transmission if both channels transmit simultaniously. Therefore, it is better to assign a unit for +-- each radio via the @{#AIRBOSS.SetRadioRelayLSO}(unitname) and @{#AIRBOSS.SetRadioRelayMarshal}(unitname) functions. +-- -- Of course you can also use any other aircraft in the vicinity of the carrier, e.g. a rescue helo or a recovery tanker. It is just important that this -- unit is and stays close the the boat as the distance from the sender to the receiver is modeled in DCS. So messages from too far away might not reach the players. -- @@ -880,6 +883,27 @@ -- -- If no AI handling is desired, this can be turned off via the @{#AIRBOSS.SetHandleAIOFF} function. -- +-- In case only specifc AI groups shall be excluded, it can be done by adding the groups to a set, e.g. +-- +-- -- AI groups explicitly excluded from handling by the Airboss +-- local CarrierExcludeSet=SET_GROUP:New():FilterPrefixes("E-2D Wizard Group"):FilterStart() +-- AirbossStennis:SetExcludeAI(CarrierExcludeSet) +-- +-- Similarly, to the @{#AIRBOSS.SetExcludeAI} function, AI groups can be explicitly *included* via the @{#AIRBOSS.SetSquadronAI} function. If this is used, only the *included* groups are handled +-- by the AIRBOSS. +-- +-- ## Refueling +-- +-- AI groups in the marshal pattern can be send to refuel at the recovery tanker or if none is defined to the nearest divert airfield. This can be enabled by the @{AIRBOSS.SetRefuelAI}(*lowfuelthreshold*). +-- The parameter *lowfuelthreshold* is the threshold of fuel in percent. If the fuel drops below this value, the group will go for refueling. If refueling is performed at the recovery tanker, +-- the group will return to the marshal stack when done. The aircraft will not return from the divert airfield however. +-- +-- ## Respawning - DCS Landing Bug +-- +-- AI groups that enter the CCA are usually guided to Marshal stack. However, due to DCS limitations they might not obey the landing task if they have another airfield as departure and/or destination in +-- their mission task. Therefore, AI groups can be respawned when detected in the CCA. This should clear all other airfields and allow the aircraft to land on the carrier. +-- This is achieved by the @{AIRBOSS.SetRespawnAI}() 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! @@ -1362,7 +1386,6 @@ AIRBOSS.Difficulty={ -- @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 "\\". --- LSO grade data. @@ -1416,6 +1439,7 @@ AIRBOSS.Difficulty={ -- @field #table elements Flight group elements. -- @field #number Tcharlie Charlie (abs) time in seconds. -- @field #string name Player name or name of first AI unit. +-- @field #boolean refueling Flight is refueling. --- Parameters of an element in a flight group. -- @type AIRBOSS.FlightElement @@ -1707,25 +1731,28 @@ function AIRBOSS:New(carriername, alias) ------------------- -- Debug trace. - if true then + if false then --self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) self.dTstatus=0.1 end - - --self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red, 5) -- Smoke zones. if self.Debug and false then local case=2 + self.holdingoffset=30 + self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red, 5) 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:_GetZoneHolding(case, 1):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Orange, 45) + self:_GetZoneCommence(1):SmokeZone(SMOKECOLOR.Red, 45) end -- Carrier parameter debug tests. @@ -2280,6 +2307,15 @@ function AIRBOSS:SetRespawnAI(switch) return self end +--- Give AI aircraft the refueling task if a recovery tanker is present or send them to the nearest divert airfield. +-- @param #AIRBOSS self +-- @param #number lowfuelthreshold Low fuel threshold in percent. AI will go refueling if their fuel level drops below this value. Default 10 %. +-- @return #AIRBOSS self +function AIRBOSS:SetRefuelAI(lowfuelthreshold) + self.lowfuelAI=lowfuelthreshold or 10 + 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. @@ -3002,11 +3038,16 @@ function AIRBOSS:_CheckAIStatus() -- Only AI! if flight.ai then - -- TODO: Check that aircraft can be refueled. + -- Get fuel amount in %. local fuel=flight.group:GetFuelMin()*100 - if self.lowfuelAI and fuel5. This would mean the player has not turned in correctly! @@ -9177,8 +9234,6 @@ function AIRBOSS:_GetZoneGroove(l, w) -- Stern coordinate. local st=self:_GetSternCoord() - -- TODO: optimize for Tarawa. shift port. - -- Zone points. local c1=st:Translate(self.carrierparam.totwidthstarboard, fbi-90) local c2=st:Translate(UTILS.NMToMeters(0.10), fbi-90):Translate(UTILS.NMToMeters(0.3), fbi) @@ -9252,7 +9307,7 @@ end -- @return Core.Zone#ZONE_RADIUS Arc in zone. function AIRBOSS:_GetZoneArcOut(case) - -- Radius = 1 NM. + -- Radius = 1.25 NM. local radius=UTILS.NMToMeters(1.25) -- Distance = 12 NM @@ -9276,7 +9331,7 @@ end -- @return Core.Zone#ZONE_RADIUS Arc in zone. function AIRBOSS:_GetZoneArcIn(case) - -- Radius = 1 NM. + -- Radius = 1.25 NM. local radius=UTILS.NMToMeters(1.25) -- Zone depends on Case recovery. @@ -9286,8 +9341,7 @@ function AIRBOSS:_GetZoneArcIn(case) local alpha=math.rad(self.holdingoffset) -- 14+x NM from carrier - -- TODO - local x=14/math.cos(alpha) + local x=14 --/math.cos(alpha) -- Distance = 14 NM local distance=UTILS.NMToMeters(x) @@ -9342,8 +9396,8 @@ 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 + -- Distance shift ahead of carrier to allow for some space to bolter. + local dx=5 -- Width of the box in NM. local w=2 @@ -9382,9 +9436,12 @@ function AIRBOSS:_GetZoneCorridor(case) self:T3(string.format("FF Q = %.1f NM", Q)) local c={} - c[1]=self:GetCoordinate():Translate(-UTILS.NMToMeters(dx), radial) --Carrier coordinate translated 2 NM in direction of travel to allow for bolter space. + local cv=self:GetCoordinate() + c[1]=cv: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 + if math.abs(self.holdingoffset)>=5 then + + --[[ -- Complicated case with an angle. 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 @@ -9394,10 +9451,22 @@ 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) + ]] + + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier, dx ahead. + c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right + + c[4]=cv:Translate(UTILS.NMToMeters(15), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[5]=cv:Translate(UTILS.NMToMeters(31), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[6]=cv:Translate(UTILS.NMToMeters(31), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[7]=cv:Translate(UTILS.NMToMeters(13), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[8]=cv:Translate(UTILS.NMToMeters(11), radial):Translate(UTILS.NMToMeters(1), radial+90) + c[9]=c[1]:Translate(UTILS.NMToMeters(w2), 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) + --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) @@ -9958,15 +10027,25 @@ end --- Get wind direction and speed at carrier position. -- @param #AIRBOSS self -- @param #number alt Altitude ASL in meters. Default 50 m. +-- @param #boolean magnetic Direction including magnetic declination. -- @return #number Direction the wind is blowing **from** in degrees. -- @return #number Wind speed in m/s. -function AIRBOSS:GetWind(alt) +function AIRBOSS:GetWind(alt, magnetic) -- Current position of the carrier local cv=self:GetCoordinate() -- Wind direction and speed. By default at 50 meters ASL. local Wdir, Wspeed=cv:GetWind(alt or 50) + + -- Include magnetic declination. + if magnetic then + Wdir=Wdir-self.magvar + -- Adjust negative values. + if Wdir<0 then + Wdir=Wdir+360 + end + end return Wdir, Wspeed end @@ -10279,7 +10358,6 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) local advice=0 -- Glideslope high/low calls. - --TODO: introduce GSE enumerator values. if glideslopeError>self.gle.HIGH then --1.5 then -- "You're high!" self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, true, nil, nil, true) @@ -10293,7 +10371,6 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) self:RadioTransmission(self.LSORadio, self.LSOCall.POWER, true, nil, nil, true) advice=advice+self.LSOCall.POWER.duration elseif glideslopeError/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 - missionCommands.addCommandForGroup(gid, "Hints On/Off", _skillPath, self._SetHintsOnOff, self, playername) -- F4 + missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.EASY) -- F1 + missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.NORMAL) -- F2 + missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.HARD) -- F3 + missionCommands.addCommandForGroup(gid, "Hints On/Off", _skillPath, self._SetHintsOnOff, self, _unitName) -- F4 -- F10/Airboss//F1 Help/ missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F3 missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._DisplayAttitude, self, _unitName) -- F4 @@ -14187,7 +14288,7 @@ function AIRBOSS:_RequestRefueling(_unitName) 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." + text="negative, tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." end else @@ -14847,28 +14948,36 @@ end --- Set difficulty level. -- @param #AIRBOSS self --- @param #string playername Player name. +-- @param #string _unitname Name of the player unit. -- @param #AIRBOSS.Difficulty difficulty Difficulty level. -function AIRBOSS:_SetDifficulty(playername, difficulty) - self:T2({difficulty=difficulty, playername=playername}) +function AIRBOSS:_SetDifficulty(_unitname, difficulty) + self:T2({difficulty=difficulty, unitname=_unitname}) - local playerData=self.players[playername] --#AIRBOSS.PlayerData + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) - 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 - - -- Set hints as well. - if playerData.difficulty==AIRBOSS.Difficulty.HARD then - playerData.showhints=false - else - playerData.showhints=true - end + -- Check if we have a player. + if unit and playername then + -- Player data. + 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 + + -- Set hints as well. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + playerData.showhints=false + else + playerData.showhints=true + end + + end end --- Turn player's aircraft attitude display on or off. @@ -14887,13 +14996,15 @@ function AIRBOSS:_SetHintsOnOff(_unitname) local playerData=self.players[playername] --#AIRBOSS.PlayerData if playerData then + + -- Invert hints. playerData.showhints=not playerData.showhints -- Inform player. local text="" if playerData.showhints==true then text=string.format("hints are now ON.") - elseif playerData.showhints==false then + else text=string.format("hints are now OFF.") end self:MessageToPlayer(playerData, text, nil, playerData.name, 5) From 71001adc5e256fe49b90203819bffcd5aae88308 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 21 Feb 2019 09:55:57 +0100 Subject: [PATCH 174/485] AIRBOSS v0.9.8 --- Moose Development/Moose/Core/Point.lua | 24 +- Moose Development/Moose/Ops/Airboss.lua | 403 ++++++++++++++++-------- 2 files changed, 300 insertions(+), 127 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 2a5c8d0ee..a0cebabb4 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -210,6 +210,7 @@ do -- COORDINATE FromParkingAreaHot = "From Parking Area Hot", FromRunway = "From Runway", Landing = "Landing", + LandingReFuAr = "LandingReFuAr", } --- @field COORDINATE.WaypointType @@ -219,6 +220,7 @@ do -- COORDINATE TakeOff = "TakeOffParkingHot", TurningPoint = "Turning Point", Land = "Land", + LandingReFuAr = "LandingReFuAr", } @@ -1027,8 +1029,9 @@ do -- COORDINATE -- @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. + -- @param #number timeReFuAr Time in minutes the aircraft stays at the airport for ReFueling and ReArming. -- @return #table The route point. - function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked, airbase, DCSTasks, description ) + function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked, airbase, DCSTasks, description, timeReFuAr ) self:F2( { AltType, Type, Action, Speed, SpeedLocked } ) -- Set alttype or "RADIO" which is AGL. @@ -1055,7 +1058,7 @@ do -- COORDINATE -- Waypoint type. RoutePoint.type = Type or nil - RoutePoint.action = Action or nil + RoutePoint.action = Action or nil -- Speed. RoutePoint.speed = Speed/3.6 @@ -1084,6 +1087,11 @@ do -- COORDINATE --self:MarkToAll(string.format("Landing waypoint at airbase %s, ID=%d, Category=%d", airbase:GetName(), AirbaseID, AirbaseCategory )) end + -- Time in minutes to stay at the airbase before resuming route. + if Type==COORDINATE.WaypointType.LandingReFuAr then + RoutePoint.timeReFuAr=timeReFuAr or 10 + end + -- Waypoint tasks. RoutePoint.task = {} RoutePoint.task.id = "ComboTask" @@ -1173,7 +1181,17 @@ do -- COORDINATE return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, false, airbase, DCSTasks, description) end - + --- Build a Waypoint Air "LandingReFuAr". Mimics the aircraft ReFueling and ReArming. + -- @param #COORDINATE self + -- @param DCS#Speed Speed Airspeed in km/h. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points. + -- @param #number timeReFuAr Time in minutes, the aircraft stays at the airbase. Default 10 min. + -- @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. + function COORDINATE:WaypointAirLandingReFu( Speed, airbase, timeReFuAr, DCSTasks, description ) + return self:WaypointAir(nil, COORDINATE.WaypointType.LandingReFuAr, COORDINATE.WaypointAction.LandingReFuAr, Speed, false, airbase, DCSTasks, description, timeReFuAr or 10) + end --- Build an ground type route point. diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 7de0646ec..d706f01f0 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -82,6 +82,12 @@ -- * [DCS World - F/A-18 - Case I Carrier Recovery Tutorial](https://www.youtube.com/watch?v=lm-M3VUy-_I) -- * [DCS World - Case I Recovery Tutorial - Followup](https://www.youtube.com/watch?v=cW5R32Q6xC8) -- * [DCS World - CASE III Recovery Tutorial](https://www.youtube.com/watch?v=Lnfug5CVAvo) +-- +-- Wags DCS Hornet Videos: +-- +-- * [DCS: F/A-18C Hornet - Episode 9: CASE I Carrier Landing](https://www.youtube.com/watch?v=TuigBLhtAH8) +-- * [DCS: F/A-18C Hornet – Episode 16: CASE III Introduction](https://www.youtube.com/watch?v=DvlMHnLjbDQ) +-- * [DCS: F/A-18C Hornet Case I Carrier Landing Training Lesson Recording](https://www.youtube.com/watch?v=D33uM9q4xgA) -- -- ### Open Questions? -- @@ -363,6 +369,8 @@ -- -- All section members are supposed to follow. Player (or section lead) is removed from all other queues and automatically added to the landing pattern queue. -- +-- The mission designer can forbid this option my setting @{#AIRBOSS.SetEmergencyLandings}(false) in the script. +-- -- ### [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. @@ -426,7 +434,7 @@ -- -- ### Subtitles On/Off -- --- This command toggles the display of radio message subtitles. By default subtitles are on. +-- This command toggles the display of radio message subtitles if no radio relay unit is used. By default subtitles are on. -- Note that subtitles for radio messages which do not have a complete voice over are always displayed. -- -- ## Kneeboard Menu @@ -892,12 +900,18 @@ -- Similarly, to the @{#AIRBOSS.SetExcludeAI} function, AI groups can be explicitly *included* via the @{#AIRBOSS.SetSquadronAI} function. If this is used, only the *included* groups are handled -- by the AIRBOSS. -- +-- ## Keep the Deck Clean +-- +-- Once the AI groups have landed on the carrier, they can be despawned automatically after they shut down their engines. This is achieved by the @{#AIRBOSS.SetDespawnOnEngineShutdown}() function. +-- -- ## Refueling -- -- AI groups in the marshal pattern can be send to refuel at the recovery tanker or if none is defined to the nearest divert airfield. This can be enabled by the @{AIRBOSS.SetRefuelAI}(*lowfuelthreshold*). -- The parameter *lowfuelthreshold* is the threshold of fuel in percent. If the fuel drops below this value, the group will go for refueling. If refueling is performed at the recovery tanker, -- the group will return to the marshal stack when done. The aircraft will not return from the divert airfield however. -- +-- Note that this feature is not enabled by default as there might be bugs in DCS that prevent a smooth refueling of the AI. Enable at your own risk. +-- -- ## Respawning - DCS Landing Bug -- -- AI groups that enter the CCA are usually guided to Marshal stack. However, due to DCS limitations they might not obey the landing task if they have another airfield as departure and/or destination in @@ -1268,29 +1282,20 @@ AIRBOSS.GroovePos={ --- LSO radio calls. -- @type AIRBOSS.LSOCalls --- @field #AIRBOSS.RadioCall RADIOCHECK "Paddles, radio check" call. --- @field #AIRBOSS.RadioCall RIGHTFORLINEUP "Right for line up" call. +-- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call. +-- @field #AIRBOSS.RadioCall CALLTHEBALL "Call the Ball" call. +-- @field #AIRBOSS.RadioCall CHECK "CHECK" call. +-- @field #AIRBOSS.RadioCall CLEAREDTOLAND "Cleared to land" 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 "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 EXPECTHEAVYWAVEOFF "Expect heavy wavoff" call. -- @field #AIRBOSS.RadioCall EXPECTSPOT75 "Expect spot 7.5" call. --- @field #AIRBOSS.RadioCall CLEAREDTOLAND "Cleared to land" call. --- @field #AIRBOSS.RadioCall CHECK "CHECK" call. --- @field #AIRBOSS.RadioCall STABILIZED "Stabilized" call. +-- @field #AIRBOSS.RadioCall FAST "You're fast" call. +-- @field #AIRBOSS.RadioCall FOULDECK "Foul Deck" call. +-- @field #AIRBOSS.RadioCall HIGH "You're high" call. -- @field #AIRBOSS.RadioCall IDLE "Idle" call. +-- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove" call. +-- @field #AIRBOSS.RadioCall LOW "You're low" call. -- @field #AIRBOSS.RadioCall N0 "Zero" call. -- @field #AIRBOSS.RadioCall N1 "One" call. -- @field #AIRBOSS.RadioCall N2 "Two" call. @@ -1301,14 +1306,25 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall N7 "Seven" call. -- @field #AIRBOSS.RadioCall N8 "Eight" call. -- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall PADDLESCONTACT "Paddles, contact" call. +-- @field #AIRBOSS.RadioCall POWER "Power" call. +-- @field #AIRBOSS.RadioCall RADIOCHECK "Paddles, radio check" call. +-- @field #AIRBOSS.RadioCall RIGHTFORLINEUP "Right for line up" call. +-- @field #AIRBOSS.RadioCall ROGERBALL "Roger ball" call. +-- @field #AIRBOSS.RadioCall SLOW "You're slow" call. +-- @field #AIRBOSS.RadioCall STABILIZED "Stabilized" call. +-- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call. +-- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard" call. -- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. -- @field #AIRBOSS.RadioCall NOISE Static noise sound. --- @field #AIRBOSS.RadioCall SKYHAWK "Skyhawk" sound. --- @field #AIRBOSS.RadioCall HARRIER "Harrier" sound. --- @field #AIRBOSS.RadioCall HAWKEYE "Hawkeye" sound. --- @field #AIRBOSS.RadioCall TOMCAT "Tomcat" sound. --- @field #AIRBOSS.RadioCall HORNET "Hornet" sound. --- @field #AIRBOSS.RadioCall BALL "Ball" sound. +-- @field #AIRBOSS.RadioCall BALL "Ball" call. +-- @field #AIRBOSS.RadioCall HARRIER "Harrier" call. +-- @field #AIRBOSS.RadioCall HAWKEYE "Hawkeye" call. +-- @field #AIRBOSS.RadioCall HORNET "Hornet" call. +-- @field #AIRBOSS.RadioCall SKYHAWK "Skyhawk" call. +-- @field #AIRBOSS.RadioCall TOMCAT "Tomcat" call. +-- @field #AIRBOSS.RadioCall VIKING "Viking" call. +-- @field #AIRBOSS.RadioCall SPINIT "Spin it" call. --- Marshal radio calls. -- @type AIRBOSS.MarshalCalls @@ -1347,12 +1363,15 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall RECOVERYPAUSEDRESUMED "Recovery paused and will be resumed at" call. -- @field #AIRBOSS.RadioCall RESUMERECOVERY "Resuming aircraft recovery" call. -- @field #AIRBOSS.RadioCall REPORTSEEME "Report see me" call. +-- @field #AIRBOSS.RadioCall ROGER "Roger" call. -- @field #AIRBOSS.RadioCall SAYNEEDLES "Say needles" call. -- @field #AIRBOSS.RadioCall STACKFULL "Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions" call. -- @field #AIRBOSS.RadioCall STARTINGRECOVERY "Starting aircraft recovery" call. -- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. -- @field #AIRBOSS.RadioCall NOISE Static noise sound. - +-- @field #AIRBOSS.RadioCall BINGOFUEL "Bingo Fuel" call. +-- @field #AIRBOSS.RadioCall GASATDIVERT "Going for gas at the divert field" call. +-- @field #AIRBOSS.RadioCall GASATTANKER "Going for gas at the recovery tanker" call. --- Difficulty level. -- @type AIRBOSS.Difficulty @@ -2063,7 +2082,7 @@ function AIRBOSS:SetRecoveryCase(case) end --- Set holding pattern offset from final bearing for Case II/III recoveries. --- 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. +-- Usually, this is +-15 or +-30 degrees. You should not use and offset 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. @@ -2390,12 +2409,12 @@ end -- @param #number RIGHT -- @return #AIRBOSS self function AIRBOSS:SetLineupErrorThresholds(_max,_min, Left, LEFT, Right, RIGHT) - self.lue._max=_max or 0.5 - self.lue._min=_min or -0.5 - self.lue.Left=Left or -1.0 - self.lue.LEFT=LEFT or -3.0 - self.lue.Right=Right or 1.0 - self.lue.RIGHT=RIGHT or 3.0 + self.lue._max=_max or 0.5 + self.lue._min=_min or -0.5 + self.lue.Left=Left or -1.0 + self.lue.LEFT=LEFT or -3.0 + self.lue.Right=Right or 1.0 + self.lue.RIGHT=RIGHT or 3.0 return self end @@ -2512,10 +2531,10 @@ end --- Set beacon (TACAN/ICLS) time refresh interfal in case the beacons die. -- @param #AIRBOSS self --- @param #number interval Time interval in seconds. Default 300 sec = 5 min. +-- @param #number interval Time interval in seconds. Default 1200 sec = 20 min. -- @return #AIRBOSS self function AIRBOSS:SetBeaconRefresh(interval) - self.dTbeacon=interval or 300 + self.dTbeacon=interval or 20*60 return self end @@ -2865,11 +2884,13 @@ function AIRBOSS:_ActivateBeacons() -- Activate TACAN. if self.TACANon then + self:I(self.lid..string.format("Activating TACAN Channel %d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse)) self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) end -- Activate ICLS. if self.ICLSon then + self:I(self.lid..string.format("Activating ICLS Channel %d (%s)", self.ICLSchannel, self.ICLSmorse)) self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) end @@ -3045,9 +3066,15 @@ function AIRBOSS:_CheckAIStatus() local text=string.format("Group %s fuel=%.1f %%", flight.groupname, fuel) self:T3(self.lid..text) - -- Send AI for refueling at tanker or divert field. + -- Check if flight is low on fuel and not yet refueling. if self.lowfuelAI and fuelglMax then - local text=string.format("Wave off due to glideslope error %.2f > %.1f degrees!", glideslopeError, glMax) + local text=string.format("\n- Waveoff due to glideslope error %.2f > %.1f degrees!", glideslopeError, glMax) self:T(self.lid..string.format("%s: %s", playerData.name, text)) self:_AddToDebrief(playerData, text) waveoff=true elseif glideslopeErrorluAbs then - local text=string.format("Wave off due to line up error |%.1f| > %.1f degrees!", lineupError, luAbs) + local text=string.format("\n- Waveoff due to line up error |%.1f| > %.1f degrees!", lineupError, luAbs) self:T(self.lid..string.format("%s: %s", playerData.name, text)) self:_AddToDebrief(playerData, text) waveoff=true @@ -8820,12 +8911,12 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) 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) + local text=string.format("\n- Waveoff 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 @@ -10638,36 +10729,34 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) end -- Glideslope/altitude. Good [-0.3, 0.4] asymmetric! - -- TODO: introduce enumerator with GSE values. local A=nil - if GSE>1.5 then + if GSE>self.gle.HIGH then A=underline("H") - elseif GSE>0.8 then + elseif GSE>self.gle.High then A="H" - elseif GSE>0.4 then + elseif GSE>self.gle._max then A=little("H") - elseif GSE<-0.9 then + elseif GSE3 then + if LUE>self.lue.RIGHT then D=underline("LUL") - elseif LUE>1 then + elseif LUE>self.lue.Right then D="LUL" - elseif LUE>0.5 then + elseif LUE>self.lue._max then D=little("LUL") - elseif LUE<-3 then + elseif LUE Date: Sat, 23 Feb 2019 14:57:06 +0100 Subject: [PATCH 175/485] Optimized attack. Prevent airborne defenders to fly over enemy zones to target areas behind enemy lines! This is an important feature. I still need to optimize the attack for SEAD; so when SEAD is behind enemy lines and crossing an enemy area that is in line of the attack; the SEAD won't be engaged. Further optimization is required, but it works already quite well! --- .../Moose/AI/AI_A2G_Dispatcher.lua | 65 ++++++++++++++++--- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index e67cead3b..8de026adc 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3919,17 +3919,18 @@ do -- AI_A2G_DISPATCHER local DetectionIndex = DetectedItem.Index local DetectedItemChanged = DetectedItem.Changed - local AttackerCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + local AttackCoordinate = 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 + local EngageCoordinate = nil - for DefenseCoordinateName, EvaluateCoordinate in pairs( self.DefenseCoordinates ) do + for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do + local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE - local EvaluateDistance = AttackerCoordinate:Get2DDistance( EvaluateCoordinate ) + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) if EvaluateDistance <= self.DefenseRadius then @@ -3939,18 +3940,62 @@ do -- AI_A2G_DISPATCHER self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then - DefenseCoordinate = EvaluateCoordinate - break + + -- Now check if this coordinate is not in a danger zone, meaning, that the attack line is not crossing other coordinates. + -- (y1 – y2)x + (x2 – x1)y + (x1y2 – x2y1) = 0 + + local c1 = DefenseCoordinate + local c2 = AttackCoordinate + + local a = c1.z - c2.z -- Calculate a + local b = c2.x - c1.x -- Calculate b + local c = c1.x * c2.z - c2.x * c1.z -- calculate c + + local ok = true + + -- Now we check if each coordinate radius of about 30km of each attack is crossing a defense line. If yes, then this is not a good attack! + for AttackItemID, CheckAttackItem in pairs( Detection:GetDetectedItems() ) do + + -- Only compare other detected coordinates. + if AttackItemID ~= DetectedItemID then + + local CheckAttackCoordinate = self.Detection:GetDetectedItemCoordinate( CheckAttackItem ) + + local x = CheckAttackCoordinate.x + local y = CheckAttackCoordinate.z + local r = 8000 + + -- now we check if the coordinate is intersecting with the defense line. + + local IntersectDistance = ( math.abs( a * x + b * y + c ) ) / math.sqrt( a * a + b * b ) + self:F( { IntersectDistance = IntersectDistance, x = x, y = y } ) + + local IntersectAttackDistance = CheckAttackCoordinate:Get2DDistance( DefenseCoordinate ) + + self:F( { IntersectAttackDistance=IntersectAttackDistance, EvaluateDistance=EvaluateDistance } ) + + -- If the distance of the attack coordinate is larger than the test radius; then the line intersects, and this is not a good coordinate. + if IntersectDistance < r and IntersectAttackDistance < EvaluateDistance then + ok = false + break + end + end + end + + if ok == true then + EngageCoordinate = DefenseCoordinate + break + end end end end - if DefenseCoordinate then + if EngageCoordinate 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 ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD", EngageCoordinate ) Delay = Delay + 1 end end @@ -3959,7 +4004,7 @@ do -- AI_A2G_DISPATCHER 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 ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS", EngageCoordinate ) Delay = Delay + 1 end end @@ -3968,7 +4013,7 @@ do -- AI_A2G_DISPATCHER 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 ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI", EngageCoordinate ) Delay = Delay + 1 end end From 1ff41ec7ec1e73c864963b11328dad9fbd1b93a7 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 25 Feb 2019 19:16:18 +0100 Subject: [PATCH 176/485] Improvements --- Moose Development/Moose/AI/AI_A2G_BAI.lua | 30 ++++++++++----- Moose Development/Moose/AI/AI_A2G_CAS.lua | 37 +++++++++++++------ .../Moose/AI/AI_A2G_Dispatcher.lua | 17 +++++---- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 33 +++++++++-------- .../Moose/Functional/Detection.lua | 2 +- 5 files changed, 73 insertions(+), 46 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index efd48c397..79871ba52 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -127,19 +127,30 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit local AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] - if not AttackUnit then - self.AttackSetUnit.AttackIndex = 1 - AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] - end + local AttackUnitTasks = {} + +-- 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() } ) +-- AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) +-- end +-- 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 ) + for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "BAI Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) + end end end + - if #AttackTasks == 0 then + if #AttackUnitTasks == 0 then self:E( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() self:__RTB( self.TaskDelay ) @@ -148,6 +159,7 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit DefenderGroup:OptionROTEvadeFire() DefenderGroup:OptionKeepWeaponsOnThreat() + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) end diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 8d483f317..54e677576 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -74,7 +74,7 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit if DefenderGroup:IsAlive() then - local EngageAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local EngageAltitude = math.random( self.EngageFloorAltitude or 500, self.EngageCeilingAltitude or 1000 ) local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) local DefenderCoord = DefenderGroup:GetPointVec3() @@ -121,21 +121,33 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit local AttackSetUnitPerThreatLevel = self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) - local AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] + --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 ) + local AttackUnitTasks = {} + +-- 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 + + for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "CAS Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) + end end end + - if #AttackTasks == 0 then + if #AttackUnitTasks == 0 then self:E( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() self:__RTB( self.TaskDelay ) @@ -144,6 +156,7 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit DefenderGroup:OptionROTEvadeFire() DefenderGroup:OptionKeepWeaponsOnThreat() + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) end diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 8de026adc..8cde9e245 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -2089,8 +2089,8 @@ do -- AI_A2G_DISPATCHER Sead.Name = SquadronName Sead.EngageMinSpeed = EngageMinSpeed Sead.EngageMaxSpeed = EngageMaxSpeed - Sead.EngageFloorAltitude = EngageFloorAltitude - Sead.EngageCeilingAltitude = EngageCeilingAltitude + Sead.EngageFloorAltitude = EngageFloorAltitude or 500 + Sead.EngageCeilingAltitude = EngageCeilingAltitude or 1000 Sead.Defend = true self:F( { Sead = Sead } ) @@ -2186,8 +2186,8 @@ do -- AI_A2G_DISPATCHER Cas.Name = SquadronName Cas.EngageMinSpeed = EngageMinSpeed Cas.EngageMaxSpeed = EngageMaxSpeed - Cas.EngageFloorAltitude = EngageFloorAltitude - Cas.EngageCeilingAltitude = EngageCeilingAltitude + Cas.EngageFloorAltitude = EngageFloorAltitude or 500 + Cas.EngageCeilingAltitude = EngageCeilingAltitude or 1000 Cas.Defend = true self:F( { Cas = Cas } ) @@ -2284,8 +2284,8 @@ do -- AI_A2G_DISPATCHER Bai.Name = SquadronName Bai.EngageMinSpeed = EngageMinSpeed Bai.EngageMaxSpeed = EngageMaxSpeed - Bai.EngageFloorAltitude = EngageFloorAltitude - Bai.EngageCeilingAltitude = EngageCeilingAltitude + Bai.EngageFloorAltitude = EngageFloorAltitude or 500 + Bai.EngageCeilingAltitude = EngageCeilingAltitude or 1000 Bai.Defend = true self:F( { Bai = Bai } ) @@ -3942,7 +3942,7 @@ do -- AI_A2G_DISPATCHER if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then -- Now check if this coordinate is not in a danger zone, meaning, that the attack line is not crossing other coordinates. - -- (y1 – y2)x + (x2 – x1)y + (x1y2 – x2y1) = 0 + -- (y1 – y2)x + (x2 – x1)y + (x1y2 – x2y1) = 0 local c1 = DefenseCoordinate local c2 = AttackCoordinate @@ -4029,7 +4029,8 @@ do -- AI_A2G_DISPATCHER if self.TacticalDisplay then -- Show tactical situation - Report:Add( string.format( " - %s ( %s ): ( #%d - %4s ) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", DetectedItem.Set:GetObjectNames() ) ) + local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() + Report:Add( string.format( " - %s ( %s ): ( #%d - %4s ) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "â– ", ThreatLevel ) ) ) 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_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index 124fe5049..e47d09449 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -129,7 +129,7 @@ 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 EngageAltitude = math.random( self.EngageFloorAltitude or 500, self.EngageCeilingAltitude or 1000 ) local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) local DefenderCoord = DefenderGroup:GetPointVec3() @@ -150,7 +150,7 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, EngageSpeed, - true + false ) EngageRoute[#EngageRoute+1] = FromWP @@ -174,28 +174,28 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni 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 - +-- 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 ) + local AttackUnitTasks = {} + 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 + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:F( { "SEAD Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) end end end end - if #AttackTasks == 0 then + if #AttackUnitTasks == 0 then self:E( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() self:__RTB( self.TaskDelay ) @@ -205,6 +205,7 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni DefenderGroup:OptionKeepWeaponsOnThreat() --DefenderGroup:OptionRTBAmmo( Weapon.flag.AnyASM ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) end diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 087a02207..c3eb65ccd 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -788,7 +788,7 @@ do -- DETECTION_BASE -- IsDetected = false! -- This is used in A2A_TASK_DISPATCHER to initiate fighter sweeping! The TASK_A2A_INTERCEPT tasks will be replaced with TASK_A2A_SWEEP tasks. for DetectedObjectName, DetectedObject in pairs( self.DetectedObjects ) do - if self.DetectedObjects[DetectedObjectName].IsDetected == true and self.DetectedObjects[DetectedObjectName].DetectionTimeStamp + 60 <= DetectionTimeStamp then + if self.DetectedObjects[DetectedObjectName].IsDetected == true and self.DetectedObjects[DetectedObjectName].DetectionTimeStamp + 300 <= DetectionTimeStamp then self.DetectedObjects[DetectedObjectName].IsDetected = false end end From c16edd65b0fff7b36d18a45c4d5f032330cae7eb Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 25 Feb 2019 20:23:03 +0100 Subject: [PATCH 177/485] AIRBOSS v0.9.9 --- Moose Development/Moose/Ops/Airboss.lua | 1155 +++++++++++++++-------- 1 file changed, 774 insertions(+), 381 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index d706f01f0..7c11390ce 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1,4 +1,4 @@ ---- **Ops** - (R2.5) - Manages aircraft recoveries for carrier operations. +--- **Ops** - (R2.5) - Manages aircraft CASE X recoveries for carrier operations (X=I, II, III). -- -- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. -- @@ -13,7 +13,7 @@ -- * 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 (aircraft attitude, marking of zones etc). +-- * Advanced F10 radio menu including carrier info, weather, radio frequencies, TACAN/ICLS channels, player LSO grades, 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 players to sections (WIP). @@ -21,6 +21,7 @@ -- * 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. +-- * Trap sheet (optional). -- * Finite State Machine (FSM) implementation. -- -- **Supported Carriers:** @@ -66,36 +67,32 @@ -- -- ## Youtube Videos -- --- Early AIRBOSS Groove Testing: +-- ### Early AIRBOSS Groove Testing: -- -- * [[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) -- * [[MOOSE] Airboss - Groove Test: On-the-fly LSO Grading](https://www.youtube.com/watch?v=Xgs1hwDcPyM) -- --- Lex explaining Boat Ops: +-- ### Lex explaining Boat Ops: -- -- * [( DCS HORNET ) Some boat ops basics VID 1](https://www.youtube.com/watch?v=LvGQS-3AzMc) -- * [( DCS HORNET ) Some boat ops basics VID 2](https://www.youtube.com/watch?v=bN44wvtRsw0) -- --- Jabbers Case I and III Recovery Tutorials: +-- ### Jabbers Case I and III Recovery Tutorials: -- -- * [DCS World - F/A-18 - Case I Carrier Recovery Tutorial](https://www.youtube.com/watch?v=lm-M3VUy-_I) -- * [DCS World - Case I Recovery Tutorial - Followup](https://www.youtube.com/watch?v=cW5R32Q6xC8) -- * [DCS World - CASE III Recovery Tutorial](https://www.youtube.com/watch?v=Lnfug5CVAvo) -- --- Wags DCS Hornet Videos: +-- ### Wags DCS Hornet Videos: -- -- * [DCS: F/A-18C Hornet - Episode 9: CASE I Carrier Landing](https://www.youtube.com/watch?v=TuigBLhtAH8) -- * [DCS: F/A-18C Hornet – Episode 16: CASE III Introduction](https://www.youtube.com/watch?v=DvlMHnLjbDQ) -- * [DCS: F/A-18C Hornet Case I Carrier Landing Training Lesson Recording](https://www.youtube.com/watch?v=D33uM9q4xgA) +-- +-- ### AV-8B Harrier at USS Tarawa -- --- ### Open Questions? --- --- * What is the next step after a pattern wave off during Case II or III recovery? A: Go back to start! --- * 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? --- --- 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. +-- * [Harrier Ship Landing Mission with Auto LSO!](https://www.youtube.com/watch?v=lqmVvpunk2c) -- -- === -- @@ -219,6 +216,8 @@ -- @field #boolean turning If true, carrier is currently turning. -- @field #AIRBOSS.GLE gle Glidesope error thresholds. -- @field #AIRBOSS.LUE lue Lineup error thresholds. +-- @field #boolean trapsheet If true, players can save their trap sheets. +-- @field #string trappath Path where to save the trap sheets. -- @extends Core.Fsm#FSM --- Be the boss! @@ -590,8 +589,10 @@ -- 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. --- * 2.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". +-- * 2.0 Points **PWO**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. +-- * 2.0 Points **OWO**: "Own Wave-Off**, when pilot flies past the deck without touching it. +-- * 1.0 Points **WO**: "Technique Wave-Off": Player got waved off in the final parts of the groove. +-- * 1.0 Points **LIG**: "Long In the Groove", when pilot extents the downwind leg too far and screws up the timing for the following aircraft. -- * 0.0 Points **CUT**: "Cut pass", when player was waved off but landed anyway. -- -- ## Foul Deck Waveoff @@ -734,7 +735,6 @@ -- 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. @@ -789,6 +789,44 @@ -- 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. -- +-- # Trap Sheet +-- +-- Important aircraft attitude parameters during the Groove can be saved to file for later analysis. This also requires the **io** and optionally **lfs** modules to be desanitized. +-- +-- In the script you have to add the @{#AIRBOSS.SetTrapSheet}(*path*) function to activate this feature. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetTable.png) +-- +-- Data the is written to a file in csv format and contains the following information: +-- +-- * *Time*: time in seconds since start. +-- * *Rho*: distance from rundown to player aircraft in NM. +-- * *X*: distance parallel to the carrier in meters. +-- * *Z*: distance perpendicular to the carrier in meters. +-- * *Alt*: altitude of player aircraft in feet. +-- * *AoA*: angle of attack in degrees. +-- * *GSE*: glideslope error in degrees. +-- * *LUE*: lineup error in degrees. +-- * *Vtot*: total velocity of player aircraft in knots. +-- * *Vy*: vertical (descent) velocity in ft/min. +-- * *Gamma*: angle between vector of aircraft nose and vector point in the direction of the carrier runway in degrees. +-- * *Pitch*: pitch angle of player aircraft in degrees. +-- * *Roll*: roll angle of player aircraft in degrees. +-- * *Yaw*: yaw angle of player aircraft in degrees. +-- * *Step*: Step in the groove. +-- +--## Lineup Error +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetLUE.png) +-- +-- The graph displayes the lineup error as a function of the distance to the carrier. +-- +-- ## Glideslope Error +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetGSE.png) +-- +-- -- The graph displayes the glideslope error as a function of the distance to the carrier. +-- -- === -- -- # Sound Files @@ -906,7 +944,7 @@ -- -- ## Refueling -- --- AI groups in the marshal pattern can be send to refuel at the recovery tanker or if none is defined to the nearest divert airfield. This can be enabled by the @{AIRBOSS.SetRefuelAI}(*lowfuelthreshold*). +-- AI groups in the marshal pattern can be send to refuel at the recovery tanker or if none is defined to the nearest divert airfield. This can be enabled by the @{#AIRBOSS.SetRefuelAI}(*lowfuelthreshold*). -- The parameter *lowfuelthreshold* is the threshold of fuel in percent. If the fuel drops below this value, the group will go for refueling. If refueling is performed at the recovery tanker, -- the group will return to the marshal stack when done. The aircraft will not return from the divert airfield however. -- @@ -1094,6 +1132,8 @@ AIRBOSS = { respawnAI = nil, gle = {}, lue = {}, + trapsheet = nil, + trappath = nil, } --- Aircraft types capable of landing on carrier (human+AI). @@ -1399,12 +1439,20 @@ AIRBOSS.Difficulty={ --- Groove data. -- @type AIRBOSS.GrooveData -- @field #number Step Current step. +-- @field #number Time Time in seconds. +-- @field #number Rho Distance in meters. +-- @field #number X Distance in meters. +-- @field #nubmer Z Distance in meters. -- @field #number AoA Angle of Attack. -- @field #number Alt Altitude in meters. -- @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 Pitch Pitch angle in degrees. +-- @field #number Roll Roll angle in degrees. +-- @field #number Yaw Yaw angle in degrees. +-- @field #number Vel Total velocity in m/s. +-- @field #number Vy Vertical velocity in m/s. +-- @field #number Gamma Relative heading player to carrier's runway. 0=parallel, +-90=perpendicular. -- @field #string FlyThrough Fly through up "/" or fly through down "\\". --- LSO grade data. @@ -1484,9 +1532,10 @@ 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 off during the pattern. +-- @field #boolean wop 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 #boolean owo If true, own waveoff by player. +-- @field #boolean wofd 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 TIG0 Time in groove start timer.getTime(). @@ -1497,6 +1546,8 @@ AIRBOSS.Difficulty={ -- @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. -- @field #boolean showhints If true, show step hints. +-- @field #table trapsheet Groove data table recorded every 0.5 seconds. +-- @field #boolean trapon If true, save trap sheets. -- @extends #AIRBOSS.FlightGroup --- Main group level radio menu: F10 Other/Airboss. @@ -1509,7 +1560,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.8" +AIRBOSS.version="0.9.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1759,7 +1810,7 @@ function AIRBOSS:New(carriername, alias) end -- Smoke zones. - if self.Debug and false then + if false then local case=2 self.holdingoffset=30 self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red, 5) @@ -1771,7 +1822,7 @@ function AIRBOSS:New(carriername, alias) self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) self:_GetZoneHolding(case, 1):SmokeZone(SMOKECOLOR.White, 45) self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Orange, 45) - self:_GetZoneCommence(1):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneCommence(case):SmokeZone(SMOKECOLOR.Red, 45) end -- Carrier parameter debug tests. @@ -2468,6 +2519,16 @@ function AIRBOSS:SetMenuSmokeZones(switch) return self end +--- Enable saving of player's trap sheets and specify an optional directory path. +-- @param #AIRBOSS self +-- @param #string path (Optional) Path where to save the trap sheets. +-- @return #AIRBOSS self +function AIRBOSS:SetTrapSheet(path) + self.trapsheet=true + self.trappath=path + 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. @@ -2954,7 +3015,10 @@ function AIRBOSS:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Ejection) self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) self:HandleEvent(EVENTS.MissionEnd) - + + -- DCS event handler. + --world.addEventHandler(self) + -- Start status check in 1 second. self:__Status(1) end @@ -2984,8 +3048,8 @@ function AIRBOSS:onafterStatus(From, Event, To) 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 - Collision Warning=%s", - clock, self:GetState(), self.case, self.carrier:GetVelocityKNOTS(), hdg, self.currentwp, eta, tostring(collision)) + local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s - Turning=%s - Collision Warning=%s", + clock, self:GetState(), self.case, self.carrier:GetVelocityKNOTS(), hdg, self.currentwp, eta, tostring(self.turning), tostring(collision)) self:T(self.lid..text) -- Check for collision. @@ -3026,6 +3090,9 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Check marshal and pattern queues. self:_CheckQueue() + -- Check if carrier is currently turning. + self:_CheckCarrierTurning() + -- Check if marshal pattern of AI needs an update. self:_CheckPatternUpdate() @@ -6388,6 +6455,11 @@ function AIRBOSS:_NewPlayer(unitname) -- Attitude monitor. playerData.attitudemonitor=false + -- Trap sheet save. + if playerData.trapon==nil then + playerData.trapon=self.trapsheet + end + -- Set difficulty level. playerData.difficulty=playerData.difficulty or self.defaultskill @@ -6440,14 +6512,16 @@ function AIRBOSS:_InitPlayer(playerData, step) playerData.step=step or AIRBOSS.PatternStep.UNDEFINED playerData.groove={} playerData.debrief={} + playerData.trapsheet={} playerData.warning=nil playerData.holding=nil playerData.refueling=false playerData.valid=false playerData.lig=false - playerData.patternwo=false + playerData.wop=false playerData.waveoff=false - playerData.fouldeckwo=false + playerData.wofd=false + playerData.owo=false playerData.boltered=false playerData.landed=false playerData.Tlso=timer.getTime() @@ -7251,6 +7325,29 @@ end -- EVENT functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- General DCS 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("Skipping onEvent. Event or Event.initiator unknown.") + return true + end + if Unit.getByName(Event.initiator:getName()) == nil then + self:T3("Skipping onEvent. Initiator unit name unknown.") + return true + end + ]] + + --env.info("FF DCS Event") + --self:E(Event) + +end + + --- Airboss event handler for event birth. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData @@ -7848,7 +7945,7 @@ function AIRBOSS:_Holding(playerData) -- Angels. local angels=self:_GetAngels(patternalt) - -- TODO: Check if player is flying counter clockwise. AOB<0. + -- XXX: Check if player is flying counter clockwise. AOB<0. -- Message text. local text="" @@ -8070,13 +8167,13 @@ function AIRBOSS:_CheckCorridor(playerData) -- Issue warning. if invalid and (not playerData.warning) then - self:MessageToPlayer(playerData, "You left the approach corridor!", "AIRBOSS") + 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.", "AIRBOSS") + self:MessageToPlayer(playerData, "you're back in the approach corridor.", "AIRBOSS") playerData.warning=false end @@ -8194,8 +8291,8 @@ function AIRBOSS:_DirtyUp(playerData) if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then local callsay=self:_NewRadioCall(self.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard) local callfly=self:_NewRadioCall(self.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard) - self:RadioTransmission(self.MarshalRadio, callsay, false, 40, nil, true) - self:RadioTransmission(self.MarshalRadio, callfly, false, 45, nil, true) + self:RadioTransmission(self.MarshalRadio, callsay, false, 50, nil, true) + self:RadioTransmission(self.MarshalRadio, callfly, false, 55, nil, true) end -- TODO: Make Fly Bullseye call if no automatic ICLS is active. @@ -8278,7 +8375,7 @@ end 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) + local X, Z=self:_GetDistances(playerData.unit) -- Abort condition check. if self:_CheckAbort(X, Z, self.BreakEntry) then @@ -8306,7 +8403,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, rho, phi=self:_GetDistances(playerData.unit) + local X, Z=self:_GetDistances(playerData.unit) -- Early or late break. local breakpoint = self.BreakEarly @@ -8346,8 +8443,8 @@ 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) - -- 1.5 NM from carrier is too far. - local limit=UTILS.NMToMeters(-1.5) + -- 1.6 NM from carrier is too far. + local limit=UTILS.NMToMeters(-1.6) -- For the tarawa we give a bit more space. if self.carriertype==AIRBOSS.CarrierType.TARAWA then @@ -8366,7 +8463,7 @@ function AIRBOSS:_CheckForLongDownwind(playerData) --grade="LIG PATTERN WAVE OFF - CUT 1 PT" playerData.lig=true - playerData.patternwo=true + playerData.wop=true -- Next step: Debriefing. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) @@ -8381,7 +8478,7 @@ end function AIRBOSS:_Abeam(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) -- Check abort conditions. if self:_CheckAbort(X, Z, self.Abeam) then @@ -8415,7 +8512,7 @@ end function AIRBOSS:_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) + local X, Z=self:_GetDistances(playerData.unit) -- Check abort conditions. if self:_CheckAbort(X, Z, self.Ninety) then @@ -8442,9 +8539,9 @@ function AIRBOSS:_Ninety(playerData) elseif relheading>90 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") + self:MessageToPlayer(playerData, "you are already at the wake and have not passed the 90. Turn faster next time!", "LSO") self:RadioTransmission(self.LSORadio, self.LSOCall.DEPARTANDREENTER, nil, nil, nil, true) - playerData.patternwo=true + playerData.wop=true -- Debrief. self:_AddToDebrief(playerData, "Overshoot at wake - Pattern Waveoff!") self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) @@ -8457,7 +8554,7 @@ end 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) + local X, Z=self:_GetDistances(playerData.unit) -- Check abort conditions. if self:_CheckAbort(X, Z, self.Wake) then @@ -8477,13 +8574,61 @@ function AIRBOSS:_Wake(playerData) end end +--- Get groove data. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #AIRBOSS.GrooveData Groove data table. +function AIRBOSS:_GetGrooveData(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier). + local X, Z=self:_GetDistances(playerData.unit) + + -- Stern position at the rundown. + local stern=self:_GetSternCoord() + + -- Distance from rundown to player aircraft. + local rho=stern:Get2DDistance(playerData.unit:GetCoordinate()) + + -- Aircraft is behind the carrier. + local astern=X5. This would mean the player has not turned in correctly! -- Groove data. @@ -8545,46 +8679,14 @@ end function AIRBOSS:_Groove(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) - -- Player altitude - local alt=playerData.unit:GetAltitude() - - -- Player group. - local player=playerData.unit:GetGroup() - -- Check abort conditions. if self:_CheckAbort(X, Z, self.Groove) then 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) - - -- Glideslope. - local glideslopeError=self:_Glideslope(playerData.unit) - - -- Get AoA. - local AoA=playerData.unit:GetAoA() - - -- Get Angle of Bank. - local roll=playerData.unit:GetRoll() - - -- Aircraft is behind the carrier. - local astern=XRXX and playerData.difficulty~=AIRBOSS.Difficulty.EASY then + _advice=false + end -- LSO call if necessary. - if deltaT>=self.LSOdT then + if deltaT>=self.LSOdT and _advice then self:_LSOadvice(playerData, glideslopeError, lineupError) end @@ -8837,12 +8947,13 @@ function AIRBOSS:_Groove(playerData) else -- 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?") + self:T("Player was not waved off but flew past the carrier without landing ==> Own wave off!") - -- TODO: This is more like a pilot wave off then. - self:_AddToDebrief(playerData, "Pilot waveoff.") + -- We count this as OWO. + self:_AddToDebrief(playerData, "Own waveoff.") - playerData.waveoff=true + -- Set Owo + playerData.owo=true end @@ -8956,7 +9067,7 @@ function AIRBOSS:_CheckFoulDeck(playerData) end -- Check if player was already waved off. Should not be necessary as player step is set to debrief afterwards! - if playerData.fouldeckwo==true or check==false then + if playerData.wofd==true or check==false then -- Player was already waved off. return end @@ -9023,7 +9134,7 @@ function AIRBOSS:_CheckFoulDeck(playerData) end -- Set player parameters for foul deck. - playerData.fouldeckwo=true + playerData.wofd=true -- Debrief. playerData.step=AIRBOSS.PatternStep.DEBRIEF @@ -9179,6 +9290,7 @@ function AIRBOSS:_Trapped(playerData) local dcorr=100 if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then dcorr=100 + -- TODO: Check Tomcat. elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then -- A-4E gets slowed down much faster the the F/A-18C! dcorr=56 @@ -9477,16 +9589,17 @@ end --- Get approach corridor zone. Shape depends on recovery case. -- @param #AIRBOSS self -- @param #number case Recovery case. +-- @param #number l Length of the zone in NM. Default 31 (=21+10) NM. -- @return Core.Zone#ZONE_POLYGON_BASE Box zone. -function AIRBOSS:_GetZoneCorridor(case) +function AIRBOSS:_GetZoneCorridor(case, l) + + -- Total length. + l=l or 31 -- Radial and offset. local radial=self:GetRadial(case, false, false) local offset=self:GetRadial(case, false, true) - -- 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=5 @@ -9496,11 +9609,68 @@ function AIRBOSS:_GetZoneCorridor(case) -- Distance from carrier to arc out zone. local d=12 + + -- Carrier position. + local cv=self:GetCoordinate() - -- Length of the box in NM. - local x=(d+w/2)/math.cos(alpha) - local l=31-x + -- Polygon points. + local c={} + -- First point. Carrier coordinate translated 5 NM in direction of travel to allow for bolter space. + c[1]=cv:Translate(-UTILS.NMToMeters(dx), radial) + + if math.abs(self.holdingoffset)>=5 then + + ----------------- + -- Angled Case -- + ----------------- + + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier, dx ahead. + c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right + + c[4]=cv:Translate(UTILS.NMToMeters(15), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[5]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[6]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[7]=cv:Translate(UTILS.NMToMeters(13), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[8]=cv:Translate(UTILS.NMToMeters(11), radial):Translate(UTILS.NMToMeters(1), radial+90) + + c[9]=c[1]:Translate(UTILS.NMToMeters(w2), 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(dx+l), radial) -- 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 + + + -- Create an array of a square! + local p={} + for _i,_c in ipairs(c) do + if self.Debug then + --_c:SmokeBlue() + end + p[_i]=_c:GetVec2() + end + + -- 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 Approach Corridor", p) + + return zone + + --[[ + -- OLD + + -- Angle between radial and offset in rad. + local alpha=math.rad(self.holdingoffset) + -- Some math... local y1=d-w2 local x1=y1*math.tan(alpha) @@ -9526,61 +9696,21 @@ function AIRBOSS:_GetZoneCorridor(case) self:T3(string.format("FF P = %.1f NM", P)) self:T3(string.format("FF Q = %.1f NM", Q)) - local c={} - local cv=self:GetCoordinate() - c[1]=cv:Translate(-UTILS.NMToMeters(dx), radial) --Carrier coordinate translated 2 NM in direction of travel to allow for bolter space. + -- Complicated case with an angle. + 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) - if math.abs(self.holdingoffset)>=5 then + -- 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) + ]] - --[[ - -- Complicated case with an angle. - 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) - ]] - - c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier, dx ahead. - c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right - - c[4]=cv:Translate(UTILS.NMToMeters(15), offset):Translate(UTILS.NMToMeters(1), offset-90) - c[5]=cv:Translate(UTILS.NMToMeters(31), offset):Translate(UTILS.NMToMeters(1), offset-90) - c[6]=cv:Translate(UTILS.NMToMeters(31), offset):Translate(UTILS.NMToMeters(1), offset+90) - c[7]=cv:Translate(UTILS.NMToMeters(13), offset):Translate(UTILS.NMToMeters(1), offset+90) - c[8]=cv:Translate(UTILS.NMToMeters(11), radial):Translate(UTILS.NMToMeters(1), radial+90) - c[9]=c[1]:Translate(UTILS.NMToMeters(w2), 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) - 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 - - - -- Create an array of a square! - local p={} - for _i,_c in ipairs(c) do - if self.Debug then - --_c:SmokeBlue() - end - p[_i]=_c:GetVec2() - end - - -- 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 Approach Corridor", p) - - return zone end @@ -9639,7 +9769,7 @@ function AIRBOSS:_GetZoneRunwayBox() -- Points. 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, 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. @@ -9825,8 +9955,34 @@ function AIRBOSS:_GetZoneCommence(case) else -- Case II/III - -- We simply take the corridor for now. - zone=self:_GetZoneCorridor(case) + -- We simply take the corridor for now. But a bit shorter. Holding starts at 21 and add 2 NM box as commence. + --zone=self:_GetZoneCorridor(case, 23) + + -- Total length. + local l=21 + + -- Offset angle + local offset=self:GetRadial(case, false, true) + + -- Carrier position. + local cv=self:GetCoordinate() + + -- Polygon points. + local c={} + + c[1]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[2]=cv:Translate(UTILS.NMToMeters(l+2.5), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[3]=cv:Translate(UTILS.NMToMeters(l+2.5), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[4]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset+90) + + -- Create an array of a square! + local p={} + for _i,_c in ipairs(c) do + p[_i]=_c:GetVec2() + end + + -- Zone polygon. + zone=ZONE_POLYGON_BASE:New("CASE II/III Commence Zone", p) end @@ -9860,11 +10016,9 @@ function AIRBOSS:_AttitudeMonitor(playerData) -- Aircraft veloecity vector. local velo=unit:GetVelocityVec3() - local vabs=UTILS.VecNorm(velo) - - -- Relative heading Aircraft to Carrier. - local relhead=self:_GetRelativeHeading(playerData.unit) - + local vabs=UTILS.VecNorm(velo) + + local rwy=false local step=playerData.step if playerData.step==AIRBOSS.PatternStep.FINAL or playerData.step==AIRBOSS.PatternStep.GROOVE_XX or @@ -9875,7 +10029,11 @@ function AIRBOSS:_AttitudeMonitor(playerData) playerData.step==AIRBOSS.PatternStep.GROOVE_LC or playerData.step==AIRBOSS.PatternStep.GROOVE_IW then step=self:_GS(step,-1) - end + rwy=true + end + + -- Relative heading Aircraft to Carrier. + local relhead=self:_GetRelativeHeading(playerData.unit, rwy) -- Output local text=string.format("Pattern step: %s", step) @@ -9897,8 +10055,8 @@ function AIRBOSS:_AttitudeMonitor(playerData) playerData.step==AIRBOSS.PatternStep.GROOVE_AL or playerData.step==AIRBOSS.PatternStep.GROOVE_LC or playerData.step==AIRBOSS.PatternStep.GROOVE_IW then - local lineup=self:_Lineup(playerData.unit, true) - local glideslope=self:_Glideslope(playerData.unit) + local lue=self:_Lineup(playerData.unit, true) + local gle=self:_Glideslope(playerData.unit) local dist=self:_GetOptLandingCoordinate():Get2DDistance(playerData.unit) -- Get player velocity in km/h. local vplayer=playerData.unit:GetVelocityKMH() @@ -9907,13 +10065,14 @@ function AIRBOSS:_AttitudeMonitor(playerData) -- Speed difference. local dv=math.abs(vplayer-vcarrier) text=text..string.format("\nDist=%.1f m Alt=%.1f m delta|V|=%.1f km/h", dist, self:_GetAltCarrier(playerData.unit), dv) - text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f", lineup, glideslope, self:_AoADeg2Units(playerData, aoa)) + text=text..string.format("\nGamma=%.1f° | Rho=%.1f°", relhead, phi) + text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f", lue, gle, self:_AoADeg2Units(playerData, aoa)) local grade, points, analysis=self:_LSOgrade(playerData) text=text..string.format("\nTgroove=%.1f sec", self:_GetTimeInGroove(playerData)) 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) + text=text..string.format("\nGamma=%.1f° | Rho=%.1f°", relhead, phi) end MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) @@ -10025,8 +10184,12 @@ end -- @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. + + -- TODO: Value 4 meters is for the Hornet. Adjust for Harrier, A4E and + + -- Altitude of unit corrected by the deck height of the carrier. local h=unit:GetAltitude()-self.carrierparam.deckheight-4 + return h end @@ -10340,24 +10503,42 @@ function AIRBOSS:_GetRelativeHeading(unit, runway) -- Direction vector of the carrier. local vC=self.carrier:GetOrientationX() + -- Include runway angle. + if runway then + vC=UTILS.Rotate2D(vC, -self.carrierparam.rwyangle) + end + -- 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 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 + -- Get angle between the two orientation vectors in degrees. + local rhdg=UTILS.VecAngle(vC,vP) -- Return heading in degrees. return rhdg end +--- Get relative velocity of player unit wrt to carrier +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Player unit. +-- @return #number Relative velocity in m/s. +function AIRBOSS:_GetRelativeVelocity(unit) + + local vC=self.carrier:GetVelocityVec3() + local vP=unit:GetVelocityVec3() + + -- Only X-Z plane is necessary here. + vC.y=0 ; vP.y=0 + + local v=UTILS.VecSubstract(vP, vC) + + return UTILS.VecNorm(v),v +end + + --- Calculate distances between carrier and aircraft unit. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Aircraft unit. @@ -10388,19 +10569,14 @@ function AIRBOSS:_GetDistances(unit) -- Projection of player pos on z component. local dz=UTILS.VecDot(z,c) - -- Polar coordinates + -- 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 - 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 - + -- Correct for negative values. if phi<0 then phi=phi+360 end @@ -10639,16 +10815,26 @@ function AIRBOSS:_LSOgrade(playerData) self:T2(self.lid..text) -- Special cases. - if playerData.patternwo then - -- Pattern Wave Off - grade="WOP" + if playerData.wop then + --------------------- + -- Pattern Waveoff -- + --------------------- if playerData.lig then + -- Long In the Groove (LIG). + -- According to Stingers this is a CUT pass and gives 1.0 points. + grade="CUT" + points=1.0 G="LIG" - elseif playerData.patternwo then + else + -- Other pattern WO + grade="WOP" + points=2.0 G="n/a" - end - points=2.0 - elseif playerData.fouldeckwo then + end + elseif playerData.wofd then + ----------------------- + -- Foul Deck Waveoff -- + ----------------------- if playerData.landed then --AIRBOSS wants to talk to you! grade="CUT" @@ -10658,8 +10844,19 @@ function AIRBOSS:_LSOgrade(playerData) points=-1.0 end G="n/a" + elseif playerData.owo then + ----------------- + -- Own Waveoff -- + ----------------- + grade="OWO" + points=2.0 + if G=="Unicorn" then + G="n/a" + end elseif playerData.waveoff then - -- Wave Off + ------------- + -- Waveoff -- + ------------- if playerData.landed then --AIRBOSS wants to talk to you! grade="CUT" @@ -10985,7 +11182,7 @@ function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) if patternwo then -- Pattern wave off! - playerData.patternwo=true + playerData.wop=true -- Add to debrief. self:_AddToDebrief(playerData, string.format("Pattern wave off: %s", text)) @@ -11005,7 +11202,8 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -- @param #number delay Delay before playing sound messages. Default 0 sec. -function AIRBOSS:_PlayerHint(playerData, delay) +-- @param #boolean soundoff If true, don't play and sound hint. +function AIRBOSS:_PlayerHint(playerData, delay, soundoff) -- No hint for the pros. if not playerData.showhints then @@ -11029,32 +11227,32 @@ function AIRBOSS:_PlayerHint(playerData, delay) -- Message to player. local hint="" - if hintAlt then + if hintAlt and hintAlt~="" then hint=hint.."\n"..hintAlt end - if hintSpeed then + if hintSpeed and hintSpeed~="" then hint=hint.."\n"..hintSpeed end - if hintAoA then + if hintAoA and hintAoA~="" then hint=hint.."\n"..hintAoA end - if hintDist then + if hintDist and hintDist~="" then hint=hint.."\n"..hintDist end -- Debriefing text. local debrief="" - if debriefAlt then - debrief=debrief.."\n-"..debriefAlt + if debriefAlt and debriefAlt~="" then + debrief=debrief.."\n- "..debriefAlt end - if debriefSpeed then - debrief=debrief.."\n-"..debriefSpeed + if debriefSpeed and debriefSpeed~="" then + debrief=debrief.."\n- "..debriefSpeed end - if debriefAoA then - debrief=debrief.."\n-"..debriefAoA + if debriefAoA and debriefAoA~="" then + debrief=debrief.."\n- "..debriefAoA end - if debriefDist then - debrief=debrief.."\n-"..debriefDist + if debriefDist and debriefDist~="" then + debrief=debrief.."\n- "..debriefDist end -- Add step to debriefing. @@ -11064,23 +11262,25 @@ function AIRBOSS:_PlayerHint(playerData, delay) -- Voice hint. delay=delay or 0 - if callAlt then - self:Sound2Player(playerData, self.LSORadio, callAlt, false, delay) - delay=delay+callAlt.duration+0.5 + if not soundoff then + if callAlt then + self:Sound2Player(playerData, self.LSORadio, callAlt, false, delay) + delay=delay+callAlt.duration+0.5 + end + if callSpeed then + self:Sound2Player(playerData, self.LSORadio, callSpeed, false, delay) + delay=delay+callSpeed.duration+0.5 + end + if callAoA then + self:Sound2Player(playerData, self.LSORadio, callAoA, false, delay) + delay=delay+callAoA.duration+0.5 + end + if callDist then + self:Sound2Player(playerData, self.LSORadio, callDist, false, delay) + delay=delay+callDist.duration+0.5 + end end - if callSpeed then - self:Sound2Player(playerData, self.LSORadio, callSpeed, false, delay) - delay=delay+callSpeed.duration+0.5 - end - if callAoA then - self:Sound2Player(playerData, self.LSORadio, callAoA, false, delay) - delay=delay+callAoA.duration+0.5 - end - if callDist then - self:Sound2Player(playerData, self.LSORadio, callDist, false, delay) - delay=delay+callDist.duration+0.5 - end - + -- ARC IN info. if playerData.step==AIRBOSS.PatternStep.ARCIN then @@ -11500,6 +11700,11 @@ function AIRBOSS:_Debrief(playerData) mygrade.osdate=os.date() --os.date("%d.%m.%Y") end + -- Save trap sheet. + if playerData.trapon and self.trapsheet then + self:_SaveTrapSheet(playerData, mygrade) + end + -- Add LSO grade to player grades table. table.insert(self.playerscores[playerData.name], mygrade) @@ -11510,7 +11715,7 @@ function AIRBOSS:_Debrief(playerData) end -- Wire and Groove time only if not pattern WO. - if not (playerData.patternwo or playerData.fouldeckwo) then + if not (playerData.wop or playerData.wofd) then -- Wire trapped. Not if pattern WI. if playerData.wire and playerData.wire<=4 then @@ -11519,7 +11724,7 @@ function AIRBOSS:_Debrief(playerData) -- Time in the groove. Only Case I/II and not pattern WO. if playerData.Tgroove and playerData.Tgroove<=360 and playerData.case<3 then - text=text..string.format("\nTime in the groove %d seconds: %s", playerData.Tgroove, self:_EvalGrooveTime(playerData)) + text=text..string.format("\nTime in the groove %.1f seconds: %s", playerData.Tgroove, self:_EvalGrooveTime(playerData)) end end @@ -11541,7 +11746,7 @@ function AIRBOSS:_Debrief(playerData) -- Check what happened? - if playerData.patternwo then + if playerData.wop then ---------------------- -- Pattern Wave Off -- @@ -11597,7 +11802,7 @@ function AIRBOSS:_Debrief(playerData) end - elseif playerData.fouldeckwo then + elseif playerData.wofd then --------------- -- Foul Deck -- @@ -11614,10 +11819,30 @@ function AIRBOSS:_Debrief(playerData) self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) -- Airboss talkto! - local text=string.format("the deck was fouled but landed anyway. Airboss wants to talk to you!") + local text=string.format("deck was fouled but you landed anyway. Airboss wants to talk to you!") self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) end + + elseif playerData.owo then + + ------------------ + -- Own Wave Off -- + ------------------ + + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + else + + -- Welcome aboard! + -- NOTE: This should not happen as owo is only triggered if player flew past the carrier. + self:E(self.lid.."ERROR: player landed when OWO was issues. This should not happen. Please report!") + self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + + end elseif playerData.waveoff then @@ -12060,47 +12285,18 @@ function AIRBOSS:_GetETAatNextWP() return eta end - ---- Check if heading or position of carrier have changed significantly. +--- Check if carrier is turning. If turning started or stopped, we inform the players via radio message. -- @param #AIRBOSS self -function AIRBOSS:_CheckPatternUpdate() +function AIRBOSS:_CheckCarrierTurning() - -- 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 - -- Starting to turn. + -- Check if turning stopped. (Carrier was turning but is not any more.) + if self.turning and not turning then + + -- Get final bearing. + local FB=self:GetFinalBearing(true) + + -- Marshal radio call: "99, new final bearing XYZ degrees." + self:_MarshalCallNewFinalBearing(FB) + + end + + -- Check if turning started. (Carrier was not turning and is now.) if turning and not self.turning then - -- Turning! - self.turning=true -- Get heading. local hdg if self.turnintowind then - hdg=select(1,self:GetWind()) + -- We are now steaming into the wind. + hdg=self:GetHeadingIntoWind(false) else + -- We turn towards the next waypoint. hdg=self:GetCoordinate():HeadingTo(self:_GetNextWaypoint()) end @@ -12130,32 +12337,86 @@ function AIRBOSS:_CheckPatternUpdate() hdg=360+hdg end - -- Radio call: "99, starting turn to heading XYZ". + -- Radio call: "99, Carrier starting turn to heading XYZ degrees". self:_MarshalCallCarrierTurnTo(hdg) end - -- No update if carrier is turning! - if turning then - self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) + -- Update turning. + self.turning=turning +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°. + local Hupdate=5 + + ----------------------- + -- Time Update Check -- + ----------------------- + + -- Time since last pattern update + local dt=timer.getTime()-self.Tpupdate + + -- Check whether at least 10 min between updates and not turning currently. + if dt=Hupdate then - self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) + self:T(self.lid..string.format("Carrier heading changed by %d°.", deltaHeading)) Hchange=true end + --------------------------- + -- Distance Update Check -- + --------------------------- + + -- Get current position and orientation of carrier. + local pos=self:GetCoordinate() + -- 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))) + self:T(self.lid..string.format("Carrier position changed by %.1f NM.", UTILS.MetersToNM(dist))) Dchange=true end + + ---------------------------- + -- Update Marshal Flights -- + ---------------------------- -- If heading or distance changed ==> update marshal AI patterns. if Hchange or Dchange then @@ -12171,18 +12432,6 @@ function AIRBOSS:_CheckPatternUpdate() end - -- Inform player about new final bearing. - if Hchange then - -- Get final bearing. - local FB=self:GetFinalBearing(true) - - -- Marshal radio call: "99, new final bearing XYZ degrees." - self:_MarshalCallNewFinalBearing(FB) - - -- Not turning any more. - self.turning=false - end - -- Reset parameters for next update check. self.Corientation=vNew self.Cposition=pos @@ -13037,7 +13286,7 @@ function AIRBOSS:Broadcast(radio, call, loud) 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. + -- 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. @@ -13190,42 +13439,48 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) else + -- Wait until previous sound finished. + local wait=0 + + -- Onboard number to get the attention. if receiver==playerData.onboard then - -- Sound only to player group. + -- Which voice over number to use. if sender and (sender=="LSO" or sender=="MARSHAL" or sender=="AIRBOSS") then -- User sound of board number. - local wait=self:_Number2Sound(playerData, sender, receiver) - - -- Negative response. - if string.find(text, "negative") then - local filename=self:_RadioFilename(self.MarshalCall.NEGATIVE) - USERSOUND:New(filename):ToGroup(playerData.group, wait) - wait=wait+self.MarshalCall.NEGATIVE.duration - end - - -- Positive response. - if string.find(text, "affirm") then - local filename=self:_RadioFilename(self.MarshalCall.AFFIRMATIVE) - USERSOUND:New(filename):ToGroup(playerData.group, wait) - wait=wait+self.MarshalCall.AFFIRMATIVE.duration - end - - -- Positive response. - if string.find(text, "roger") then - local filename=self:_RadioFilename(self.MarshalCall.ROGER) - USERSOUND:New(filename):ToGroup(playerData.group, wait) - wait=wait+self.MarshalCall.ROGER.duration - end - - -- Play click sound to end message. - local filename=self:_RadioFilename(self.MarshalCall.CLICK) - USERSOUND:New(filename):ToGroup(playerData.group, wait) - - end + wait=wait+self:_Number2Sound(playerData, sender, receiver) + + end end + -- Negative. + if string.find(text:lower(), "negative") then + local filename=self:_RadioFilename(self.MarshalCall.NEGATIVE) + USERSOUND:New(filename):ToGroup(playerData.group, wait) + wait=wait+self.MarshalCall.NEGATIVE.duration + end + + -- Affirm. + if string.find(text:lower(), "affirm") then + local filename=self:_RadioFilename(self.MarshalCall.AFFIRMATIVE) + USERSOUND:New(filename):ToGroup(playerData.group, wait) + wait=wait+self.MarshalCall.AFFIRMATIVE.duration + end + + -- Roger. + if string.find(text:lower(), "roger") then + local filename=self:_RadioFilename(self.MarshalCall.ROGER) + USERSOUND:New(filename):ToGroup(playerData.group, wait) + wait=wait+self.MarshalCall.ROGER.duration + end + + -- Play click sound to end message. + if wait>0 then + local filename=self:_RadioFilename(self.MarshalCall.CLICK) + USERSOUND:New(filename):ToGroup(playerData.group, wait) + end + -- Text message to player client. if playerData.client then MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) @@ -13275,7 +13530,7 @@ function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, de end ---- Generate a new radio call (deepcopy) from an existing default call. P +--- Generate a new radio call (deepcopy) from an existing default call. -- @param #AIRBOSS self -- @param #AIRBOSS.RadioCall call Radio call to be enhanced. -- @param #string sender Sender of the message. Default is the radio alias. @@ -13967,6 +14222,7 @@ function AIRBOSS:_AddF10Commands(_unitName) 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 + missionCommands.addCommandForGroup(gid, "Trapsheet On/Off", _helpPath, self._TrapsheetOnOff, self, _unitName) -- F8 ------------------------------------- -- F10/Airboss//F2 Kneeboard @@ -14434,7 +14690,7 @@ function AIRBOSS:_RequestRefueling(_unitName) -- Inform section and set step. for _,sec in pairs(playerData.section) do - local sectext="Follow your section leader to the tanker." + local sectext="follow your section leader to the tanker." self:MessageToPlayer(sec, sectext, "MARSHAL") self:_SetPlayerStep(sec, AIRBOSS.PatternStep.REFUELING) end @@ -14899,8 +15155,6 @@ function AIRBOSS:_DisplayQueue(_unitname, qname) end - - --- Report information about carrier. -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. @@ -14960,7 +15214,10 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) -- Only include current and future recovery windows. if Tabs=5 then -- Break the loop after 5 recovery times. @@ -14981,10 +15238,19 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) end end + -- Carrier FSM state. Idle is not clear enough. + local state=self:GetState() + if state=="Idle" then + state="Deck closed" + end + if self.turning then + state=state.." (turning currently)" + end + -- 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("Carrier state: %s\n", state) if self.case==1 then text=text..string.format("Case %d recovery ops\n", self.case) else @@ -15119,7 +15385,7 @@ function AIRBOSS:_SetDifficulty(_unitname, difficulty) if playerData then playerData.difficulty=difficulty - local text=string.format("your skill level is now: %s.", difficulty) + local text=string.format("roger, 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)) @@ -15158,9 +15424,9 @@ function AIRBOSS:_SetHintsOnOff(_unitname) -- Inform player. local text="" if playerData.showhints==true then - text=string.format("hints are now ON.") + text=string.format("roger, hints are now ON.") else - text=string.format("hints are now OFF.") + text=string.format("affirm, hints are now OFF.") end self:MessageToPlayer(playerData, text, nil, playerData.name, 5) @@ -15190,7 +15456,7 @@ function AIRBOSS:_DisplayAttitude(_unitname) end ---- Turn radio subtitles of player on or off +--- Turn radio subtitles of player on or off. -- @param #AIRBOSS self -- @param #string _unitname Name of the player unit. function AIRBOSS:_SubtitlesOnOff(_unitname) @@ -15210,9 +15476,9 @@ function AIRBOSS:_SubtitlesOnOff(_unitname) -- Inform player. local text="" if playerData.subtitles==true then - text=string.format("subtitiles are now ON.") + text=string.format("roger, subtitiles are now ON.") elseif playerData.subtitles==false then - text=string.format("subtitiles are now OFF.") + text=string.format("affirm, subtitiles are now OFF.") end self:MessageToPlayer(playerData, text, nil, playerData.name, 5) end @@ -15220,6 +15486,48 @@ function AIRBOSS:_SubtitlesOnOff(_unitname) end +--- Turn radio subtitles of player on or off. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_TrapsheetOnOff(_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 + + -- Check if option is enabled at all. + local text="" + if self.trapsheet then + + -- Invert current setting. + playerData.trapon=not playerData.trapon + + -- Inform player. + if playerData.trapon==true then + text=string.format("roger, your trapsheets are now SAVED.") + else + text=string.format("affirm, your trapsheets are NOT SAVED.") + end + + else + text="negative, trap sheet data recorder is broken on this carrier." + end + + -- Message to player. + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + end + end + +end + --- Display player status. -- @param #AIRBOSS self @@ -15363,30 +15671,36 @@ function AIRBOSS:_MarkMarshalZone(_unitName, flare) -- Flare and smoke at the ground. patternalt=5 + -- Roger! + text="roger, marking" if flare then - text=text.."Marking Marshal zone with WHITE flares." + + -- Marshal WHITE flares. + text=text..string.format("\n* Marshal zone stack %d with WHITE flares.", stack) zoneHolding:FlareZone(FLARECOLOR.White, 45, nil, patternalt) - if playerData.case==1 then - text=text.."\nMarking Commence zone with RED flares." - zoneThree:FlareZone(FLARECOLOR.Red, 45, nil, patternalt) - end + -- Commence RED flares. + text=text.."\n* Commence zone with RED flares." + zoneThree:FlareZone(FLARECOLOR.Red, 45, nil, patternalt) + else - text="Marking Marshal zone with WHITE smoke." + + -- Marshal WHITE smoke. + text=text..string.format("\n* Marshal zone stack %d with WHITE smoke.", stack) zoneHolding:SmokeZone(SMOKECOLOR.White, 45, patternalt) - if playerData.case==1 then - text=text.."\nMarking Commence zone with RED smoke." - zoneThree:SmokeZone(SMOKECOLOR.Red, 45, patternalt) - end + -- Commence RED smoke + text=text.."\n* Commence zone with RED smoke." + zoneThree:SmokeZone(SMOKECOLOR.Red, 45, patternalt) + end else - text="You are currently not in a marshal stack. No zone to mark!" + text="negative, you are currently not in a Marshal stack. No zones will be marked!" end -- Send message to player. - self:MessageToPlayer(playerData, text, "MARSHAL", "") + self:MessageToPlayer(playerData, text, "MARSHAL", playerData.name) end end @@ -15412,7 +15726,7 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) local case=playerData.case -- Initial - local text=string.format("Marking CASE %d zones\n", case) + local text=string.format("affirm, marking CASE %d zones", case) -- Flare or smoke? if flare then @@ -15423,25 +15737,25 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) -- Case I/II: Initial if case==1 or case==2 then - text=text.."* initial with GREEN flares\n" + text=text.."\n* initial with GREEN flares" 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" + text=text.."\n* approach corridor with GREEN flares" self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) end -- Case II/III: platform if case==2 or case==3 then - text=text.."* platform with RED flares\n" + text=text.."\n* platform with RED flares" self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) end -- Case III: dirty up if case==3 then - text=text.."* dirty up with YELLOW flares\n" + text=text.."\n* dirty up with YELLOW flares" self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow, 45) end @@ -15449,26 +15763,26 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) if case==2 or case==3 then if math.abs(self.holdingoffset)>0 then self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.White, 45) - text=text.."* arc turn in with WHITE flares\n" + text=text.."\n* arc turn in with WHITE flares" self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) - text=text.."* arc trun out with WHITE flares\n" + text=text.."\n* arc trun out with WHITE flares" end end -- Case III: bullseye if case==3 then - text=text.."* bullseye with GREEN flares\n" + text=text.."\n* bullseye with GREEN flares" self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.Green, 45) end -- Tarawa landing spots. if self.carriertype==AIRBOSS.CarrierType.TARAWA then - text=text.."* abeam landing stop with RED flares\n" + text=text.."\n* abeam landing stop with RED flares" -- Abeam landing spot zone. local ALSPT=self:_GetZoneAbeamLandingSpot() ALSPT:FlareZone(FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters(110)) -- Primary landing spot zone. - text=text.."* primary landing spot with GREEN flares\n" + text=text.."\n* primary landing spot with GREEN flares" local LSPT=self:_GetZoneLandingSpot() LSPT:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) end @@ -15481,19 +15795,19 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) -- Case I/II: Initial if case==1 or case==2 then - text=text.."* initial with GREEN smoke\n" + text=text.."\n* initial with GREEN smoke" 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" + text=text.."\n* approach corridor with GREEN smoke" self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) end -- Case II/III: platform if case==2 or case==3 then - text=text.."* platform with RED smoke\n" + text=text.."\n* platform with RED smoke" self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) end @@ -15501,28 +15815,28 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) if case==2 or case==3 then if math.abs(self.holdingoffset)>0 then self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) - text=text.."* arc turn in with BLUE smoke\n" + text=text.."\n* arc turn in with BLUE smoke" self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) - text=text.."* arc trun out with BLUE smoke\n" + text=text.."\n* arc trun out with BLUE smoke" end end -- Case III: dirty up if case==3 then - text=text.."* dirty up with ORANGE smoke\n" + text=text.."\n* dirty up with ORANGE smoke" self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) end -- Case III: bullseye if case==3 then - text=text.."* bullseye with GREEN smoke\n" + text=text.."\n* bullseye with GREEN smoke" self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Green, 45) end end -- Send message to player. - self:MessageToPlayer(playerData, text, "MARSHAL", "") + self:MessageToPlayer(playerData, text, "MARSHAL", playerData.name) end end @@ -15571,6 +15885,86 @@ end -- Persistence Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +--- Save trapsheet data. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #AIRBOSS.LSOgrade grade LSO grad data. +function AIRBOSS:_SaveTrapSheet(playerData, grade) + + -- Nothing to save. + if playerData.trapsheet==nil or #playerData.trapsheet==0 or not io then + return + end + + --- 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. + local path=self.trappath + if lfs then + path=path or lfs.writedir() + end + + + -- Create unused file name. + local filename=nil + for i=1,9999 do + + -- Create file name + filename=string.format("AIRBOSS-%s_Trapsheet-%s_%s-%04d.csv", self.alias, playerData.name, playerData.actype, i) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local _exists=UTILS.FileExists(filename) + if not _exists then + break + end + end + + + -- Info + local text=string.format("Saving player %s trapsheet to file %s", playerData.name, filename) + self:I(self.lid..text) + + -- Header line + local data="#Time,Rho,X,Z,Alt,AoA,GSE,LUE,Vtot,Vy,Gamma,Pitch,Roll,Yaw,Step\n" + + local g0=playerData.trapsheet[1] --#AIRBOSS.GrooveData + local T0=g0.Time + + for _,_groove in ipairs(playerData.trapsheet) do + local groove=_groove --#AIRBOSS.GrooveData + local t=groove.Time-T0 + local a=UTILS.MetersToNM(groove.Rho) + local b=-groove.X + local c=groove.Z + local d=UTILS.MetersToFeet(groove.Alt) + local e=groove.AoA + local f=groove.GSE + local g=groove.LUE + local h=UTILS.MpsToKnots(groove.Vel) + local i=groove.Vy*196.85 + local j=groove.Gamma + local k=groove.Pitch + local l=groove.Roll + local m=groove.Yaw + local n=groove.Step + -- t a b c d e f g h i j k l m n + data=data..string.format("%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s\n",t,a,b,c,d,e,f,g,h,i,j,k,l,m,n) + end + + -- Save file. + _savefile(filename, data) +end + --- On before "Save" event. Checks if io and lfs are available. -- @param #AIRBOSS self -- @param #string From From state. @@ -15629,7 +16023,7 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) self:I(self.lid..text) -- Header line - local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type, Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n" + local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type,Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n" -- Loop over all players. for playername,grades in pairs(self.playerscores) do @@ -15655,7 +16049,6 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) end -- Compile grade line. - --scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d\n", scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case, grade.wind, grade.modex, grade.airframe, grade.carriertype, grade.carriername, grade.theatre, grade.mitime, grade.midate, grade.osdate) From 775757e65784d389012e827f63ebc94196c1964b Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 26 Feb 2019 16:58:57 +0100 Subject: [PATCH 178/485] AB Docs --- Moose Development/Moose/Ops/Airboss.lua | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 7c11390ce..29cf4cd2b 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -589,7 +589,7 @@ -- Furthermore, we have the cases: -- -- * 2.5 Points **B**: "Bolder", when the player landed but did not catch a wire. --- * 2.0 Points **PWO**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. +-- * 2.0 Points **WOP**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. -- * 2.0 Points **OWO**: "Own Wave-Off**, when pilot flies past the deck without touching it. -- * 1.0 Points **WO**: "Technique Wave-Off": Player got waved off in the final parts of the groove. -- * 1.0 Points **LIG**: "Long In the Groove", when pilot extents the downwind leg too far and screws up the timing for the following aircraft. @@ -819,13 +819,28 @@ -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetLUE.png) -- --- The graph displayes the lineup error as a function of the distance to the carrier. +-- The graph displays the lineup error (LUE) as a function of the distance to the carrier. +-- +-- The pilot approaches the carrier from the port side, LUE>0°, at a distance of ~1 NM. +-- At the beginning of the groove (X), he significantly overshoots to the starboard side (LUE<5°). +-- In the middle (IM), he performs good corrections and smoothly reduces the lineup error. +-- Finally, at a distance of ~0.3 NM (IC) he has corrected his lineup with the runway to a reasonable level, |LUE|<0.5°. -- -- ## Glideslope Error -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetGSE.png) -- --- -- The graph displayes the glideslope error as a function of the distance to the carrier. +-- The graph displays the glideslope error (GLE) as a function of the distance to the carrier. +-- +-- In this case the pilot already enters the groove (X) below the optimal glideslope. He is not able to correct his height in the IM part and +-- stays significantly too low. In close, he performs a harsh correction to gain altitude and ends up even slightly too high (GLE>0.5°). +-- At his point further corrections are necessary. +-- +-- ## Angle of Attack +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetAoA.png) +-- +-- The graph displays the angle of attack (AoA) as a function of the distance to the carrier. -- -- === -- From e6e0cf5977e761b63d5a5e3280ece991bbc26a33 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 27 Feb 2019 16:33:24 +0100 Subject: [PATCH 179/485] AB 0991 --- Moose Development/Moose/Ops/Airboss.lua | 81 ++++++++++++++------- Moose Development/Moose/Utilities/Utils.lua | 12 +++ 2 files changed, 65 insertions(+), 28 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 29cf4cd2b..0b315c6c7 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -436,6 +436,11 @@ -- This command toggles the display of radio message subtitles if no radio relay unit is used. By default subtitles are on. -- Note that subtitles for radio messages which do not have a complete voice over are always displayed. -- +-- ### Trapsheet On/Off +-- +-- Each player can activated or deactivate the recording of his flight data (AoA, glideslope, lineup, etc.) during his landing approaches. +-- Note that this feature also has to be enabled by the mission designer. +-- -- ## Kneeboard Menu -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuKneeboard.png) @@ -828,12 +833,12 @@ -- -- ## Glideslope Error -- --- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetGSE.png) +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetGLE.png) -- --- The graph displays the glideslope error (GLE) as a function of the distance to the carrier. +-- The graph displays the glideslope error (GSE) as a function of the distance to the carrier. -- -- In this case the pilot already enters the groove (X) below the optimal glideslope. He is not able to correct his height in the IM part and --- stays significantly too low. In close, he performs a harsh correction to gain altitude and ends up even slightly too high (GLE>0.5°). +-- stays significantly too low. In close, he performs a harsh correction to gain altitude and ends up even slightly too high (GSE>0.5°). -- At his point further corrections are necessary. -- -- ## Angle of Attack @@ -841,6 +846,9 @@ -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetAoA.png) -- -- The graph displays the angle of attack (AoA) as a function of the distance to the carrier. +-- +-- The pilot starts off being on speed after the ball call. Then he get way to fast troughout the most part of the groove. He manages to correct +-- this somewhat short before touchdown. -- -- === -- @@ -1468,6 +1476,7 @@ AIRBOSS.Difficulty={ -- @field #number Vel Total velocity in m/s. -- @field #number Vy Vertical velocity in m/s. -- @field #number Gamma Relative heading player to carrier's runway. 0=parallel, +-90=perpendicular. +-- @field #string Grade LSO grade details. -- @field #string FlyThrough Fly through up "/" or fly through down "\\". --- LSO grade data. @@ -1575,7 +1584,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9" +AIRBOSS.version="0.9.9.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -8632,6 +8641,7 @@ function AIRBOSS:_GetGrooveData(playerData) groovedata.Vel=UTILS.VecNorm(vel) groovedata.Vy=vel.y groovedata.Gamma=self:_GetRelativeHeading(playerData.unit, true) + groovedata.Grade=select(3, self:_LSOgrade(playerData)) return groovedata end @@ -8875,6 +8885,7 @@ function AIRBOSS:_Groove(playerData) -- Get current groove data. local gd=playerData.groove[gs] --#AIRBOSS.GrooveData + if gd then self:T3(gd) @@ -10319,37 +10330,49 @@ function AIRBOSS:GetWind(alt, magnetic) return Wdir, Wspeed end ---- Get wind direction and speed on carrier deck. +--- Get wind speed on carrier deck parallel and perpendicular to runway. -- @param #AIRBOSS self -- @param #number alt Altitude in meters. Default 50 m. --- @return #number Direction the wind is blowing **from** in degrees. --- @return #number Wind speed in m/s. +-- @return #number Wind component parallel to runway im m/s. +-- @return #number Wind component perpendicular to runway in m/s. +-- @return #number Total wind strength in m/s. function AIRBOSS:GetWindOnDeck(alt) - -- Position of carrier + -- Position of carrier. local cv=self:GetCoordinate() -- Velocity vector of carrier. local vc=self.carrier:GetVelocityVec3() - -- Rotate to angled deck. - vc=UTILS.Rotate2D(vc, self.carrierparam.rwyangle) + -- Carrier orientation X. + local xc=self.carrier:GetOrientationX() + + -- Carrier orientation Z. + local zc=self.carrier:GetOrientationZ() + + -- Rotate back so that angled deck points to wind. + xc=UTILS.Rotate2D(xc, -self.carrierparam.rwyangle) + zc=UTILS.Rotate2D(zc, -self.carrierparam.rwyangle) -- Wind (from) vector local vw=cv:GetWindWithTurbulenceVec3(alt or 50) + -- Total wind velocity vector. -- Carrier velocity has to be negative. If carrier drives in the direction the wind is blowing from, we have less wind in total. - local vd=UTILS.VecSubstract(vw, vc) - - local vh=math.deg(math.atan2(vd.z, vd.x)) - if vh<0 then - vh=vh+360 - end + local vT=UTILS.VecSubstract(vw, vc) - -- Strength. - local vabs=UTILS.VecNorm(vd) + -- || Parallel component. + local vpa=UTILS.VecDot(vT,xc) - return vh, vabs + -- == Perpendicular component. + local vpp=UTILS.VecDot(vT,zc) + + -- Strength. + local vabs=UTILS.VecNorm(vT) + + -- We return positive values as head wind and negative values as tail wind. + --TODO: Check minus sign. + return -vpa, vpp, vabs end @@ -11701,7 +11724,7 @@ function AIRBOSS:_Debrief(playerData) mygrade.finalscore=Points end mygrade.case=playerData.case - local _,windondeck=self:GetWindOnDeck() + local windondeck=self:GetWindOnDeck() mygrade.wind=tostring(UTILS.Round(UTILS.MpsToKnots(windondeck), 1)) mygrade.modex=playerData.onboard mygrade.airframe=playerData.actype @@ -15205,7 +15228,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) end -- Wind on flight deck - local wind=UTILS.MpsToKnots(select(2, self:GetWindOnDeck())) + local wind=UTILS.MpsToKnots(select(1, self:GetWindOnDeck())) -- Get groups, units in queues. local Nmarshal,nmarshal = self:_GetQueueInfo(self.Qmarshal, playerData.case) @@ -15328,8 +15351,10 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) -- Get Beaufort wind scale. local Bn,Bd=UTILS.BeaufortScale(Ws) - -- Wind on flight deck - local Wod=UTILS.MpsToKnots(select(2, self:GetWindOnDeck())) + -- Wind on flight deck. + local WodPA,WodPP=self:GetWindOnDeck() + local WodPA=UTILS.MpsToKnots(WodPA) + local WodPP=UTILS.MpsToKnots(WodPP) local WD=string.format('%03d°', Wd) local Ts=string.format("%d°C",T) @@ -15337,13 +15362,13 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) 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("Wind on deck %.1f knots\n", Wod) + text=text..string.format("Wind on deck || %.1f kts, == %.1f kts\n", WodPA, WodPP) text=text..string.format("QFE %.1f hPa = %s", P, tP) -- More info only reliable if Mission uses static weather. @@ -15972,8 +15997,8 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) local l=groove.Roll local m=groove.Yaw local n=groove.Step - -- t a b c d e f g h i j k l m n - data=data..string.format("%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s\n",t,a,b,c,d,e,f,g,h,i,j,k,l,m,n) + -- t a b c d e f g h i j k l m n o + data=data..string.format("%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s,%s\n",t,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o) end -- Save file. diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index f9af6400a..7abf26489 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -799,6 +799,18 @@ function UTILS.VecAngle(a, b) return math.deg(alpha) end +--- Calculate "heading" of a 3D vector in the X-Z plane. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @return #number Heading in degrees in [0,360). +function UTILS.VecHdg(a) + local h=math.deg(math.atan2(a.z, a.x)) + if h<0 then + h=h+360 + end + return h +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. From 71c7e15fc337041850d155de3160b6c5eda94bce Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 27 Feb 2019 23:32:46 +0100 Subject: [PATCH 180/485] Update Airboss.lua --- Moose Development/Moose/Ops/Airboss.lua | 49 ++++++++++++++++++------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 0b315c6c7..550be2637 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1476,7 +1476,9 @@ AIRBOSS.Difficulty={ -- @field #number Vel Total velocity in m/s. -- @field #number Vy Vertical velocity in m/s. -- @field #number Gamma Relative heading player to carrier's runway. 0=parallel, +-90=perpendicular. --- @field #string Grade LSO grade details. +-- @field #string Grade LSO grade. +-- @field #number GradePoints LSO grade points +-- @field #string GradeDetail LSO grade details. -- @field #string FlyThrough Fly through up "/" or fly through down "\\". --- LSO grade data. @@ -1826,7 +1828,7 @@ function AIRBOSS:New(carriername, alias) -- Debug trace. if false then - --self.Debug=true + self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) @@ -2548,8 +2550,12 @@ end -- @param #string path (Optional) Path where to save the trap sheets. -- @return #AIRBOSS self function AIRBOSS:SetTrapSheet(path) - self.trapsheet=true - self.trappath=path + if io then + self.trapsheet=true + self.trappath=path + else + self:E(self.lid.."ERROR: io is not desanitized. Cannot save trap sheet.") + end return self end @@ -6806,8 +6812,14 @@ end -- @param Wrapper.Unit#UNIT unit The aircraft unit that was recovered. -- @return #AIRBOSS.FlightGroup Flight group of element. function AIRBOSS:_RecoveredElement(unit) - local element, idx, flight=self:_GetFlightElement(unit:GetName()) --#AIRBOSS.FlightElement - element.recovered=true + + -- Get element of flight. + local element, idx, flight=self:_GetFlightElement(unit:GetName()) --#AIRBOSS.FlightElement + + -- Nil check. Could be if a helo landed or something else we dont know! + if element then + element.recovered=true + end return flight end @@ -8622,7 +8634,10 @@ function AIRBOSS:_GetGrooveData(playerData) end -- Velocity vector. - local vel=playerData.unit:GetVelocityVec3() + local vel=playerData.unit:GetVelocityVec3() + + -- Grade, points, details + local Gg,Gp,Gd=self:_LSOgrade(playerData) -- Gather pilot data. local groovedata={} --#AIRBOSS.GrooveData @@ -8641,7 +8656,9 @@ function AIRBOSS:_GetGrooveData(playerData) groovedata.Vel=UTILS.VecNorm(vel) groovedata.Vy=vel.y groovedata.Gamma=self:_GetRelativeHeading(playerData.unit, true) - groovedata.Grade=select(3, self:_LSOgrade(playerData)) + groovedata.Grade=Gg + groovedata.GradePoints=Gp + groovedata.GradeDetail=Gd return groovedata end @@ -12138,7 +12155,7 @@ function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed) local wp={} -- Pos1 is a bit into. - local pos1=pos0:Translate(500, pos0:HeadingTo(coord)) + local pos1=pos0:Translate(500, pos0:HeadingTo(coord)):Translate(500, self:GetHeading()) -- Create from/to waypoints. table.insert(wp, pos0:WaypointGround(cspeedkmh)) @@ -12161,6 +12178,7 @@ function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed) -- Debug mark. if self.Debug then + pos1:MarkToAll("Detour Pos1") coord:MarkToAll("Detour Point") end @@ -12199,7 +12217,7 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) local intowind=self:GetHeadingIntoWind(false) -- Translate current position. - local pos1=self:GetCoordinate():Translate(dist, intowind) + local pos1=self:GetCoordinate():Translate(dist, intowind):Translate(500, self:GetHeading()) -- Debug mark. if self.Debug then @@ -15975,7 +15993,7 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) self:I(self.lid..text) -- Header line - local data="#Time,Rho,X,Z,Alt,AoA,GSE,LUE,Vtot,Vy,Gamma,Pitch,Roll,Yaw,Step\n" + local data="#Time,Rho,X,Z,Alt,AoA,GSE,LUE,Vtot,Vy,Gamma,Pitch,Roll,Yaw,Step,Grade,Points,Details\n" local g0=playerData.trapsheet[1] --#AIRBOSS.GrooveData local T0=g0.Time @@ -15996,9 +16014,12 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) local k=groove.Pitch local l=groove.Roll local m=groove.Yaw - local n=groove.Step - -- t a b c d e f g h i j k l m n o - data=data..string.format("%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s,%s\n",t,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o) + local n=self:_GS(groove.Step, -1) + local o=groove.Grade + local p=groove.GradePoints + local q=groove.GradeDetail + -- t a b c d e f g h i j k l m n o p q + data=data..string.format("%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s,%s,%.1f,%s\n",t,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q) end -- Save file. From 25c7b1ddac2591610f05d973971cd00572559cf3 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 28 Feb 2019 22:38:17 +0100 Subject: [PATCH 181/485] AIRBOSS v0.9.9.1 --- Moose Development/Moose/Ops/Airboss.lua | 101 +++++++++++++++++++----- 1 file changed, 81 insertions(+), 20 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 550be2637..84f0ed431 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -10325,12 +10325,13 @@ end -- @param #AIRBOSS self -- @param #number alt Altitude ASL in meters. Default 50 m. -- @param #boolean magnetic Direction including magnetic declination. +-- @param Core.Point#COORDINATE coord (Optional) Coordinate at which to get the wind. Default is current carrier position. -- @return #number Direction the wind is blowing **from** in degrees. -- @return #number Wind speed in m/s. -function AIRBOSS:GetWind(alt, magnetic) +function AIRBOSS:GetWind(alt, magnetic, coord) - -- Current position of the carrier - local cv=self:GetCoordinate() + -- Current position of the carrier or input. + local cv=coord or self:GetCoordinate() -- Wind direction and speed. By default at 50 meters ASL. local Wdir, Wspeed=cv:GetWind(alt or 50) @@ -10396,11 +10397,12 @@ 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. +-- @param Core.Point#COORDINATE coord (Optional) Coodinate from which heading is calculated. Default is current carrier position. -- @return #number Carrier heading in degrees. -function AIRBOSS:GetHeadingIntoWind(magnetic) +function AIRBOSS:GetHeadingIntoWind(magnetic, coord) -- Get direction the wind is blowing from. This is where we want to go. - local windfrom, vwind=self:GetWind() + local windfrom, vwind=self:GetWind(nil, nil, coord) -- Actually, we want the runway in the wind. local intowind=windfrom-self.carrierparam.rwyangle @@ -12137,8 +12139,9 @@ end -- @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. -- @param #number uspeed Speed in knots after U-turn. Default is same as before. +-- @param Core.Point#COORDINATE tcoord Additional coordinate to make turn smoother. -- @return #AIRBOSS self -function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed) +function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed, tcoord) -- Current coordinate of the carrier. local pos0=self:GetCoordinate() @@ -12149,17 +12152,16 @@ function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed) -- Speed in km/h. local speedkmh=UTILS.KnotsToKmph(speed) local cspeedkmh=self.carrier:GetVelocityKMH() - local uspeedkmh=UTILS.KnotsToKmph(uspeed) + local uspeedkmh=UTILS.KnotsToKmph(uspeed or speed) -- Waypoint table. local wp={} - -- Pos1 is a bit into. - local pos1=pos0:Translate(500, pos0:HeadingTo(coord)):Translate(500, self:GetHeading()) - -- Create from/to waypoints. table.insert(wp, pos0:WaypointGround(cspeedkmh)) - table.insert(wp, pos1:WaypointGround(cspeedkmh)) + if tcoord then + table.insert(wp, tcoord:WaypointGround(cspeedkmh)) + end table.insert(wp, coord:WaypointGround(speedkmh)) -- If enabled, go back to where you came from. @@ -12178,7 +12180,9 @@ function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed) -- Debug mark. if self.Debug then - pos1:MarkToAll("Detour Pos1") + if tcoord then + tcoord:MarkToAll("Detour Tcoord") + end coord:MarkToAll("Detour Point") end @@ -12214,15 +12218,72 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) self:I(self.lid..string.format("Carrier steaming into the wind (%.1f kts). Distance=%.1f NM, Speed=%.1f knots, Time=%d sec.", UTILS.MpsToKnots(vwind), distNM, speedknots, time)) -- Get heading into the wind accounting for angled runway. - local intowind=self:GetHeadingIntoWind(false) - - -- Translate current position. - local pos1=self:GetCoordinate():Translate(dist, intowind):Translate(500, self:GetHeading()) + local hiw=self:GetHeadingIntoWind() + + -- Current heading. + local hdg=self:GetHeading() + + -- Heading difference. + local deltaH=self:_GetDeltaHeading(hdg, hiw) + + local Cv=self:GetCoordinate() + + local Ctiw=nil --Core.Point#COORDINATE + local Csoo=nil --Core.Point#COORDINATE + + -- Define path depending on turn angle. + if deltaH<45 then + -- Small turn. + + -- Point in the right direction to help turning. + Csoo=Cv:Translate(750, hdg):Translate(750, hiw) + + -- Heading into wind from Csoo. + local hsw=self:GetHeadingIntoWind(false, Csoo) + + -- Into the wind coord. + Ctiw=Csoo:Translate(dist, hsw) + + elseif deltaH<90 then + -- Medium turn. + + -- Point in the right direction to help turning. + Csoo=Cv:Translate(900, hdg):Translate(900, hiw) + + -- Heading into wind from Csoo. + local hsw=self:GetHeadingIntoWind(false, Csoo) + + -- Into the wind coord. + Ctiw=Csoo:Translate(dist, hsw) + + elseif deltaH<135 then + -- Large turn backwards. + + -- Point in the right direction to help turning. + -- TODO turn left or right? + Csoo=Cv:Translate(1200, hdg-90):Translate(1200, hiw) + + -- Heading into wind from Csoo. + local hsw=self:GetHeadingIntoWind(false, Csoo) + + -- Into the wind coord. + Ctiw=Csoo:Translate(dist, hsw) + + else + -- Huge turn backwards. + + -- Point in the right direction to help turning. + -- TODO turn left or right? + Csoo=Cv:Translate(1200, hdg+90):Translate(1200, hiw) + + -- Heading into wind from Csoo. + local hsw=self:GetHeadingIntoWind(false, Csoo) + + -- Into the wind coord. + Ctiw=Csoo:Translate(dist, hsw) - -- Debug mark. - if self.Debug then - pos1:MarkToAll("Into the wind point") end + -- Return to coordinate if collision is detected. self.Creturnto=self:GetCoordinate() @@ -12234,7 +12295,7 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) local vdownwind=UTILS.MpsToKnots(nextwp:GetVelocity()) -- Let the carrier make a detour from its route but return to its current position. - self:CarrierDetour(pos1, speedknots, uturn, vdownwind) + self:CarrierDetour(Ctiw, speedknots, uturn, vdownwind, Csoo) -- Set switch that we are currently turning into the wind. self.turnintowind=true From fb784a9a967fa3cf2fbe07dc905ed7fe7e8a037e Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 28 Feb 2019 23:06:03 +0100 Subject: [PATCH 182/485] AIRBOSS v0.9.9.1 --- Moose Development/Moose/Ops/Airboss.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 84f0ed431..416914a8d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -12274,7 +12274,7 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) -- Point in the right direction to help turning. -- TODO turn left or right? - Csoo=Cv:Translate(1200, hdg+90):Translate(1200, hiw) + Csoo=Cv:Translate(1200, hdg-90):Translate(1200, hiw) -- Heading into wind from Csoo. local hsw=self:GetHeadingIntoWind(false, Csoo) From 6f13aa2c5b0691ba0b287d05b6dcc401af7fb8af Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 3 Mar 2019 19:44:39 +0100 Subject: [PATCH 183/485] These changes will blast the hell out of A2G... this is going to be great. --- Moose Development/Moose/AI/AI_A2G_BAI.lua | 2 +- Moose Development/Moose/AI/AI_A2G_CAS.lua | 2 +- .../Moose/AI/AI_A2G_Dispatcher.lua | 175 ++++++++++-------- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 2 +- 4 files changed, 98 insertions(+), 83 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 79871ba52..027474c2c 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -144,7 +144,7 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit if AttackUnit then if AttackUnit:IsAlive() and AttackUnit:IsGround() then self:T( { "BAI Unit:", AttackUnit:GetName() } ) - AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) end end end diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 54e677576..221341608 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -141,7 +141,7 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit if AttackUnit then if AttackUnit:IsAlive() and AttackUnit:IsGround() then self:T( { "CAS Unit:", AttackUnit:GetName() } ) - AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) end end end diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 8cde9e245..abbfd61ff 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3169,14 +3169,13 @@ do -- AI_A2G_DISPATCHER --- -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:CountDefendersEngaged( AttackerDetection ) + function AI_A2G_DISPATCHER:CountDefendersEngaged( AttackerDetection, AttackerCount ) -- 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() @@ -3628,13 +3627,64 @@ do -- AI_A2G_DISPATCHER --- -- @param #AI_A2G_DISPATCHER self - function AI_A2G_DISPATCHER:onafterDefend( From, Event, To, AttackerDetection, DefendersTotal, DefendersEngaged, DefendersMissing, DefenderFriendlies, DefenseTaskType ) + function AI_A2G_DISPATCHER:HasDefenseLine( DefenseCoordinate, DetectedItem ) - self:F( { From, Event, To, AttackerDetection.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } ) + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) - AttackerDetection.Type = DefenseTaskType -- This is set to report the task type in the status panel. + -- Now check if this coordinate is not in a danger zone, meaning, that the attack line is not crossing other coordinates. + -- (y1 – y2)x + (x2 – x1)y + (x1y2 – x2y1) = 0 + + local c1 = DefenseCoordinate + local c2 = AttackCoordinate + + local a = c1.z - c2.z -- Calculate a + local b = c2.x - c1.x -- Calculate b + local c = c1.x * c2.z - c2.x * c1.z -- calculate c + + local ok = true + + -- Now we check if each coordinate radius of about 30km of each attack is crossing a defense line. If yes, then this is not a good attack! + for AttackItemID, CheckAttackItem in pairs( self.Detection:GetDetectedItems() ) do + + -- Only compare other detected coordinates. + if AttackItemID ~= DetectedItem.ID then + + local CheckAttackCoordinate = self.Detection:GetDetectedItemCoordinate( CheckAttackItem ) + + local x = CheckAttackCoordinate.x + local y = CheckAttackCoordinate.z + local r = 8000 + + -- now we check if the coordinate is intersecting with the defense line. + + local IntersectDistance = ( math.abs( a * x + b * y + c ) ) / math.sqrt( a * a + b * b ) + self:F( { IntersectDistance = IntersectDistance, x = x, y = y } ) + + local IntersectAttackDistance = CheckAttackCoordinate:Get2DDistance( DefenseCoordinate ) + + self:F( { IntersectAttackDistance=IntersectAttackDistance, EvaluateDistance=EvaluateDistance } ) + + -- If the distance of the attack coordinate is larger than the test radius; then the line intersects, and this is not a good coordinate. + if IntersectDistance < r and IntersectAttackDistance < EvaluateDistance then + ok = false + break + end + end + end + + return ok + end - local AttackerSet = AttackerDetection.Set + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterDefend( From, Event, To, DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, DefenderFriendlies, DefenseTaskType ) + + self:F( { From, Event, To, DetectedItem.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } ) + + DetectedItem.Type = DefenseTaskType -- This is set to report the task type in the status panel. + + local AttackerSet = DetectedItem.Set local AttackerUnit = AttackerSet:GetFirst() if AttackerUnit and AttackerUnit:IsAlive() then @@ -3642,18 +3692,25 @@ do -- AI_A2G_DISPATCHER 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( AttackerSet ) -- Engage on the TargetSetUnit - - self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) + -- Here we check if the defenders have a defense line to the attackers. + -- If the attackers are behind enemy lines or too close to an other defense line; then don´t engage. + local DefenseCoordinate = DefenderGroup:GetCoordinate() + local HasDefenseLine = self:HasDefenseLine( DefenseCoordinate, DetectedItem ) - local DefenderGroupSize = DefenderGroup:GetSize() - DefendersMissing = DefendersMissing - DefenderGroupSize / SquadronOverhead - DefendersTotal = DefendersTotal + DefenderGroupSize / SquadronOverhead + if HasDefenseLine == true then + local SquadronName = self:GetDefenderTask( DefenderGroup ).SquadronName + local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) + + local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) + Fsm:Engage( AttackerSet ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( DefenderGroup, DetectedItem ) + + local DefenderGroupSize = DefenderGroup:GetSize() + DefendersMissing = DefendersMissing - DefenderGroupSize / SquadronOverhead + DefendersTotal = DefendersTotal + DefenderGroupSize / SquadronOverhead + end if DefendersMissing <= 0 then break @@ -3676,21 +3733,26 @@ do -- AI_A2G_DISPATCHER if DefenderSquadron[DefenseTaskType] then - local SpawnCoord = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE + local AirbaseCoordinate = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE local AttackerCoord = AttackerUnit:GetCoordinate() - local InterceptCoord = AttackerDetection.InterceptCoord + local InterceptCoord = DetectedItem.InterceptCoord self:F( { InterceptCoord = InterceptCoord } ) if InterceptCoord then - local InterceptDistance = SpawnCoord:Get2DDistance( InterceptCoord ) - local AirbaseDistance = SpawnCoord:Get2DDistance( AttackerCoord ) + local InterceptDistance = AirbaseCoordinate:Get2DDistance( InterceptCoord ) + local AirbaseDistance = AirbaseCoordinate: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 + + -- Check if there is a defense line... + local HasDefenseLine = self:HasDefenseLine( AirbaseCoordinate, DetectedItem ) + if HasDefenseLine == true then + ClosestDistance = InterceptDistance + ClosestDefenderSquadronName = SquadronName + end end end end @@ -3735,7 +3797,7 @@ do -- AI_A2G_DISPATCHER end while ( DefendersNeeded > 0 ) do - self:ResourceQueue( false, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, ClosestDefenderSquadronName ) + self:ResourceQueue( false, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, DetectedItem, ClosestDefenderSquadronName ) DefendersNeeded = DefendersNeeded - DefenderGrouping DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead end -- while ( DefendersNeeded > 0 ) do @@ -3764,13 +3826,12 @@ do -- AI_A2G_DISPATCHER 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? + local AttackerCount = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group, then the amount of radar emitters will be returned; that need to be attacked. - if ( IsSEAD > 0 ) then + if ( AttackerCount > 0 ) then -- First, count the active defenders, engaging the DetectedItem. - local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) @@ -3805,7 +3866,7 @@ do -- AI_A2G_DISPATCHER if IsCas == true then -- First, count the active defenders, engaging the DetectedItem. - local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) @@ -3838,7 +3899,7 @@ do -- AI_A2G_DISPATCHER if IsBai == true then -- First, count the active defenders, engaging the DetectedItem. - local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) @@ -3854,6 +3915,8 @@ do -- AI_A2G_DISPATCHER 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. @@ -3900,7 +3963,6 @@ do -- AI_A2G_DISPATCHER 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 @@ -3940,52 +4002,8 @@ do -- AI_A2G_DISPATCHER self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then - - -- Now check if this coordinate is not in a danger zone, meaning, that the attack line is not crossing other coordinates. - -- (y1 – y2)x + (x2 – x1)y + (x1y2 – x2y1) = 0 - - local c1 = DefenseCoordinate - local c2 = AttackCoordinate - - local a = c1.z - c2.z -- Calculate a - local b = c2.x - c1.x -- Calculate b - local c = c1.x * c2.z - c2.x * c1.z -- calculate c - - local ok = true - - -- Now we check if each coordinate radius of about 30km of each attack is crossing a defense line. If yes, then this is not a good attack! - for AttackItemID, CheckAttackItem in pairs( Detection:GetDetectedItems() ) do - - -- Only compare other detected coordinates. - if AttackItemID ~= DetectedItemID then - - local CheckAttackCoordinate = self.Detection:GetDetectedItemCoordinate( CheckAttackItem ) - - local x = CheckAttackCoordinate.x - local y = CheckAttackCoordinate.z - local r = 8000 - - -- now we check if the coordinate is intersecting with the defense line. - - local IntersectDistance = ( math.abs( a * x + b * y + c ) ) / math.sqrt( a * a + b * b ) - self:F( { IntersectDistance = IntersectDistance, x = x, y = y } ) - - local IntersectAttackDistance = CheckAttackCoordinate:Get2DDistance( DefenseCoordinate ) - - self:F( { IntersectAttackDistance=IntersectAttackDistance, EvaluateDistance=EvaluateDistance } ) - - -- If the distance of the attack coordinate is larger than the test radius; then the line intersects, and this is not a good coordinate. - if IntersectDistance < r and IntersectAttackDistance < EvaluateDistance then - ok = false - break - end - end - end - - if ok == true then - EngageCoordinate = DefenseCoordinate - break - end + EngageCoordinate = DefenseCoordinate + break end end end @@ -3996,7 +4014,6 @@ do -- AI_A2G_DISPATCHER if DefendersMissing and DefendersMissing > 0 then self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD", EngageCoordinate ) - Delay = Delay + 1 end end @@ -4005,7 +4022,6 @@ do -- AI_A2G_DISPATCHER if DefendersMissing and DefendersMissing > 0 then self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS", EngageCoordinate ) - Delay = Delay + 1 end end @@ -4014,7 +4030,6 @@ do -- AI_A2G_DISPATCHER if DefendersMissing and DefendersMissing > 0 then self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI", EngageCoordinate ) - Delay = Delay + 1 end end end @@ -4030,7 +4045,7 @@ do -- AI_A2G_DISPATCHER if self.TacticalDisplay then -- Show tactical situation local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() - Report:Add( string.format( " - %s ( %s ): ( #%d - %4s ) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "â– ", ThreatLevel ) ) ) + Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "â– ", ThreatLevel ) ) ) 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_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index e47d09449..77a69ca10 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -189,7 +189,7 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni local HasRadar = AttackUnit:HasSEAD() if HasRadar then self:F( { "SEAD Unit:", AttackUnit:GetName() } ) - AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) end end end From b56d62c7770f6d55380c0fd4cb193e46e340caf6 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 4 Mar 2019 17:50:45 +0100 Subject: [PATCH 184/485] Carrier Ops Update AIRBOSS v0.9.9.2 RECOVERYTANKER v1.0.7 --- Moose Development/Moose/Ops/Airboss.lua | 231 ++++++++++++++---- .../Moose/Ops/RecoveryTanker.lua | 46 +++- Moose Development/Moose/Utilities/Utils.lua | 4 +- .../Moose/Wrapper/Controllable.lua | 46 +++- 4 files changed, 269 insertions(+), 58 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 416914a8d..2619aa3c1 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -234,9 +234,9 @@ -- -- The AIRBOSS class supports all three commonly used recovery cases, i.e. -- --- * **CASE I** during daytime and good weather, --- * **CASE II** during daytime but poor visibility conditions, --- * **CASE III** when below Case II conditions, e.g. during nighttime. +-- * **CASE I** during daytime and good weather (ceiling > 3000 ft, visibility > 5 NM), +-- * **CASE II** during daytime but poor visibility conditions (ceiling > 1000 ft, visibility > 5NM), +-- * **CASE III** when below Case II conditions and during nighttime (ceiling < 1000 ft, visibility < 5 NM). -- -- 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. -- @@ -819,6 +819,9 @@ -- * *Roll*: roll angle of player aircraft in degrees. -- * *Yaw*: yaw angle of player aircraft in degrees. -- * *Step*: Step in the groove. +-- * *Grade*: Current LSO grade. +-- * *Points*: Current points for the pass. +-- * *Details*: Detailed grading analysis. -- --## Lineup Error -- @@ -998,6 +1001,36 @@ -- -- === -- +-- # Finite State Machine (FSM) +-- +-- The AIRBOSS class has a Finite State Machine (FSM) implementation for the carrier. This allows mission designers to hook into certain events and helps +-- simulate complex behaviour easier. +-- +-- FSM events are: +-- +-- * @{#AIRBOSS.Start}: Starts the AIRBOSS FSM. +-- * @{#AIRBOSS.Stop}: Stops the AIRBOSS FSM. +-- * @{#AIRBOSS.Idle}: Carrier is set to idle and not recovering. +-- * @{#AIRBOSS.RecoveryStart}: Starts the recovery ops. +-- * @{#AIRBOSS.RecoveryStop}: Stops the recovery ops. +-- * @{#AIRBOSS.RecoveryPause}: Pauses the recovery ops. +-- * @{#AIRBOSS.RecoveryUnpause}: Unpauses the recovery ops. +-- * @{#AIRBOSS.RecoveryCase}: Sets/switches the recovery case. +-- * @{#AIRBOSS.PassingWaypoint}: Carrier passes a waypoint defined in the mission editor. +-- +-- These events can be used in the user script. When the event is triggered, it is automatically a function OnAfter*Eventname* called. For example +-- +-- --- Carrier just passed waypoint *n*. +-- function AirbossStennis:OnAfterPassingWaypoint(From, Event, To, n) +-- -- Launch green flare. +-- self.carrier:FlareGreen() +-- end +-- +-- In this example, we only launch a green flare every time the carrier passes a waypoint defined in the mission editor. But, of course, you can also use it to add new +-- recovery windows each time a carrier passes a waypoint. Therefore, you can create an "infinite" number of windows easily. +-- +-- === +-- -- # Examples -- -- In this section a few simple examples are given to illustrate the scripting part. @@ -1457,6 +1490,7 @@ AIRBOSS.Difficulty={ -- @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 #boolean UTURN If true, carrier makes a U-turn to the point it came from before resuming its route to the next waypoint. -- @field #number ID Recovery window ID. --- Groove data. @@ -1586,7 +1620,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.1" +AIRBOSS.version="0.9.9.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1832,7 +1866,7 @@ function AIRBOSS:New(carriername, alias) BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) BASE:TraceLevel(1) - self.dTstatus=0.1 + --self.dTstatus=0.1 end -- Smoke zones. @@ -1953,6 +1987,7 @@ function AIRBOSS:New(carriername, alias) 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("*", "PassingWaypoint", "*") -- Carrier is passing a waypoint. self:AddTransition("*", "Save", "*") -- Save player scores to file. self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS FMS. @@ -1966,6 +2001,13 @@ function AIRBOSS:New(carriername, alias) -- @param #AIRBOSS self -- @param #number delay Delay in seconds. + --- On after "Start" user function. Called when the AIRBOSS FSM is started. + -- @function [parent=#AIRBOSS] OnAfterStart + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + --- Triggers the FSM event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out. -- @function [parent=#AIRBOSS] Idle @@ -1999,6 +2041,7 @@ function AIRBOSS:New(carriername, alias) -- @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 -- @param #AIRBOSS self @@ -2044,6 +2087,27 @@ function AIRBOSS:New(carriername, alias) -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + --- Triggers the FSM event "PassingWaypoint". Called when the carrier passes a waypoint. + -- @function [parent=#AIRBOSS] PassingWaypoint + -- @param #AIRBOSS self + -- @param #number waypoint Number of waypoint. + + --- Triggers the FSM delayed event "PassingWaypoint". Called when the carrier passes a waypoint. + -- @function [parent=#AIRBOSS] __PassingWaypoint + -- @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 "PassingWaypoint" user function. Called when the carrier passes a waypoint of its route. + -- @function [parent=#AIRBOSS] OnAfterPassingWaypoint + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number waypoint Number of waypoint. + + --- Triggers the FSM event "Save" that saved the player scores to a file. -- @function [parent=#AIRBOSS] Save -- @param #AIRBOSS self @@ -2183,8 +2247,9 @@ end -- @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. +-- @param #boolean uturn If true (or nil), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint. -- @return #AIRBOSS.Recovery Recovery window. -function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, turnintowind, speed) +function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, turnintowind, speed, uturn) -- Absolute mission time in seconds. local Tnow=timer.getAbsTime() @@ -2234,6 +2299,12 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, tur recovery.SPEED=speed or 20 recovery.ID=self.windowcount + if uturn==nil or uturn==true then + recovery.UTURN=true + else + recovery.UTURN=false + end + -- Add to table table.insert(self.recoverytimes, recovery) @@ -2283,6 +2354,7 @@ function AIRBOSS:DeleteAllRecoveryWindows(delay) -- Loop over all recovery windows. for _,recovery in pairs(self.recoverytimes) do + env.info("FF deleting recovery window ID "..tostring(recovery.ID)) self:DeleteRecoveryWindow(recovery, delay) end @@ -2297,7 +2369,7 @@ function AIRBOSS:GetRecoveryWindowByID(id) if id then for _,_window in pairs(self.recoverytimes) do local window=_window --#AIRBOSS.Recovery - if window.ID==id then + if window and window.ID==id then return window end end @@ -2993,7 +3065,7 @@ end -- FSM event functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- On after Start event. Starts the AIRBOSS. Addes event handlers and schedules status updates of reqests and queue. +--- On after Start event. Starts the AIRBOSS. Adds event handlers and schedules status updates of requests and queue. -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. @@ -3017,7 +3089,7 @@ function AIRBOSS:onafterStart(From, Event, To) self.Tpupdate=timer.getTime() -- Init patrol route of carrier. - self:_PatrolRoute() + self:_PatrolRoute(1) -- Check if no recovery window is set. if #self.recoverytimes==0 then @@ -3441,6 +3513,7 @@ function AIRBOSS:_CheckRecoveryTimes() -- Check if there is a next windows defined. if nextwindow then + -- Set case and offset of the next window. self:RecoveryCase(nextwindow.CASE, nextwindow.OFFSET) @@ -3458,6 +3531,11 @@ function AIRBOSS:_CheckRecoveryTimes() if vwind<0.1 then uturn=false end + + -- U-turn disabled by user input. + if not nextwindow.UTURN then + uturn=false + end --Debug info self:T(self.lid..string.format("Heading=%03d°, Wind=%03d° %.1f kts, Delta=%03d° ==> U-turn=%s", hdg, wind,UTILS.MpsToKnots(vwind), delta, tostring(uturn))) @@ -3468,7 +3546,7 @@ function AIRBOSS:_CheckRecoveryTimes() local v=UTILS.KnotsToMps(nextwindow.SPEED) -- Check that we do not go above max possible speed. - local vmax=self.carrier:GetSpeedMax() + local vmax=self.carrier:GetSpeedMax()/3.6 -- convert to m/s v=math.min(v,vmax) -- Route carrier into the wind. Sets self.turnintowind=true @@ -3624,21 +3702,31 @@ function AIRBOSS:onafterRecoveryStop(From, Event, To) -- Debug output. 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 - -- Recovery ops stopped message. self:_MarshalCallRecoveryStopped(self.case) -- If carrier is currently heading into the wind, we resume the original route. if self.turnintowind then - self:CarrierResumeRoute(self.Creturnto) + + -- Coordinate to return to. + local coord=self.Creturnto + + -- No U-turn. + if self.recoverywindow and self.recoverywindow.UTURN==false then + coord=nil + end + + -- Carrier resumes route. + self:CarrierResumeRoute(coord) end + -- 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 + -- Check recovery windows. This sets self.recoverywindow to the next window. self:_CheckRecoveryTimes() end @@ -3691,6 +3779,16 @@ function AIRBOSS:onafterRecoveryUnpause(From, Event, To) end +--- On after "PassingWaypoint" event. Carrier has just passed a waypoint +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Number of waypoint that was passed. +function AIRBOSS:onafterPassingWaypoint(From, Event, To, n) + -- Debug output. + self:I(self.lid..string.format("Carrier passed waypoint %d.", n)) +end --- On after "Idle" event. Carrier goes to state "Idle". -- @param #AIRBOSS self @@ -10879,7 +10977,7 @@ function AIRBOSS:_LSOgrade(playerData) if playerData.lig then -- Long In the Groove (LIG). -- According to Stingers this is a CUT pass and gives 1.0 points. - grade="CUT" + grade="WO" points=1.0 G="LIG" else @@ -12146,25 +12244,36 @@ function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed, tcoord) -- Current coordinate of the carrier. local pos0=self:GetCoordinate() - -- Default. - speed=speed or self.carrier:GetVelocityKNOTS() + -- Current speed in knots. + local vel0=self.carrier:GetVelocityKNOTS() + + -- Default. If speed is not given we take the current speed but at least 5 knots. + speed=speed or math.max(vel0, 5) - -- Speed in km/h. - local speedkmh=UTILS.KnotsToKmph(speed) - local cspeedkmh=self.carrier:GetVelocityKMH() + -- Speed in km/h. At least 2 knots. + local speedkmh=math.max(UTILS.KnotsToKmph(speed), UTILS.KnotsToKmph(2)) + + -- Turn speed in km/h. At least 10 knots. + local cspeedkmh=math.max(self.carrier:GetVelocityKMH(), UTILS.KnotsToKmph(10)) + + -- U-turn speed in km/h. local uspeedkmh=UTILS.KnotsToKmph(uspeed or speed) -- Waypoint table. local wp={} - -- Create from/to waypoints. + -- Waypoint at current position. table.insert(wp, pos0:WaypointGround(cspeedkmh)) + + -- Waypooint to help the turn. if tcoord then table.insert(wp, tcoord:WaypointGround(cspeedkmh)) end + + -- Detour waypoint. table.insert(wp, coord:WaypointGround(speedkmh)) - -- If enabled, go back to where you came from. + -- U-turn waypoint. If enabled, go back to where you came from. if uturn then table.insert(wp, pos0:WaypointGround(uspeedkmh)) end @@ -12181,9 +12290,12 @@ function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed, tcoord) -- Debug mark. if self.Debug then if tcoord then - tcoord:MarkToAll("Detour Tcoord") + tcoord:MarkToAll("Detour Turn Help") end coord:MarkToAll("Detour Point") + if uturn then + pos0:MarkToAll(string.format("U-turn Return Point. Speed %.1f km/h", uspeedkmh)) + end end -- Detour switch true. @@ -12259,9 +12371,8 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) elseif deltaH<135 then -- Large turn backwards. - -- Point in the right direction to help turning. - -- TODO turn left or right? - Csoo=Cv:Translate(1200, hdg-90):Translate(1200, hiw) + -- Point in the right direction to help turning. + Csoo=Cv:Translate(1100, hdg-90):Translate(1000, hiw) -- Heading into wind from Csoo. local hsw=self:GetHeadingIntoWind(false, Csoo) @@ -12273,8 +12384,7 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) -- Huge turn backwards. -- Point in the right direction to help turning. - -- TODO turn left or right? - Csoo=Cv:Translate(1200, hdg-90):Translate(1200, hiw) + Csoo=Cv:Translate(1200, hdg-90):Translate(1000, hiw) -- Heading into wind from Csoo. local hsw=self:GetHeadingIntoWind(false, Csoo) @@ -12293,6 +12403,11 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) -- For downwind, we take the velocity at the next WP. local vdownwind=UTILS.MpsToKnots(nextwp:GetVelocity()) + + -- Make sure we move at all in case the speed at the waypoint is zero. + if vdownwind<1 then + vdownwind=10 + end -- Let the carrier make a detour from its route but return to its current position. self:CarrierDetour(Ctiw, speedknots, uturn, vdownwind, Csoo) @@ -12309,19 +12424,29 @@ end -- @return #number Number of waypoint function AIRBOSS:_GetNextWaypoint() - -- Next wp = current+1 (or last) - local Nnextwp=math.min(self.currentwp+1, #self.waypoints) + -- Next waypoint. + local Nextwp=nil + if self.currentwp==#self.waypoints then + Nextwp=1 + else + Nextwp=self.currentwp+1 + end + + -- Debug output + local text=string.format("Current WP=%d/%d, next WP=%d", self.currentwp, #self.waypoints, Nextwp) + self:T2(self.lid..text) -- Next waypoint. - local nextwp=self.waypoints[Nnextwp] --Core.Point#COORDINATE + local nextwp=self.waypoints[Nextwp] --Core.Point#COORDINATE - return nextwp,Nnextwp + return nextwp,Nextwp end --- Patrol carrier. -- @param #AIRBOSS self +-- @param #number n Current waypoint. -- @return #AIRBOSS self -function AIRBOSS:_PatrolRoute() +function AIRBOSS:_PatrolRoute(n) -- Get carrier group. local CarrierGroup=self.carrier:GetGroup() @@ -12338,10 +12463,12 @@ function AIRBOSS:_PatrolRoute() -- Call task function when carrier arrives at waypoint. CarrierGroup:SetTaskWaypoint(Waypoints[n], TaskPassingWP) end + + -- Init array. + self.waypoints={} -- Set waypoint table. - local i=1 - for _,point in ipairs(Waypoints) do + for i,point in ipairs(Waypoints) do -- Coordinate of the waypoint local coord=COORDINATE:New(point.x, point.alt, point.y) @@ -12357,12 +12484,10 @@ function AIRBOSS:_PatrolRoute() 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 + self.currentwp=n or 1 -- Route carrier group. CarrierGroup:Route(Waypoints) @@ -12581,12 +12706,15 @@ function AIRBOSS._PassingWaypoint(group, airboss, i, final) -- Set current waypoint. airboss.currentwp=i + -- Passing Waypoint event. + airboss:PassingWaypoint(i) + -- Reactivate beacons. --airboss:_ActivateBeacons() -- If final waypoint reached, do route all over again. if i==final and final>1 and airboss.adinfinitum then - airboss:_PatrolRoute() + airboss:_PatrolRoute(i) end end @@ -12599,15 +12727,17 @@ function AIRBOSS._ResumeRoute(group, airboss, gotocoord) -- Get next waypoint local nextwp,Nextwp=airboss:_GetNextWaypoint() - -- Velocity at that coordinate. + -- Speed set at waypoint. local speedkmh=nextwp.Velocity*3.6 + -- If speed at waypoint is zero, we set it to 10 knots. + if speedkmh<1 then + speedkmh=UTILS.KnotsToKmph(10) + end + -- Waypoints array. local waypoints={} - -- Get current velocity in km/h. - local velocity=group:GetVelocityKMH() - -- Current positon as first waypoint. local wp0=group:GetCoordinate():WaypointGround(speedkmh) table.insert(waypoints, wp0) @@ -12634,6 +12764,11 @@ function AIRBOSS._ResumeRoute(group, airboss, gotocoord) -- Speed in km/h of that WP. Velocity is in m/s. local speed=coord.Velocity*3.6 + -- If speed is zero we set it to 10 knots. + if speed<1 then + speed=UTILS.KnotsToKmph(10) + end + -- Create waypoint. local wp=coord:WaypointGround(speed) @@ -13873,7 +14008,7 @@ function AIRBOSS:_Number2Radio(radio, number, delay, interval) end ---- Inform everyone that recovery ops are stopped and deck is closed. +--- AI aircraft calls the ball. -- @param #AIRBOSS self -- @param #string modex Tail number. -- @param #string nickname Aircraft nickname. diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index d0fa243f9..1ad33047f 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -201,8 +201,8 @@ -- -- ## 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. +-- You can use the class also to have an AWACS orbiting overhead the carrier. This requires to add the @{#RECOVERYTANKER.SetAWACS}(*switch*, *eplrs*) function to the script, which sets the enroute tasks AWACS +-- as soon as the aircraft enters its pattern. Note that the EPLRS data link is enabled by default. To disable it, the second parameter *eplrs* must be set to *false*. -- -- A simple script could look like this: -- @@ -294,6 +294,7 @@ RECOVERYTANKER = { callsignname = nil, callsignnumber = nil, modex = nil, + eplrs = nil, } --- Unique ID (global). @@ -302,7 +303,7 @@ RECOVERYTANKER.UID=0 --- Class version. -- @field #string version -RECOVERYTANKER.version="1.0.6" +RECOVERYTANKER.version="1.0.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -598,16 +599,24 @@ 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. +-- @param #boolean eplrs If true or nil, enable EPLRS. If false, EPLRS will be off. -- @return #RECOVERYTANKER self -function RECOVERYTANKER:SetAWACS(switch) +function RECOVERYTANKER:SetAWACS(switch, eplrs) if switch==nil or switch==true then self.awacs=true else self.awacs=false end + if eplrs==nil or eplrs==true then + self.eplrs=true + else + self.eplrs=false + end + return self end + --- Set callsign of the tanker group. -- @param #RECOVERYTANKER self -- @param #number callsignname Number @@ -894,6 +903,12 @@ function RECOVERYTANKER:onafterStart(From, Event, To) self.tanker:CommandSetCallsign(self.callsignname, self.callsignnumber, 2) end + -- Turn EPLRS datalink on. + if self.eplrs then + self.tanker:CommandEPLRS(true, 3) + end + + -- Get initial orientation and position of carrier. self.orientation=self.carrier:GetOrientationX() self.orientlast=self.carrier:GetOrientationX() @@ -954,6 +969,9 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) -- Respawn tanker. 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. + self:__PatternUpdate(2) + -- Create tanker beacon and activate TACAN. if self.TACANon then self:_ActivateTACAN(3) @@ -964,8 +982,10 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) 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) + -- Turn EPLRS datalink on. + if self.eplrs then + self.tanker:CommandEPLRS(true, 4) + end end else @@ -1066,11 +1086,18 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To) self.tanker:WayPointInitialize(wp) -- Task combo. + + -- Be a tanker or be an AWACS. local taskroll = self.tanker:EnRouteTaskTanker() if self.awacs then taskroll=self.tanker:EnRouteTaskAWACS() end + + --local taskeplrs=self.tanker:TaskEPLRS(true, 2) + + -- Route task. local taskroute = self.tanker:TaskRoute(wp) + -- Note that the order is important here! tasktanker has to come first. Otherwise it does not work. local taskcombo = self.tanker:TaskCombo({taskroll, taskroute}) @@ -1184,7 +1211,12 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) -- Set callsign. if self.callsignname then self.tanker:CommandSetCallsign(self.callsignname, self.callsignnumber, 3) - end + end + + -- Turn EPLRS datalink on. + if self.eplrs then + self.tanker:CommandEPLRS(true, 4) + 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 7abf26489..9070b0087 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -345,11 +345,11 @@ UTILS.MpsToMiph = function( mps ) end UTILS.MpsToKnots = function( mps ) - return mps * 3600 / 1852 + return mps * 1.94384 --3600 / 1852 end UTILS.KnotsToMps = function( knots ) - return knots * 1852 / 3600 + return knots / 1.94384 --* 1852 / 3600 end UTILS.CelciusToFarenheit = function( Celcius ) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index f50040282..ce317db04 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -764,7 +764,7 @@ function CONTROLLABLE:CommandDeactivateICLS(Delay) return self end ---- Set callsign of the CONTROLLABLE. See [DCS_command_setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign) +--- 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. @@ -785,6 +785,50 @@ function CONTROLLABLE:CommandSetCallsign(CallName, CallNumber, Delay) return self end +--- Set EPLRS of the CONTROLLABLE on/off. See [DCS command EPLRS](https://wiki.hoggitworld.com/view/DCS_command_eplrs) +-- @param #CONTROLLABLE self +-- @param #boolean SwitchOnOff If true (or nil) switch EPLRS on. If false switch off. +-- @param #number Delay (Optional) Delay in seconds before the callsign is set. Default is immediately. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandEPLRS(SwitchOnOff, Delay) + self:F() + + if SwitchOnOff==nil then + SwitchOnOff=true + end + + -- ID + local _id=self:GetID() + + -- Command to set the callsign. + local CommandEPLRS={id='EPLRS', params={value=SwitchOnOff, groupId=_id}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandEPLRS, {self, SwitchOnOff}, Delay) + else + self:T(string.format("EPLRS=%s for controllable %s (id=%s)", tostring(SwitchOnOff), tostring(self:GetName()), tostring(_id))) + self:SetCommand(CommandEPLRS) + end + + return self +end + +--- Set EPLRS data link on/off. +-- @param #CONTROLLABLE self +-- @param #boolean SwitchOnOff If true (or nil) switch EPLRS on. If false switch off. +-- @param #number idx Task index. Default 1. +-- @return #table Task wrapped action. +function CONTROLLABLE:TaskEPLRS(SwitchOnOff, idx) + + -- ID + local _id=self:GetID() + + -- Command to set the callsign. + local CommandEPLRS={id='EPLRS', params={value=SwitchOnOff, groupId=_id}} + + return self:TaskWrappedAction(CommandEPLRS, idx or 1) + +end -- TASKS FOR AIR CONTROLLABLES --- (AIR) Attack a Controllable. From 6fe86678855a17314e6ad314fc38cf97752fccf7 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 4 Mar 2019 20:27:40 +0100 Subject: [PATCH 185/485] Some important fixes that improves the routing of the engagements of aircraft. when ground forces move, the aircraft engaging will approach correctly. also added the squaddrons with resources in the tactical information panel. and for patrols, now the amount of resources are correctly calculated. --- Moose Development/Moose/AI/AI_A2G_BAI.lua | 83 +++++++---------- Moose Development/Moose/AI/AI_A2G_CAS.lua | 90 +++++++------------ .../Moose/AI/AI_A2G_Dispatcher.lua | 21 ++++- Moose Development/Moose/AI/AI_A2G_Engage.lua | 4 +- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 84 ++++++++--------- 5 files changed, 123 insertions(+), 159 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 027474c2c..055994d6b 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -66,9 +66,7 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit local DefenderGroupName = DefenderGroup:GetName() - self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT - - local AttackCount = self.AttackSetUnit:Count() + local AttackCount = AttackSetUnit:Count() if AttackCount > 0 then @@ -84,12 +82,14 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit local DefenderCoord = DefenderGroup:GetPointVec3() DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. - local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() + local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) local EngageRoute = {} + local AttackTasks = {} --- Calculate the target route point. @@ -105,64 +105,47 @@ function AI_A2G_BAI: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. - local ToWP = TargetCoord:Translate( EngageDistance, FromEngageAngle ):WaypointAir( + local FromEngageAngle = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) + local ToWP = DefenderCoord: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 = {} - - self.AttackSetUnit.AttackIndex = self.AttackSetUnit.AttackIndex and self.AttackSetUnit.AttackIndex + 1 or 1 - - local AttackSetUnitPerThreatLevel = self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) - - local AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] - - local AttackUnitTasks = {} --- 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() } ) --- AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, false, false, nil, nil, EngageAltitude ) --- end --- end - - for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do - if AttackUnit then - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "BAI Unit:", AttackUnit:GetName() } ) - AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + EngageRoute[#EngageRoute+1] = ToWP + + if TargetDistance <= EngageDistance * 3 then + + local AttackUnitTasks = {} + + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "BAI Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + end end end - end - - if #AttackUnitTasks == 0 then - self:E( DefenderGroupName .. ": No targets found -> Going RTB") - self:Return() - self:__RTB( self.TaskDelay ) - else - DefenderGroup:OptionROEOpenFire() - DefenderGroup:OptionROTEvadeFire() - DefenderGroup:OptionKeepWeaponsOnThreat() + if #AttackUnitTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + DefenderGroup:OptionKeepWeaponsOnThreat() + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) + end - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) - EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) end + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self, AttackSetUnit ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) DefenderGroup:Route( EngageRoute, self.TaskDelay ) end diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 221341608..1978803d0 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -66,9 +66,7 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit local DefenderGroupName = DefenderGroup:GetName() - self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT - - local AttackCount = self.AttackSetUnit:Count() + local AttackCount = AttackSetUnit:Count() if AttackCount > 0 then @@ -80,14 +78,14 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit local DefenderCoord = DefenderGroup:GetPointVec3() DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. - local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() + local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) local EngageRoute = {} - - --- Calculate the target route point. + local AttackTasks = {} local FromWP = DefenderCoord:WaypointAir( self.PatrolAltType or "RADIO", @@ -96,70 +94,50 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit 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 10000 ) - - --- Create a route point of type air. - local ToWP = TargetCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( + + local FromEngageAngle = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) + local ToWP = DefenderCoord: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 = {} - - self.AttackSetUnit.AttackIndex = self.AttackSetUnit.AttackIndex and self.AttackSetUnit.AttackIndex + 1 or 1 - - local AttackSetUnitPerThreatLevel = self.AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) - - --local AttackUnit = AttackSetUnitPerThreatLevel[self.AttackSetUnit.AttackIndex] - - local AttackUnitTasks = {} - --- 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 - for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do - if AttackUnit then - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "CAS Unit:", AttackUnit:GetName() } ) - AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + if TargetDistance <= EngageDistance * 3 then + + local AttackUnitTasks = {} + + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "CAS Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + end end end + + + if #AttackUnitTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + DefenderGroup:OptionKeepWeaponsOnThreat() + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) + end end - - - if #AttackUnitTasks == 0 then - self:E( DefenderGroupName .. ": No targets found -> Going RTB") - self:Return() - self:__RTB( self.TaskDelay ) - else - DefenderGroup:OptionROEOpenFire() - DefenderGroup:OptionROTEvadeFire() - DefenderGroup:OptionKeepWeaponsOnThreat() - - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) - EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) - end + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self, AttackSetUnit ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) DefenderGroup:Route( EngageRoute, self.TaskDelay ) end diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index abbfd61ff..5d32f9354 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3351,8 +3351,20 @@ do -- AI_A2G_DISPATCHER local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType ) + -- Determine if there are sufficient resources to form a complete group for patrol. + local DefendersNeeded + if DefenderSquadron.ResourceCount == nil then + DefendersNeeded = DefenderSquadron.Grouping + else + if DefenderSquadron.ResourceCount >= DefenderSquadron.Grouping then + DefendersNeeded = DefenderSquadron.Grouping + else + DefendersNeeded = DefenderSquadron.ResourceCount + end + end + if Patrol then - self:ResourceQueue( true, DefenderSquadron, nil, Patrol, DefenseTaskType, nil, SquadronName ) + self:ResourceQueue( true, DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, nil, SquadronName ) end end @@ -3654,7 +3666,7 @@ do -- AI_A2G_DISPATCHER local x = CheckAttackCoordinate.x local y = CheckAttackCoordinate.z - local r = 8000 + local r = 5000 -- now we check if the coordinate is intersecting with the defense line. @@ -4098,6 +4110,11 @@ do -- AI_A2G_DISPATCHER Report:Add( string.format( " - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) ) end + + Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) ) + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + Report:Add( string.format( " - %s - %d", DefenderSquadronName, DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount or "n/a" ) ) + end self:F( Report:Text( "\n" ) ) trigger.action.outText( Report:Text( "\n" ), 25 ) diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua index b71da1dce..0035ed3ef 100644 --- a/Moose Development/Moose/AI/AI_A2G_Engage.lua +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -293,12 +293,12 @@ end -- todo: need to fix this global function --- @param Wrapper.Group#GROUP AIControllable -function AI_A2G_ENGAGE.EngageRoute( AIGroup, Fsm ) +function AI_A2G_ENGAGE.EngageRoute( AIGroup, Fsm, AttackSetUnit ) AIGroup:I( { "AI_A2G_ENGAGE.EngageRoute:", AIGroup:GetName() } ) if AIGroup:IsAlive() then - Fsm:__Engage( Fsm.TaskDelay ) + Fsm:__Engage( Fsm.TaskDelay, AttackSetUnit ) --local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) --AIGroup:SetTask( Task ) diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index 77a69ca10..f5d73a217 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -117,9 +117,7 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni local DefenderGroupName = DefenderGroup:GetName() - self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT - - local AttackCount = self.AttackSetUnit:Count() + local AttackCount = AttackSetUnit:Count() if AttackCount > 0 then @@ -135,15 +133,14 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni local DefenderCoord = DefenderGroup:GetPointVec3() DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. - local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() + local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) - - local EngageRoute = {} + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 25000 ) - - --- Calculate the target route point. + local EngageRoute = {} + local AttackTasks = {} local FromWP = DefenderCoord:WaypointAir( self.PatrolAltType or "RADIO", @@ -152,66 +149,55 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni EngageSpeed, false ) - 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( + local FromEngageAngle = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) + local ToWP = DefenderCoord: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 = {} --- 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 ) - - local AttackUnitTasks = {} - - for AttackUnitID, AttackUnit in ipairs( AttackSetUnitPerThreatLevel ) do - if AttackUnit then - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - local HasRadar = AttackUnit:HasSEAD() - if HasRadar then - self:F( { "SEAD Unit:", AttackUnit:GetName() } ) - AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + if TargetDistance <= EngageDistance * 3 then + + local AttackUnitTasks = {} + + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitID, AttackUnit in ipairs( AttackSetUnitPerThreatLevel ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:F( { "SEAD Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + end end end end + + if #AttackUnitTasks == 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:TaskCombo( AttackUnitTasks ) + end end - - if #AttackUnitTasks == 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:TaskCombo( AttackUnitTasks ) - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) - EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) - end + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self, AttackSetUnit ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) DefenderGroup:Route( EngageRoute, self.TaskDelay ) - end else self:E( DefenderGroupName .. ": No targets found -> Going RTB") From 79be7cbd2c2b5d4f5cc1b93699c2428e0f59460b Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 4 Mar 2019 23:08:59 +0100 Subject: [PATCH 186/485] Update Airboss.lua --- 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 2619aa3c1..e01b5288d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -12290,11 +12290,11 @@ function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed, tcoord) -- Debug mark. if self.Debug then if tcoord then - tcoord:MarkToAll("Detour Turn Help") + tcoord:MarkToAll(string.format("Detour Turn Help WP. Speed %.1f knots", UTILS.KmphToKnots(cspeedkmh))) end - coord:MarkToAll("Detour Point") + coord:MarkToAll(string.format("Detour Waypoint. Speed %.1f knots", UTILS.KmphToKnots(speedkmh))) if uturn then - pos0:MarkToAll(string.format("U-turn Return Point. Speed %.1f km/h", uspeedkmh)) + pos0:MarkToAll(string.format("Detour U-turn WP. Speed %.1f knots", UTILS.KmphToKnots(uspeedkmh))) end end From 26347ce779469b55ccc6c2089c36b40e763ae365 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 5 Mar 2019 09:56:19 +0100 Subject: [PATCH 187/485] Automatic assignment of tasks; and automatic accepts of tasks. Documented. Tested. Works. --- .../Moose/Tasking/CommandCenter.lua | 69 ++++++++++++++----- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 125762730..bbb159668 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -144,6 +144,25 @@ -- This is done by using the method @{#COMMANDCENTER.SetReferenceZones}(). -- For the moment, only one Reference Zone class can be specified, but in the future, more classes will become possible. -- +-- ## 7. Tasks. +-- +-- ### 7.1. Automatically assign tasks. +-- +-- One of the most important roles of the command center is the management of tasks. +-- The command center can assign automatically tasks to the players using the @{Tasking.CommandCenter#COMMANDCENTER.SetAutoAssignTasks}() method. +-- When this method is used with a parameter true; the command center will scan at regular intervals which players in a slot are not having a task assigned. +-- For those players; the tasking is enabled to assign automatically a task. +-- An Assign Menu will be accessible for the player under the command center menu, to configure the automatic tasking to switched on or off. +-- +-- ### 7.2. Automatically accept assigned tasks. +-- +-- When a task is assigned; the mission designer can decide if players are immediately assigned to the task; or they can accept/reject the assigned task. +-- Use the method @{Tasking.CommandCenter#COMMANDCENTER.SetAutoAcceptTasks}() to configure this behaviour. +-- If the tasks are not automatically accepted; the player will receive a message that he needs to access the command center menu and +-- choose from 2 added menu options either to accept or reject the assigned task within 30 seconds. +-- If the task is not accepted within 30 seconds; the task will be cancelled and a new task will be assigned. +-- +-- -- @field #COMMANDCENTER COMMANDCENTER = { ClassName = "COMMANDCENTER", @@ -169,10 +188,11 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) self.CommandCenterName = CommandCenterName or CommandCenterPositionable:GetName() self.CommandCenterCoalition = CommandCenterPositionable:GetCoalition() - self.AutoAssignTasks = false - self.Missions = {} + self:SetAutoAssignTasks( false ) + self:SetAutoAcceptTasks( true ) + self:HandleEvent( EVENTS.Birth, --- @param #COMMANDCENTER self -- @param Core.Event#EVENTDATA EventData @@ -467,7 +487,9 @@ function COMMANDCENTER:AssignRandomTask( TaskGroup ) local Task = Tasks[ math.random( 1, #Tasks ) ] -- Tasking.Task#TASK - Task:SetAssignMethod( ACT_ASSIGN_MENU_ACCEPT:New( Task.TaskBriefing ) ) + if not self.AutoAcceptTasks == true then + Task:SetAssignMethod( ACT_ASSIGN_MENU_ACCEPT:New( Task.TaskBriefing ) ) + end Task:AssignToGroup( TaskGroup ) @@ -498,29 +520,36 @@ end --- Automatically assigns tasks to all TaskGroups. +-- One of the most important roles of the command center is the management of tasks. +-- When this method is used with a parameter true; the command center will scan at regular intervals which players in a slot are not having a task assigned. +-- For those players; the tasking is enabled to assign automatically a task. +-- An Assign Menu will be accessible for the player under the command center menu, to configure the automatic tasking to switched on or off. -- @param #COMMANDCENTER self -- @param #boolean AutoAssign true for ON and false or nil for OFF. function COMMANDCENTER:SetAutoAssignTasks( AutoAssign ) self.AutoAssignTasks = AutoAssign or false - local GroupSet = self:AddGroups() - - for GroupID, TaskGroup in pairs( GroupSet:GetSet() ) do - local TaskGroup = TaskGroup -- Wrapper.Group#GROUP - self:GetMenu( TaskGroup ) - end - if self.AutoAssignTasks == true then self:ScheduleRepeat( 10, 30, 0, nil, self.AssignTasks, self ) else self:ScheduleStop( self.AssignTasks ) end - self:SetCommandCenterMenu() - end +--- Automatically accept tasks for all TaskGroups. +-- When a task is assigned; the mission designer can decide if players are immediately assigned to the task; or they can accept/reject the assigned task. +-- If the tasks are not automatically accepted; the player will receive a message that he needs to access the command center menu and +-- choose from 2 added menu options either to accept or reject the assigned task within 30 seconds. +-- If the task is not accepted within 30 seconds; the task will be cancelled and a new task will be assigned. +-- @param #COMMANDCENTER self +-- @param #boolean AutoAccept true for ON and false or nil for OFF. +function COMMANDCENTER:SetAutoAcceptTasks( AutoAccept ) + + self.AutoAcceptTasks = AutoAccept or false + +end --- Automatically assigns tasks to all TaskGroups. -- @param #COMMANDCENTER self @@ -531,12 +560,16 @@ function COMMANDCENTER:AssignTasks() for GroupID, TaskGroup in pairs( GroupSet:GetSet() ) do local TaskGroup = TaskGroup -- Wrapper.Group#GROUP - if self:IsGroupAssigned( TaskGroup ) then - else - -- Only groups with planes or helicopters will receive automatic tasks. - -- TODO Workaround DCS-BUG-3 - https://github.com/FlightControl-Master/MOOSE/issues/696 - if TaskGroup:IsAir() then - self:AssignRandomTask( TaskGroup ) + if TaskGroup:IsAlive() then + self:GetMenu( TaskGroup ) + + if self:IsGroupAssigned( TaskGroup ) then + else + -- Only groups with planes or helicopters will receive automatic tasks. + -- TODO Workaround DCS-BUG-3 - https://github.com/FlightControl-Master/MOOSE/issues/696 + if TaskGroup:IsAir() then + self:AssignRandomTask( TaskGroup ) + end end end end From 85febea3d94e05bf3f4bc532dfd127535236440b Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 5 Mar 2019 23:07:35 +0100 Subject: [PATCH 188/485] RAT v2.3.7 Added EPLRS datalink option. --- Moose Development/Moose/Functional/RAT.lua | 22 +++++++++++++++++++++- Moose Development/Moose/Ops/Airboss.lua | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 5a3d9919a..e11773099 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -151,6 +151,7 @@ -- @field #boolean parkingscanscenery If true, area around parking spots is scanned for scenery objects. Default is false. -- @field #boolean parkingverysafe If true, parking spots are considered as non-free until a possible aircraft has left and taken off. Default false. -- @field #boolean despawnair If true, aircraft are despawned when they reach their destination zone. Default. +-- @field #boolean eplrs If true, turn on EPLSR datalink for the RAT group. -- @extends Core.Spawn#SPAWN --- Implements an easy to use way to randomly fill your map with AI aircraft. @@ -430,6 +431,7 @@ RAT={ parkingscanscenery=false, -- Scan parking spots for scenery obstacles. parkingverysafe=false, -- Very safe option. despawnair=true, + eplrs=false, } ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -548,7 +550,7 @@ RAT.id="RAT | " --- RAT version. -- @list version RAT.version={ - version = "2.3.6", + version = "2.3.7", print = true, } @@ -1642,6 +1644,19 @@ function RAT:Invisible() return self end +--- Turn EPLSR datalink on/off. +-- @param #RAT self +-- @param #boolean switch If true (or nil), turn EPLRS on. +-- @return #RAT RAT self object. +function RAT:SetEPLSR(switch) + if switch==nil or switch==true then + self.eplrs=true + else + self.eplrs=false + end + return self +end + --- Aircraft are immortal. -- @param #RAT self -- @return #RAT RAT self object. @@ -2164,6 +2179,11 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self:_CommandImmortal(group, true) end + -- Set group to be immortal. + if self.eplrs then + group:CommandEPLRS(true, 1) + end + -- Set ROE, default is "weapon hold". self:_SetROE(group, self.roe) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index e01b5288d..996ddff0d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -72,6 +72,7 @@ -- * [[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) -- * [[MOOSE] Airboss - Groove Test: On-the-fly LSO Grading](https://www.youtube.com/watch?v=Xgs1hwDcPyM) +-- * [[MOOSE] Airboss - Carrier Auto Steam Into Wind](https://www.youtube.com/watch?v=IsU8dYgsp90) -- -- ### Lex explaining Boat Ops: -- From 5dbc3b1ba150c21a23311baed32d3cfa695698eb Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 5 Mar 2019 23:09:43 +0100 Subject: [PATCH 189/485] Update RAT.lua --- Moose Development/Moose/Functional/RAT.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index e11773099..071163246 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -1644,11 +1644,11 @@ function RAT:Invisible() return self end ---- Turn EPLSR datalink on/off. +--- Turn EPLRS datalink on/off. -- @param #RAT self -- @param #boolean switch If true (or nil), turn EPLRS on. -- @return #RAT RAT self object. -function RAT:SetEPLSR(switch) +function RAT:SetEPLRS(switch) if switch==nil or switch==true then self.eplrs=true else From 23f92e49ddc1c3843b5bbb4e7c1882a123c853b2 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 6 Mar 2019 22:40:44 +0100 Subject: [PATCH 190/485] New TASK CAPTURE DISPATCHER class. This class allows to send capture tasks to players. It needs a ZONE_CAPTURE_COALITION class to function. --- .../Moose/Functional/ZoneCaptureCoalition.lua | 3 + Moose Development/Moose/Modules.lua | 3 +- .../Moose/Tasking/Task_Capture_Dispatcher.lua | 307 ++++++++++++++++++ ...kZoneCapture.lua => Task_Capture_Zone.lua} | 42 +-- 4 files changed, 333 insertions(+), 22 deletions(-) create mode 100644 Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua rename Moose Development/Moose/Tasking/{TaskZoneCapture.lua => Task_Capture_Zone.lua} (89%) diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 61625bc4f..6d36beae0 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -556,6 +556,8 @@ do -- ZONE_CAPTURE_COALITION --- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:onenterCaptured() + self:F({"hello"}) + self:GetParent( self, ZONE_CAPTURE_COALITION ).onenterCaptured( self ) self.Goal:Achieved() @@ -643,6 +645,7 @@ do -- ZONE_CAPTURE_COALITION self:SetCoalition( NewCoalition ) self:Mark() + self.Goal:Achieved() end diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index d0614bbfe..5909ab54a 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -110,6 +110,7 @@ __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/Tasking/Task_Capture_Zone.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Capture_Dispatcher.lua' ) __Moose.Include( 'Scripts/Moose/Globals.lua' ) diff --git a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua new file mode 100644 index 000000000..f1b078028 --- /dev/null +++ b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua @@ -0,0 +1,307 @@ +--- **Tasking** - Creates and manages player TASK_ZONE_CAPTURE tasks. +-- +-- The **TASK_CAPTURE_DISPATCHER** allows you to setup various tasks for let human +-- players capture zones in a co-operation effort. +-- +-- The dispatcher will implement for you mechanisms to create capture zone tasks: +-- +-- * As setup by the mission designer. +-- * Dynamically capture zone tasks. +-- +-- +-- +-- **Specific features:** +-- +-- * Creates a task to capture zones and achieve mission goals. +-- * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled. +-- * Co-operation tasking, so a player joins a group of players executing the same task. +-- +-- +-- **A complete task menu system to allow players to:** +-- +-- * Join the task, abort the task. +-- * Mark the location of the zones to capture on the map. +-- * Provide details of the zones. +-- * Route to the zones. +-- * Display the task briefing. +-- +-- +-- **A complete mission menu system to allow players to:** +-- +-- * Join a task, abort the task. +-- * Display task reports. +-- * Display mission statistics. +-- * Mark the task locations on the map. +-- * Provide details of the zones. +-- * Display the mission briefing. +-- * Provide status updates as retrieved from the command center. +-- * Automatically assign a random task as part of a mission. +-- * Manually assign a specific task as part of a mission. +-- +-- +-- **A settings system, using the settings menu:** +-- +-- * Tweak the duration of the display of messages. +-- * Switch between metric and imperial measurement system. +-- * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS. +-- * Various other options. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task_Zone_Capture_Dispatcher +-- @image Task_Zone_Capture_Dispatcher.JPG + +do -- TASK_CAPTURE_DISPATCHER + + --- TASK_CAPTURE_DISPATCHER class. + -- @type TASK_CAPTURE_DISPATCHER + -- @extends Tasking.Task_Manager#TASK_MANAGER + -- @field TASK_CAPTURE_DISPATCHER.ZONE ZONE + + --- @type TASK_CAPTURE_DISPATCHER.CSAR + -- @field Wrapper.Unit#UNIT PilotUnit + -- @field Tasking.Task#TASK Task + + + --- Implements the dynamic dispatching of capture zone tasks. + -- + -- The **TASK_CAPTURE_DISPATCHER** allows you to setup various tasks for let human + -- players capture zones in a co-operation effort. + -- + -- Let's explore **step by step** how to setup the task capture zone dispatcher. + -- + -- # 1. Setup a mission environment. + -- + -- It is easy, as it works just like any other task setup, so setup a command center and a mission. + -- + -- ## 1.1. Create a command center. + -- + -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. + -- The command assumes that you´ve setup a group in the mission editor with the name HQ. + -- This group will act as the command center object. + -- It is a good practice to mark this group as invisible and invulnerable. + -- + -- local CommandCenter = COMMANDCENTER + -- :New( GROUP:FindByName( "HQ" ), "HQ" ) -- Create the CommandCenter. + -- + -- ## 1.2. Create a mission. + -- + -- Tasks work in a **mission**, which groups these tasks to achieve a joint **mission goal**. A command center can **govern multiple missions**. + -- + -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor. + -- + -- -- Declare the Mission for the Command Center. + -- local Mission = MISSION + -- :New( CommandCenter, + -- "Overlord", + -- "High", + -- "Capture the blue zones.", + -- coalition.side.RED + -- ) + -- + -- + -- # 2. Dispatch a **capture zone** task. + -- + -- So, now that we have a command center and a mission, we now create the capture zone task. + -- We create the capture zone task using the @{#TASK_CAPTURE_DISPATCHER.AddCaptureZoneTask}() constructor. + -- + -- ## 2.1. Create the capture zones. + -- + -- Because a capture zone task will not generate the capture zones, you'll need to create them first. + -- + -- + -- -- We define here a capture zone; of the type ZONE_CAPTURE_COALITION. + -- -- The zone to be captured has the name Alpha, and was defined in the mission editor as a trigger zone. + -- CaptureZone = ZONE:New( "Alpha" ) + -- CaptureZoneCoalitionApha = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED ) + -- + -- ## 2.2. Create a set of player groups. + -- + -- What is also needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- + -- -- Allocate the player slots, which must be aircraft (airplanes or helicopters), that can be manned by players. + -- -- We use the method FilterPrefixes to filter those player groups that have client slots, as defined in the mission editor. + -- -- In this example, we filter the groups where the name starts with "Blue Player", which captures the blue player slots. + -- local PlayerGroupSet = SET_GROUP:New():FilterPrefixes( "Blue Player" ):FilterStart() + -- + -- ## 2.3. Setup the capture zone task. + -- + -- First, we need to create a TASK_CAPTURE_DISPATCHER object. + -- + -- TaskCaptureZoneDispatcher = TASK_CAPTURE_DISPATCHER:New( Mission, PilotGroupSet ) + -- + -- So, the variable `TaskCaptureZoneDispatcher` will contain the object of class TASK_CAPTURE_DISPATCHER, + -- which will allow you to dispatch capture zone tasks: + -- + -- * for mission `Mission`, as was defined in section 1.2. + -- * for the group set `PilotGroupSet`, as was defined in section 2.2. + -- + -- Now that we have `TaskDispatcher` object, we can now **create the TaskCaptureZone**, using the @{#TASK_CAPTURE_DISPATCHER.AddCaptureZoneTask}() method! + -- + -- local TaskCaptureZone = TaskCaptureZoneDispatcher:AddCaptureZoneTask( + -- "Capture zone Alpha", + -- CaptureZoneCoalitionAlpha, + -- "Fly to zone Alpha and eliminate all enemy forces to capture it." ) + -- + -- As a result of this code, the `TaskCaptureZone` (returned) variable will contain an object of @{#TASK_CAPTURE_ZONE}! + -- We pass to the method the title of the task, and the `CaptureZoneCoalitionAlpha`, which is the zone to be captured, as defined in section 2.1! + -- This returned `TaskCaptureZone` object can now be used to setup additional task configurations, or to control this specific task with special events. + -- + -- And you're done! As you can see, it is a small bit of work, but the reward is great. + -- And, because all this is done using program interfaces, you can easily build a mission to capture zones yourself! + -- Based on various events happening within your mission, you can use the above methods to create new capture zones, + -- and setup a new capture zone task and assign it to a group of players, while your mission is running! + -- + -- + -- + -- @field #TASK_CAPTURE_DISPATCHER + TASK_CAPTURE_DISPATCHER = { + ClassName = "TASK_CAPTURE_DISPATCHER", + Mission = nil, + Tasks = {}, + Zones = {}, + ZoneCount = 0, + } + + + --- TASK_CAPTURE_DISPATCHER constructor. + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. + -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. + -- @return #TASK_CAPTURE_DISPATCHER self + function TASK_CAPTURE_DISPATCHER:New( Mission, SetGroup ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, TASK_MANAGER:New( SetGroup ) ) -- #TASK_CAPTURE_DISPATCHER + + self.Mission = Mission + + self:AddTransition( "Started", "Assign", "Started" ) + self:AddTransition( "Started", "ZoneCaptured", "Started" ) + + self:__StartTasks( 5 ) + + return self + end + + + + --- Add a capture zone task. + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param #string TaskPrefix (optional) The prefix of the capture zone task. + -- This prefix will be appended with a . + a number of 3 digits. + -- If no TaskPrefix is given, then "Capture" will be used as the prefix. + -- @param Functional.CaptureZoneCoalition#ZONE_CAPTURE_COALITION CaptureZone The zone of the coalition to be captured as the task goal. + -- @param #string Briefing The briefing of the task to be shown to the player. + -- @return Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE + -- @usage + -- + -- + function TASK_CAPTURE_DISPATCHER:AddCaptureZoneTask( TaskPrefix, CaptureZone, Briefing ) + + self.ZoneCount = self.ZoneCount + 1 + + local TaskName = string.format( ( TaskPrefix or "Capture" ) .. ".%03d", self.ZoneCount ) + + self.Zones[TaskName] = {} + self.Zones[TaskName].CaptureZone = CaptureZone + self.Zones[TaskName].Briefing = Briefing + self.Zones[TaskName].Task = nil + self.Zones[TaskName].TaskPrefix = TaskPrefix + + self:ManageTasks() + + return self.Zones[TaskName] and self.Zones[TaskName].Task + end + + + --- Assigns tasks to the @{Core.Set#SET_GROUP}. + -- @param #TASK_CAPTURE_DISPATCHER self + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function TASK_CAPTURE_DISPATCHER:ManageTasks() + self:F() + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local Mission = self.Mission + + if Mission:IsIDLE() or Mission:IsENGAGED() then + + local TaskReport = REPORT:New() + + -- Checking the task queue for the dispatcher, and removing any obsolete task! + for TaskIndex, TaskData in pairs( self.Tasks ) do + local Task = TaskData -- Tasking.Task#TASK + if Task:IsStatePlanned() then + -- Here we need to check if the pilot is still existing. +-- local DetectedItem = Detection:GetDetectedItemByIndex( TaskIndex ) +-- if not DetectedItem then +-- local TaskText = Task:GetName() +-- for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do +-- Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2A task %s for %s removed.", TaskText, Mission:GetShortText() ), TaskGroup ) +-- end +-- Task = self:RemoveTask( TaskIndex ) +-- end + end + end + + -- Now that all obsolete tasks are removed, loop through the Zone tasks. + for TaskName, CaptureZone in pairs( self.Zones ) do + + if not CaptureZone.Task then + -- New Transport Task + CaptureZone.Task = TASK_CAPTURE_ZONE:New( Mission, self.SetGroup, TaskName, CaptureZone.CaptureZone, CaptureZone.Briefing ) + CaptureZone.Task.TaskPrefix = CaptureZone.TaskPrefix -- We keep the TaskPrefix for further reference! + Mission:AddTask( CaptureZone.Task ) + TaskReport:Add( TaskName ) + function CaptureZone.Task.OnEnterSuccess( Task, From, Event, To ) + self:Success( Task ) + end + + function CaptureZone.Task.OnEnterCancelled( Task, From, Event, To ) + self:Cancelled( Task ) + end + + function CaptureZone.Task.OnEnterFailed( Task, From, Event, To ) + self:Failed( Task ) + end + + function CaptureZone.Task.OnEnterAborted( Task, From, Event, To ) + self:Aborted( Task ) + end + + -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher. + function CaptureZone.Task.OnAfterCaptured( Task, From, Event, To, TaskUnit ) + self:Captured( Task, Task.TaskPrefix, TaskUnit ) + end + + end + + end + + + -- TODO set menus using the HQ coordinator + Mission:GetCommandCenter():SetMenu() + + local TaskText = TaskReport:Text(", ") + + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" then + Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup ) + end + end + + end + + return true + end + +end diff --git a/Moose Development/Moose/Tasking/TaskZoneCapture.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua similarity index 89% rename from Moose Development/Moose/Tasking/TaskZoneCapture.lua rename to Moose Development/Moose/Tasking/Task_Capture_Zone.lua index fb1ab81c5..c9c0f1193 100644 --- a/Moose Development/Moose/Tasking/TaskZoneCapture.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -160,37 +160,37 @@ do -- TASK_ZONE_GOAL end -do -- TASK_ZONE_CAPTURE +do -- TASK_CAPTURE_ZONE - --- The TASK_ZONE_CAPTURE class - -- @type TASK_ZONE_CAPTURE + --- The TASK_CAPTURE_ZONE class + -- @type TASK_CAPTURE_ZONE -- @field Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal -- @extends #TASK_ZONE_GOAL - --- # TASK_ZONE_CAPTURE class, extends @{Tasking.TaskZoneGoal#TASK_ZONE_GOAL} + --- # TASK_CAPTURE_ZONE class, extends @{Tasking.TaskZoneGoal#TASK_ZONE_GOAL} -- - -- The TASK_ZONE_CAPTURE class defines an Suppression or Extermination of Air Defenses task for a human player to be executed. + -- The TASK_CAPTURE_ZONE class defines an Suppression or Extermination of Air Defenses task for a human player to be executed. -- These tasks are important to be executed as they will help to achieve air superiority at the vicinity. -- - -- The TASK_ZONE_CAPTURE is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks + -- The TASK_CAPTURE_ZONE is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks -- based on detected enemy ground targets. -- - -- @field #TASK_ZONE_CAPTURE - TASK_ZONE_CAPTURE = { - ClassName = "TASK_ZONE_CAPTURE", + -- @field #TASK_CAPTURE_ZONE + TASK_CAPTURE_ZONE = { + ClassName = "TASK_CAPTURE_ZONE", } - --- Instantiates a new TASK_ZONE_CAPTURE. - -- @param #TASK_ZONE_CAPTURE self + --- Instantiates a new TASK_CAPTURE_ZONE. + -- @param #TASK_CAPTURE_ZONE self -- @param Tasking.Mission#MISSION Mission -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. -- @param #string TaskName The name of the Task. -- @param Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoalCoalition -- @param #string TaskBriefing The briefing of the task. - -- @return #TASK_ZONE_CAPTURE self - function TASK_ZONE_CAPTURE:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, TaskBriefing) - local self = BASE:Inherit( self, TASK_ZONE_GOAL:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, "CAPTURE", TaskBriefing ) ) -- #TASK_ZONE_CAPTURE + -- @return #TASK_CAPTURE_ZONE self + function TASK_CAPTURE_ZONE:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, TaskBriefing) + local self = BASE:Inherit( self, TASK_ZONE_GOAL:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, "CAPTURE", TaskBriefing ) ) -- #TASK_CAPTURE_ZONE self:F() Mission:AddTask( self ) @@ -212,9 +212,9 @@ do -- TASK_ZONE_CAPTURE end - --- Instantiates a new TASK_ZONE_CAPTURE. - -- @param #TASK_ZONE_CAPTURE self - function TASK_ZONE_CAPTURE:UpdateTaskInfo() + --- Instantiates a new TASK_CAPTURE_ZONE. + -- @param #TASK_CAPTURE_ZONE self + function TASK_CAPTURE_ZONE:UpdateTaskInfo() local ZoneCoordinate = self.ZoneGoal:GetZone():GetCoordinate() @@ -224,7 +224,7 @@ do -- TASK_ZONE_CAPTURE end - function TASK_ZONE_CAPTURE:ReportOrder( ReportGroup ) + function TASK_CAPTURE_ZONE:ReportOrder( ReportGroup ) local Coordinate = self:GetData( "Coordinate" ) --local Coordinate = self.TaskInfo.Coordinates.TaskInfoText local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) @@ -233,11 +233,11 @@ do -- TASK_ZONE_CAPTURE end - --- @param #TASK_ZONE_CAPTURE self + --- @param #TASK_CAPTURE_ZONE self -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_ZONE_CAPTURE:OnAfterGoal( From, Event, To, PlayerUnit, PlayerName ) + function TASK_CAPTURE_ZONE:OnAfterGoal( From, Event, To, PlayerUnit, PlayerName ) - self:F( { PlayerUnit = PlayerUnit } ) + self:F( { PlayerUnit = PlayerUnit, Achieved = self.ZoneGoal.Goal:IsAchieved() } ) if self.ZoneGoal then if self.ZoneGoal.Goal:IsAchieved() then From 0af9e57b8250bf14c133b53db834183b2c618644 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 7 Mar 2019 09:54:15 +0100 Subject: [PATCH 191/485] Now the task routing is also working towards the capture zones. --- .../Moose/Actions/Act_Assign.lua | 2 +- .../Moose/Tasking/Task_Capture_Zone.lua | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Moose Development/Moose/Actions/Act_Assign.lua b/Moose Development/Moose/Actions/Act_Assign.lua index 969009ab1..9a4c907db 100644 --- a/Moose Development/Moose/Actions/Act_Assign.lua +++ b/Moose Development/Moose/Actions/Act_Assign.lua @@ -167,7 +167,7 @@ do -- ACT_ASSIGN_ACCEPT -- @param #string Event -- @param #string From -- @param #string To - function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To ) + function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To, TaskGroup ) self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() ) end diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index c9c0f1193..0745ea150 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -59,19 +59,25 @@ do -- TASK_ZONE_GOAL local Fsm = self:GetUnitProcess() - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "StartMonitoring", Rejected = "Reject" } ) - Fsm:AddTransition( "Assigned", "StartMonitoring", "Monitoring" ) Fsm:AddTransition( "Monitoring", "Monitor", "Monitoring", {} ) - Fsm:AddTransition( "Monitoring", "RouteTo", "Monitoring" ) Fsm:AddProcess( "Monitoring", "RouteToZone", ACT_ROUTE_ZONE:New(), {} ) - --Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) - --Fsm:AddTransition( "Accounted", "Success", "Success" ) Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) Fsm:AddTransition( "Failed", "Fail", "Failed" ) self:SetTargetZone( self.ZoneGoal:GetZone() ) + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task#TASK Task + function Fsm:OnAfterAssigned( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + self:__StartMonitoring( 0.1 ) + self:__RouteToZone( 0.1 ) + end --- Test -- @param #FSM_PROCESS self @@ -80,7 +86,6 @@ do -- TASK_ZONE_GOAL function Fsm:onafterStartMonitoring( TaskUnit, Task ) self:F( { self } ) self:__Monitor( 0.1 ) - self:__RouteTo( 0.1 ) end --- Monitor Loop @@ -101,7 +106,7 @@ do -- TASK_ZONE_GOAL -- Determine the first Unit from the self.TargetSetUnit if Task:GetTargetZone( TaskUnit ) then - self:__RouteTo( 0.1 ) + self:__RouteToZone( 0.1 ) end end @@ -225,7 +230,7 @@ do -- TASK_CAPTURE_ZONE function TASK_CAPTURE_ZONE:ReportOrder( ReportGroup ) - local Coordinate = self:GetData( "Coordinate" ) + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) --local Coordinate = self.TaskInfo.Coordinates.TaskInfoText local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) From eee1aca14fd3d1f2dde01002696670124b5d69f8 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 7 Mar 2019 14:02:49 +0100 Subject: [PATCH 192/485] Disabled SetSquadronVisible due to a bug fix that is required. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 5d32f9354..dd22a20c6 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -1778,21 +1778,22 @@ do -- AI_A2G_DISPATCHER -- -- 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 - self:SetSquadronTakeoffFromParkingCold( SquadronName ) - self:SetSquadronLandingAtEngineShutdown( SquadronName ) - - for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do - DefenderSpawn:InitUnControlled() - end - - end + -- TODO: disabling because of bug in queueing. +-- function AI_A2G_DISPATCHER:SetSquadronVisible( SquadronName ) +-- +-- self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} +-- +-- local DefenderSquadron = self:GetSquadron( SquadronName ) +-- +-- DefenderSquadron.Uncontrolled = true +-- self:SetSquadronTakeoffFromParkingCold( SquadronName ) +-- self:SetSquadronLandingAtEngineShutdown( SquadronName ) +-- +-- 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 From ef1a9330a45b337bed5bf51fe85f003663819db0 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 8 Mar 2019 10:03:58 +0100 Subject: [PATCH 193/485] Updates, many fixes. Now also the communication to the players is working. --- Moose Development/Moose/AI/AI_A2G_BAI.lua | 46 +++--- Moose Development/Moose/AI/AI_A2G_CAS.lua | 2 +- .../Moose/AI/AI_A2G_Dispatcher.lua | 32 +++- Moose Development/Moose/AI/AI_A2G_Engage.lua | 150 +++++++++++++++++- Moose Development/Moose/AI/AI_A2G_Patrol.lua | 36 ++--- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 2 +- Moose Development/Moose/Core/Base.lua | 6 +- .../Moose/Tasking/DetectionManager.lua | 5 +- Moose Development/Moose/Tasking/Task.lua | 2 +- Moose Development/Moose/Tasking/TaskInfo.lua | 2 +- .../Moose/Tasking/Task_Capture_Dispatcher.lua | 21 +-- 11 files changed, 234 insertions(+), 70 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 055994d6b..d572347ba 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -105,8 +105,8 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit self:SetTargetDistance( TargetCoord ) -- For RTB status check - local FromEngageAngle = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) - local ToWP = DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( + local FromEngageAngle = TargetCoord:GetAngleDegrees( TargetCoord:GetDirectionVec3( DefenderCoord ) ) + local ToWP = TargetCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, @@ -116,35 +116,31 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit EngageRoute[#EngageRoute+1] = ToWP - if TargetDistance <= EngageDistance * 3 then + local AttackUnitTasks = {} - local AttackUnitTasks = {} - - local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) - for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do - if AttackUnit then - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "BAI Unit:", AttackUnit:GetName() } ) - AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) - end + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "BAI Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) end end - - if #AttackUnitTasks == 0 then - self:E( DefenderGroupName .. ": No targets found -> Going RTB") - self:Return() - self:__RTB( self.TaskDelay ) - else - DefenderGroup:OptionROEOpenFire() - DefenderGroup:OptionROTEvadeFire() - DefenderGroup:OptionKeepWeaponsOnThreat() - - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) - end + end + + if #AttackUnitTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + DefenderGroup:OptionKeepWeaponsOnThreat() + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) end - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self, AttackSetUnit ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.___Engage", self, AttackSetUnit ) EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) DefenderGroup:Route( EngageRoute, self.TaskDelay ) diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 1978803d0..c8773b3f5 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -136,7 +136,7 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit end end - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self, AttackSetUnit ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.___Engage", self, AttackSetUnit ) EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) DefenderGroup:Route( EngageRoute, self.TaskDelay ) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index dd22a20c6..44d221ad2 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3564,17 +3564,43 @@ do -- AI_A2G_DISPATCHER if DefenderTarget then Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne." ) - Fsm:Engage( DefenderTarget.Set ) -- Engage on the TargetSetUnit + Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end + function Fsm:OnAfterEngageRoute( Defender, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", 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 FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route, bearing " .. Coordinate:ToString( Defender ) ) + end + + function Fsm:OnAfterEngage( Defender, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", 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 FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() + + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging target, bearing " .. Coordinate:ToString( Defender ) ) + end + function Fsm:onafterRTB( Defender, From, Event, To ) self:F({"Defender RTB", Defender:GetName()}) local DefenderName = Defender:GetName() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " RTB." ) self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) @@ -3716,7 +3742,7 @@ do -- AI_A2G_DISPATCHER local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) - Fsm:Engage( AttackerSet ) -- Engage on the TargetSetUnit + Fsm:EngageRoute( AttackerSet ) -- Engage on the TargetSetUnit self:SetDefenderTaskTarget( DefenderGroup, DetectedItem ) diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua index 0035ed3ef..71a2eae6f 100644 --- a/Moose Development/Moose/AI/AI_A2G_Engage.lua +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -101,6 +101,51 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor self.EngageFloorAltitude = EngageFloorAltitude or 1000 self.EngageCeilingAltitude = EngageCeilingAltitude or 1500 + self:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "EngageRoute", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event EngageRoute. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeEngageRoute + -- @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 EngageRoute. + -- @function [parent=#AI_A2G_ENGAGE] OnAfterEngageRoute + -- @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 EngageRoute. + -- @function [parent=#AI_A2G_ENGAGE] EngageRoute + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event EngageRoute. + -- @function [parent=#AI_A2G_ENGAGE] __EngageRoute + -- @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( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. --- OnBefore Transition Handler for Event Engage. @@ -119,7 +164,7 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @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 @@ -293,9 +338,22 @@ end -- todo: need to fix this global function --- @param Wrapper.Group#GROUP AIControllable -function AI_A2G_ENGAGE.EngageRoute( AIGroup, Fsm, AttackSetUnit ) +function AI_A2G_ENGAGE.___EngageRoute( AIGroup, Fsm, AttackSetUnit ) - AIGroup:I( { "AI_A2G_ENGAGE.EngageRoute:", AIGroup:GetName() } ) + AIGroup:I( { "AI_A2G_ENGAGE.___EngageRoute:", AIGroup:GetName() } ) + + if AIGroup:IsAlive() then + Fsm:__EngageRoute( Fsm.TaskDelay, AttackSetUnit ) + + --local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) + --AIGroup:SetTask( Task ) + end +end + +--- @param Wrapper.Group#GROUP AIControllable +function AI_A2G_ENGAGE.___Engage( AIGroup, Fsm, AttackSetUnit ) + + AIGroup:I( { "AI_A2G_ENGAGE.___Engage:", AIGroup:GetName() } ) if AIGroup:IsAlive() then Fsm:__Engage( Fsm.TaskDelay, AttackSetUnit ) @@ -336,7 +394,7 @@ end -- @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( DefenderGroup, From, Event, To, AttackSetUnit ) +function AI_A2G_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) @@ -376,3 +434,87 @@ function AI_A2G_ENGAGE:OnEventDead( EventData ) end end end + +--- @param #AI_A2G_ENGAGE 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_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + local AttackCount = AttackSetUnit:Count() + + if AttackCount > 0 then + + 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( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) + + if TargetDistance <= EngageDistance * 3 then + + self:__Engage( 0.1, AttackSetUnit ) + + else + + local EngageRoute = {} + local AttackTasks = {} + + --- 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 = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) + local ToWP = DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + EngageSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToWP + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.___EngageRoute", self, AttackSetUnit ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + + DefenderGroup:OptionROEReturnFire() + DefenderGroup:OptionROTEvadeFire() + + DefenderGroup:Route( EngageRoute, self.TaskDelay ) + end + + end + else + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + end +end \ No newline at end of file diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua index 4e8a6e953..86b5d49dc 100644 --- a/Moose Development/Moose/AI/AI_A2G_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -62,7 +62,7 @@ -- ### 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.AI_Patrol#AI_PATROL_ZONE.PatrolRoute}**: 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. @@ -173,10 +173,10 @@ function AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @param #string Event The Event string. -- @param #string To The To State string. - self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + self:AddTransition( "Patrolling", "PatrolRoute", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. - --- OnBefore Transition Handler for Event Route. - -- @function [parent=#AI_A2G_PATROL] OnBeforeRoute + --- OnBefore Transition Handler for Event PatrolRoute. + -- @function [parent=#AI_A2G_PATROL] OnBeforePatrolRoute -- @param #AI_A2G_PATROL self -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. -- @param #string From The From State string. @@ -184,20 +184,20 @@ function AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @param #string To The To State string. -- @return #boolean Return false to cancel Transition. - --- OnAfter Transition Handler for Event Route. - -- @function [parent=#AI_A2G_PATROL] OnAfterRoute + --- OnAfter Transition Handler for Event PatrolRoute. + -- @function [parent=#AI_A2G_PATROL] OnAfterPatrolRoute -- @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 Route. - -- @function [parent=#AI_A2G_PATROL] Route + --- Synchronous Event Trigger for Event PatrolRoute. + -- @function [parent=#AI_A2G_PATROL] PatrolRoute -- @param #AI_A2G_PATROL self - --- Asynchronous Event Trigger for Event Route. - -- @function [parent=#AI_A2G_PATROL] __Route + --- Asynchronous Event Trigger for Event PatrolRoute. + -- @function [parent=#AI_A2G_PATROL] __PatrolRoute -- @param #AI_A2G_PATROL self -- @param #number Delay The delay in seconds. @@ -234,12 +234,12 @@ function AI_A2G_PATROL:onafterPatrol( AIPatrol, From, Event, To ) self:ClearTargetDistance() - self:__Route( self.TaskDelay ) + self:__PatrolRoute( self.TaskDelay ) AIPatrol:OnReSpawn( function( PatrolGroup ) self:__Reset( self.TaskDelay ) - self:__Route( self.TaskDelay ) + self:__PatrolRoute( self.TaskDelay ) end ) end @@ -247,12 +247,12 @@ end --- @param Wrapper.Group#GROUP AIPatrol -- 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 ) +function AI_A2G_PATROL.___PatrolRoute( AIPatrol, Fsm ) - AIPatrol:F( { "AI_A2G_PATROL.PatrolRoute:", AIPatrol:GetName() } ) + AIPatrol:F( { "AI_A2G_PATROL.___PatrolRoute:", AIPatrol:GetName() } ) if AIPatrol:IsAlive() then - Fsm:Route() + Fsm:PatrolRoute() end end @@ -263,7 +263,7 @@ end -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. -function AI_A2G_PATROL:onafterRoute( AIPatrol, From, Event, To ) +function AI_A2G_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) self:F2() @@ -300,7 +300,7 @@ function AI_A2G_PATROL:onafterRoute( AIPatrol, From, Event, To ) PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint local Tasks = {} - Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2G_PATROL.PatrolRoute", self ) + Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2G_PATROL.___PatrolRoute", self ) PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) AIPatrol:OptionROEReturnFire() @@ -317,7 +317,7 @@ function AI_A2G_PATROL.Resume( AIPatrol, Fsm ) AIPatrol:F( { "AI_A2G_PATROL.Resume:", AIPatrol:GetName() } ) if AIPatrol:IsAlive() then Fsm:__Reset( self.TaskDelay ) - Fsm:__Route( self.TaskDelay ) + Fsm:__PatrolRoute( 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 f5d73a217..ae2f2dad1 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -194,7 +194,7 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni end end - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self, AttackSetUnit ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.___Engage", self, AttackSetUnit ) EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) DefenderGroup:Route( EngageRoute, self.TaskDelay ) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index cca8fedeb..a72fb0e1f 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -809,8 +809,10 @@ do -- Scheduling function BASE:ScheduleStop( SchedulerFunction ) self:F3( { "ScheduleStop:" } ) - - _SCHEDULEDISPATCHER:Stop( self.Scheduler, self._.Schedules[SchedulerFunction] ) + + if self.Scheduler then + _SCHEDULEDISPATCHER:Stop( self.Scheduler, self._.Schedules[SchedulerFunction] ) + end end end diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index 4603adf82..e7caaea2d 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -57,6 +57,9 @@ do -- DETECTION MANAGER Detection = nil, } + --- @field Tasking.CommandCenter#COMMANDCENTER + DETECTION_MANAGER.CC = {} + --- FAC constructor. -- @param #DETECTION_MANAGER self -- @param Core.Set#SET_GROUP SetGroup @@ -238,7 +241,7 @@ do -- DETECTION MANAGER function DETECTION_MANAGER:MessageToPlayers( Message ) if self.CC then - self.CC:MessageToAll( Message ) + self.CC:MessageToCoalition( Message ) end return self diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index 0f4ec262d..a99012590 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -1786,7 +1786,7 @@ function TASK:GetPlayerCount() --R2.1 Get a count of the players. if PlayerGroup:IsAlive() == true then if self:IsGroupAssigned( PlayerGroup ) then local PlayerNames = PlayerGroup:GetPlayerNames() - PlayerCount = PlayerCount + #PlayerNames + PlayerCount = PlayerCount + ((PlayerNames) and #PlayerNames or 0) -- PlayerNames can be nil when there are no players. end end end diff --git a/Moose Development/Moose/Tasking/TaskInfo.lua b/Moose Development/Moose/Tasking/TaskInfo.lua index d3fc88350..e5b23aa16 100644 --- a/Moose Development/Moose/Tasking/TaskInfo.lua +++ b/Moose Development/Moose/Tasking/TaskInfo.lua @@ -24,7 +24,7 @@ TASKINFO = { ClassName = "TASKINFO", } ---- @type #TASKINFO.Detail #string A string that flags to document which level of detail needs to be shown in the report. +--- @type TASKINFO.Detail #string A string that flags to document which level of detail needs to be shown in the report. -- -- - "M" for Markings on the Map (F10). -- - "S" for Summary Reports. diff --git a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua index f1b078028..d37ad7d5d 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua @@ -195,8 +195,8 @@ do -- TASK_CAPTURE_DISPATCHER --- Add a capture zone task. -- @param #TASK_CAPTURE_DISPATCHER self -- @param #string TaskPrefix (optional) The prefix of the capture zone task. - -- This prefix will be appended with a . + a number of 3 digits. - -- If no TaskPrefix is given, then "Capture" will be used as the prefix. + -- If no TaskPrefix is given, then "Capture" will be used as the TaskPrefix. + -- The TaskPrefix will be appended with a . + a number of 3 digits, if the TaskPrefix already exists in the task collection. -- @param Functional.CaptureZoneCoalition#ZONE_CAPTURE_COALITION CaptureZone The zone of the coalition to be captured as the task goal. -- @param #string Briefing The briefing of the task to be shown to the player. -- @return Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE @@ -205,9 +205,11 @@ do -- TASK_CAPTURE_DISPATCHER -- function TASK_CAPTURE_DISPATCHER:AddCaptureZoneTask( TaskPrefix, CaptureZone, Briefing ) - self.ZoneCount = self.ZoneCount + 1 - - local TaskName = string.format( ( TaskPrefix or "Capture" ) .. ".%03d", self.ZoneCount ) + local TaskName = TaskPrefix or "Capture" + if self.Zones[TaskName] then + self.ZoneCount = self.ZoneCount + 1 + TaskName = string.format( "%s.%03d", TaskName, self.ZoneCount ) + end self.Zones[TaskName] = {} self.Zones[TaskName].CaptureZone = CaptureZone @@ -241,15 +243,8 @@ do -- TASK_CAPTURE_DISPATCHER for TaskIndex, TaskData in pairs( self.Tasks ) do local Task = TaskData -- Tasking.Task#TASK if Task:IsStatePlanned() then - -- Here we need to check if the pilot is still existing. --- local DetectedItem = Detection:GetDetectedItemByIndex( TaskIndex ) --- if not DetectedItem then --- local TaskText = Task:GetName() --- for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do --- Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2A task %s for %s removed.", TaskText, Mission:GetShortText() ), TaskGroup ) --- end + -- Here we need to check if the pilot is still existing. -- Task = self:RemoveTask( TaskIndex ) --- end end end From d72e89d52b66829e0b9a0e151005dc08e3e377d1 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 9 Mar 2019 06:32:11 +0100 Subject: [PATCH 194/485] Fixes in AI_A2G_DISPATCHER --- .../Moose/AI/AI_A2G_Dispatcher.lua | 29 ++++++++++--------- Moose Development/Moose/AI/AI_Air.lua | 1 - .../Moose/Tasking/DetectionManager.lua | 2 +- .../Moose/Wrapper/Controllable.lua | 2 +- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 44d221ad2..4c5358729 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3216,8 +3216,8 @@ do -- AI_A2G_DISPATCHER if QueueItem.AttackerDetection and QueueItem.AttackerDetection.ItemID == AttackerDetection.ItemID then DefendersMissing = DefendersMissing - QueueItem.DefendersNeeded / QueueItem.DefenderSquadron.Overhead --DefendersEngaged = DefendersEngaged + QueueItem.DefenderGrouping + self:F( { QueueItemName = QueueItem.Defense, QueueItem_ItemID = QueueItem.AttackerDetection.ItemID, DetectedItem = AttackerDetection.ItemID, DefendersMissing = DefendersMissing } ) end - self:F( { QueueItemName = QueueItem.Defense, QueueItem_ItemID = QueueItem.AttackerDetection.ItemID, DetectedItem = AttackerDetection.ItemID, DefendersMissing = DefendersMissing } ) end self:F( { DefenderCount = DefendersEngaged } ) @@ -3352,20 +3352,23 @@ do -- AI_A2G_DISPATCHER local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType ) - -- Determine if there are sufficient resources to form a complete group for patrol. - local DefendersNeeded - if DefenderSquadron.ResourceCount == nil then - DefendersNeeded = DefenderSquadron.Grouping - else - if DefenderSquadron.ResourceCount >= DefenderSquadron.Grouping then - DefendersNeeded = DefenderSquadron.Grouping + -- Determine if there are sufficient resources to form a complete group for patrol. + if DefenderSquadron then + local DefendersNeeded + local DefendersGrouping = ( DefenderSquadron.Grouping or self.DefenderDefault.Grouping ) + if DefenderSquadron.ResourceCount == nil then + DefendersNeeded = DefendersGrouping else - DefendersNeeded = DefenderSquadron.ResourceCount + if DefenderSquadron.ResourceCount >= DefendersGrouping then + DefendersNeeded = DefendersGrouping + else + DefendersNeeded = DefenderSquadron.ResourceCount + end + end + + if Patrol then + self:ResourceQueue( true, DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, nil, SquadronName ) end - end - - if Patrol then - self:ResourceQueue( true, DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, nil, SquadronName ) end end diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 91a6eaffa..fe62ff71f 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -746,7 +746,6 @@ end 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( self.TaskDelay, EventData ) end diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index e7caaea2d..04eab84a8 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -58,7 +58,7 @@ do -- DETECTION MANAGER } --- @field Tasking.CommandCenter#COMMANDCENTER - DETECTION_MANAGER.CC = {} + DETECTION_MANAGER.CC = nil --- FAC constructor. -- @param #DETECTION_MANAGER self diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index ce317db04..a3462e786 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1900,7 +1900,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' )" + --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: (.*)") From 10b96f6cce24006185d99ef3632c3aa07dc6e9c4 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 9 Mar 2019 10:09:03 +0100 Subject: [PATCH 195/485] Updates to decision engine of the AI_A2G_DISPATCHER --- .../Moose/AI/AI_A2G_Dispatcher.lua | 12 +- .../Moose/AI/AI_Air_Dispatcher.lua | 3291 +++++++++++++++++ Moose Development/Moose/Utilities/Utils.lua | 25 + 3 files changed, 3325 insertions(+), 3 deletions(-) create mode 100644 Moose Development/Moose/AI/AI_Air_Dispatcher.lua diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 4c5358729..9ab518dbd 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -1143,7 +1143,9 @@ do -- AI_A2G_DISPATCHER for Resource = 1, DefenderSquadron.ResourceCount or 0 do self:ResourcePark( DefenderSquadron ) end + self:I( "Parked resources for squadron " .. DefenderSquadron.Name ) end + end @@ -3771,7 +3773,7 @@ do -- AI_A2G_DISPATCHER self:F( { DefenderSquadrons = self.DefenderSquadrons } ) - for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons or {} ) do + for SquadronName, DefenderSquadron in UTILS.rpairs( self.DefenderSquadrons or {} ) do if DefenderSquadron[DefenseTaskType] then @@ -3792,8 +3794,12 @@ do -- AI_A2G_DISPATCHER -- Check if there is a defense line... local HasDefenseLine = self:HasDefenseLine( AirbaseCoordinate, DetectedItem ) if HasDefenseLine == true then - ClosestDistance = InterceptDistance - ClosestDefenderSquadronName = SquadronName + local ProbabilityRange = ( self.DefenseRadius - InterceptDistance ) / self.DefenseRadius + local Probability = math.random() + if Probability > ProbabilityRange then + ClosestDistance = InterceptDistance + ClosestDefenderSquadronName = SquadronName + end end end end diff --git a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua new file mode 100644 index 000000000..ab5adbe5f --- /dev/null +++ b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua @@ -0,0 +1,3291 @@ +--- **AI** - Intermediary class that realized the common methods for the A2G and A2A dispatchers. +-- +-- === +-- +-- Features: +-- +-- * Common methods for A2G and A2A dispatchers. +-- +-- === +-- +-- ### Author: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). +-- +-- @module AI.AI_Air_Dispatcher +-- @image AI_Air_To_Ground_Dispatching.JPG + + + +do -- AI_AIR_DISPATCHER + + --- AI_AIR_DISPATCHER class. + -- @type AI_AIR_DISPATCHER + -- @extends Tasking.DetectionManager#DETECTION_MANAGER + + --- Intermediary class that realized the common methods for the A2G and A2A dispatchers. + -- + -- === + -- + -- @field #AI_AIR_DISPATCHER + AI_AIR_DISPATCHER = { + ClassName = "AI_AIR_DISPATCHER", + Detection = nil, + } + + --- Definition of a Squadron. + -- @type AI_AIR_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_AIR_DISPATCHER.DefenseCoordinates + -- @map <#string,Core.Point#COORDINATE> A list of all defense coordinates mapped per defense coordinate name. + + --- Enumerator for spawns at airbases + -- @type AI_AIR_DISPATCHER.Takeoff + -- @extends Wrapper.Group#GROUP.Takeoff + + --- @field #AI_AIR_DISPATCHER.Takeoff Takeoff + AI_AIR_DISPATCHER.Takeoff = GROUP.Takeoff + + --- Defnes Landing location. + -- @field Landing + AI_AIR_DISPATCHER.Landing = { + NearAirbase = 1, + AtRunway = 2, + AtEngineShutdown = 3, + } + + --- A defense queue item description + -- @type AI_AIR_DISPATCHER.DefenseQueueItem + -- @field Squadron + -- @field #AI_AIR_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_AIR_DISPATCHER.DefenseQueue + -- @list<#AI_AIR_DISPATCHER.DefenseQueueItem> DefenseQueueItem A list of all defenses being queued ... + + --- @field #AI_AIR_DISPATCHER.DefenseQueue DefenseQueue + AI_AIR_DISPATCHER.DefenseQueue = {} + + + + + --- AI_AIR_DISPATCHER constructor. + -- @param #AI_AIR_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. + -- @return #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:New( Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_AIR_DISPATCHER + + self.Detection = Detection -- Functional.Detection#DETECTION_AREAS + + -- 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. + + self:SetDefenseRadius() + self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. + + self:SetDefaultTakeoff( AI_AIR_DISPATCHER.Takeoff.Air ) + self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. + self:SetDefaultLanding( AI_AIR_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_AIR_DISPATCHER] OnAfterAssign + -- @param #AI_AIR_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_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] OnBeforePatrol + -- @param #AI_AIR_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Patrol Handler OnAfter for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] OnAfterPatrol + -- @param #AI_AIR_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Patrol Trigger for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] Patrol + -- @param #AI_AIR_DISPATCHER self + + --- Patrol Asynchronous Trigger for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] __Patrol + -- @param #AI_AIR_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "Defend", "*" ) + + --- Defend Handler OnBefore for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] OnBeforeDefend + -- @param #AI_AIR_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Defend Handler OnAfter for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] OnAfterDefend + -- @param #AI_AIR_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Defend Trigger for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] Defend + -- @param #AI_AIR_DISPATCHER self + + --- Defend Asynchronous Trigger for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] __Defend + -- @param #AI_AIR_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "Engage", "*" ) + + --- Engage Handler OnBefore for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] OnBeforeEngage + -- @param #AI_AIR_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Engage Handler OnAfter for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] OnAfterEngage + -- @param #AI_AIR_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Engage Trigger for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] Engage + -- @param #AI_AIR_DISPATCHER self + + --- Engage Asynchronous Trigger for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] __Engage + -- @param #AI_AIR_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.TakeoffScheduleID = self:ScheduleRepeat( 10, 10, 0, nil, self.ResourceTakeoff, self ) + + self:__Start( 5 ) + + return self + end + + + --- @param #AI_AIR_DISPATCHER self + function AI_AIR_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:ResourcePark( DefenderSquadron ) + end + end + end + + + --- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:ResourcePark( 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_AIR_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_AIR_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_AIR_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_AIR_DISPATCHER:OnEventCrashOrDead( EventData ) + self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) + end + + --- @param #AI_AIR_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_AIR_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_AIR_DISPATCHER.Landing.AtRunway then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ResourcePark( Squadron, Defender ) + return + end + if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then + -- Damaged units cannot be repaired anymore. + DefenderUnit:Destroy() + return + end + end + end + + --- @param #AI_AIR_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_AIR_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_AIR_DISPATCHER.Landing.AtEngineShutdown and + not DefenderUnit:InAir() then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ResourcePark( Squadron, Defender ) + end + end + end + + + --- Define the defense radius to check if a target can be engaged by a squadron group. + -- When targets are detected that are still really far off, you don't want the 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_AIR_DISPATCHER react immediately when a certain border or area is not being crossed. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetDefendRadius}() to set a specific defend radius for all squadrons, + -- **the Defense Radius is defined for ALL squadrons which are operational.** + -- + -- @param #AI_AIR_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_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_AIR_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_AIR_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_AIR_DISPATCHER self + -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the Tactical Display for debug mode. + -- A2GDispatcher:SetTacticalDisplay( true ) + -- + function AI_AIR_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_AIR_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_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default damage treshold. + -- A2GDispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. + -- + function AI_AIR_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_AIR_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_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol time interval. + -- A2GDispatcher:SetDefaultPatrolTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. + -- + function AI_AIR_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_AIR_DISPATCHER self + -- @param #number PatrolLimit The maximum amount of Patrol that can be airborne at the same time for the squadron. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- A2GDispatcher:SetDefaultPatrolLimit( 2 ) -- Maximum 2 Patrol per squadron. + -- + function AI_AIR_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_AIR_DISPATCHER self + -- @param #number EngageLimit The maximum engages that can be done at the same time per squadron. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- A2GDispatcher:SetDefaultEngageLimit( 2 ) -- Maximum 2 engagements with the enemy per squadron. + -- + function AI_AIR_DISPATCHER:SetDefaultEngageLimit( EngageLimit ) + + self.DefenderDefault.EngageLimit = EngageLimit + + return self + end + + + function AI_AIR_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_AIR_DISPATCHER self + -- @param DetectedItem + -- @return #table A list of the defender friendlies nearby, sorted by distance. + function AI_AIR_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_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:GetDefenderTasks() + return self.DefenderTasks or {} + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:GetDefenderTask( Defender ) + return self.DefenderTasks[Defender] + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:GetDefenderTaskFsm( Defender ) + return self:GetDefenderTask( Defender ).Fsm + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:GetDefenderTaskTarget( Defender ) + return self:GetDefenderTask( Defender ).Target + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:GetDefenderTaskSquadronName( Defender ) + return self:GetDefenderTask( Defender ).SquadronName + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_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_AIR_DISPATCHER self + function AI_AIR_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_AIR_DISPATCHER self + function AI_AIR_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_AIR_DISPATCHER self + -- @param Wrapper.Group#GROUP AIGroup + function AI_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER + function AI_AIR_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:SetSquadronTakeoffInterval( SquadronName, 0 ) + + self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) + + return self + end + + --- Get an item from the Squadron table. + -- @param #AI_AIR_DISPATCHER self + -- @return #table + function AI_AIR_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_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- A2GDispatcher:SetSquadronVisible( "Mineralnye" ) + -- + -- TODO: disabling because of bug in queueing. +-- function AI_AIR_DISPATCHER:SetSquadronVisible( SquadronName ) +-- +-- self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} +-- +-- local DefenderSquadron = self:GetSquadron( SquadronName ) +-- +-- DefenderSquadron.Uncontrolled = true +-- self:SetSquadronTakeoffFromParkingCold( SquadronName ) +-- self:SetSquadronLandingAtEngineShutdown( SquadronName ) +-- +-- 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_AIR_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_AIR_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 + + --- @param #AI_AIR_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_AIR_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. + -- + -- - @{#AI_AIR_DISPATCHER:SetSquadronSeadPatrolInterval} for SEAD tasks. + -- - @{#AI_AIR_DISPATCHER:SetSquadronSeadPatrolInterval} for CAS tasks. + -- - @{#AI_AIR_DISPATCHER:SetSquadronSeadPatrolInterval} for BAI tasks. + -- + -- @param #AI_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER:SetSquadronBaiPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "BAI" ) + + end + + + --- + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_AIR_DISPATCHER + function AI_AIR_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_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #table DefenderSquadron + function AI_AIR_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_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #table DefenderSquadron + function AI_AIR_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_AIR_DISPATCHER:SetSquadronSeadEngageLimit} for SEAD tasks. + -- - @{#AI_AIR_DISPATCHER:SetSquadronSeadEngageLimit} for CAS tasks. + -- - @{#AI_AIR_DISPATCHER:SetSquadronSeadEngageLimit} for BAI tasks. + -- + -- @param #AI_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @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. + -- A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200 ) + -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100 ) + -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200 ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} + + local Sead = DefenderSquadron.SEAD + Sead.Name = SquadronName + Sead.EngageMinSpeed = EngageMinSpeed + Sead.EngageMaxSpeed = EngageMaxSpeed + Sead.EngageFloorAltitude = EngageFloorAltitude or 500 + Sead.EngageCeilingAltitude = EngageCeilingAltitude or 1000 + Sead.Defend = true + + self:F( { Sead = Sead } ) + end + + --- Set the squadron SEAD engage limit. + -- @param #AI_AIR_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_AIR_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_AIR_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_AIR_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 (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_AIR_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_AIR_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.PatrolFloorAltitude = FloorAltitude + SeadPatrol.PatrolCeilingAltitude = CeilingAltitude + SeadPatrol.EngageFloorAltitude = FloorAltitude + SeadPatrol.EngageCeilingAltitude = 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_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @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. + -- A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200 ) + -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100 ) + -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200 ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.CAS = DefenderSquadron.CAS or {} + + local Cas = DefenderSquadron.CAS + Cas.Name = SquadronName + Cas.EngageMinSpeed = EngageMinSpeed + Cas.EngageMaxSpeed = EngageMaxSpeed + Cas.EngageFloorAltitude = EngageFloorAltitude or 500 + Cas.EngageCeilingAltitude = EngageCeilingAltitude or 1000 + Cas.Defend = true + + self:F( { Cas = Cas } ) + end + + + --- Set the squadron CAS engage limit. + -- @param #AI_AIR_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_AIR_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_AIR_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_AIR_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 (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_AIR_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_AIR_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.PatrolFloorAltitude = FloorAltitude + CasPatrol.PatrolCeilingAltitude = CeilingAltitude + CasPatrol.EngageFloorAltitude = FloorAltitude + CasPatrol.EngageCeilingAltitude = 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_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @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. + -- A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200 ) + -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100 ) + -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200 ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.BAI = DefenderSquadron.BAI or {} + + local Bai = DefenderSquadron.BAI + Bai.Name = SquadronName + Bai.EngageMinSpeed = EngageMinSpeed + Bai.EngageMaxSpeed = EngageMaxSpeed + Bai.EngageFloorAltitude = EngageFloorAltitude or 500 + Bai.EngageCeilingAltitude = EngageCeilingAltitude or 1000 + Bai.Defend = true + + self:F( { Bai = Bai } ) + end + + + --- Set the squadron BAI engage limit. + -- @param #AI_AIR_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_AIR_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_AIR_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_AIR_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 (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_AIR_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_AIR_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.PatrolFloorAltitude = FloorAltitude + BaiPatrol.PatrolCeilingAltitude = CeilingAltitude + BaiPatrol.EngageFloorAltitude = FloorAltitude + BaiPatrol.EngageCeilingAltitude = 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_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER + function AI_AIR_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_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER + function AI_AIR_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_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER + function AI_AIR_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_AIR_DISPATCHER self + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Set a grouping by default per 2 airplanes. + -- A2GDispatcher:SetDefaultGrouping( 2 ) + -- + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_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_AIR_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_AIR_DISPATCHER:New( ... ) + -- + -- -- Set a grouping per 2 airplanes. + -- A2GDispatcher:SetSquadronGrouping( "SquadronName", 2 ) + -- + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_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_AIR_DISPATCHER self + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_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_AIR_DISPATCHER + -- + function AI_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER + -- + function AI_AIR_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_AIR_DISPATCHER self + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- A2GDispatcher:SetDefaultTakeoffInAir() + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetDefaultTakeoffInAir() + + self:SetDefaultTakeoff( AI_AIR_DISPATCHER.Takeoff.Air ) + + return self + end + + + --- Sets flights to take-off in the air, as part of the defense system. + -- @param #AI_AIR_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_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoffInAir( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) + + self:SetSquadronTakeoff( SquadronName, AI_AIR_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_AIR_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off from the runway. + -- A2GDispatcher:SetDefaultTakeoffFromRunway() + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetDefaultTakeoffFromRunway() + + self:SetDefaultTakeoff( AI_AIR_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights to take-off from the runway, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from the runway. + -- A2GDispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_AIR_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_AIR_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off at a hot parking spot. + -- A2GDispatcher:SetDefaultTakeoffFromParkingHot() + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetDefaultTakeoffFromParkingHot() + + self:SetDefaultTakeoff( AI_AIR_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_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_AIR_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_AIR_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- A2GDispatcher:SetDefaultTakeoffFromParkingCold() + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetDefaultTakeoffFromParkingCold() + + self:SetDefaultTakeoff( AI_AIR_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_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- A2GDispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_AIR_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_AIR_DISPATCHER self + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_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_AIR_DISPATCHER + -- + function AI_AIR_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_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_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_AIR_DISPATCHER + -- + function AI_AIR_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_AIR_DISPATCHER self + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_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_AIR_DISPATCHER + function AI_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER + function AI_AIR_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_AIR_DISPATCHER self + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let flights by default to land near the airbase and despawn. + -- A2GDispatcher:SetDefaultLandingNearAirbase() + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetDefaultLandingNearAirbase() + + self:SetDefaultLanding( AI_AIR_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_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let flights to land near the airbase and despawn. + -- A2GDispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_AIR_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_AIR_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land at the runway and despawn. + -- A2GDispatcher:SetDefaultLandingAtRunway() + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetDefaultLandingAtRunway() + + self:SetDefaultLanding( AI_AIR_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights to land and despawn at the runway, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let flights land at the runway and despawn. + -- A2GDispatcher:SetSquadronLandingAtRunway( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_AIR_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_AIR_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land and despawn at engine shutdown. + -- A2GDispatcher:SetDefaultLandingAtEngineShutdown() + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetDefaultLandingAtEngineShutdown() + + self:SetDefaultLanding( AI_AIR_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + + --- Sets flights to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let flights land and despawn at engine shutdown. + -- A2GDispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_AIR_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_AIR_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_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_AIR_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_AIR_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_AIR_DISPATCHER self + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_AIR_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_AIR_DISPATCHER:SetDefaultTanker( TankerName ) + + self.DefenderDefault.TankerName = TankerName + + return self + end + + + --- Set the squadron tanker where defenders will Refuel in the air. + -- @param #AI_AIR_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_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_AIR_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_AIR_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.TankerName = TankerName + + return self + end + + + + + --- @param #AI_AIR_DISPATCHER self + function AI_AIR_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_AIR_DISPATCHER self + function AI_AIR_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_AIR_DISPATCHER:GetSquadronFromDefender( Defender ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + self:F( { DefenderName = DefenderName } ) + return self.Defenders[ DefenderName ] + end + + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_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_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:CountDefendersEngaged( AttackerDetection, AttackerCount ) + + -- First, count the active AIGroups Units, targetting the DetectedSet + local DefendersEngaged = 0 + local DefendersTotal = 0 + + local AttackerSet = AttackerDetection.Set + 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 ) + self:F( { SquadronOverhead = SquadronOverhead } ) + 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 + + for QueueID, QueueItem in pairs( self.DefenseQueue ) do + local QueueItem = QueueItem -- #AI_AIR_DISPATCHER.DefenseQueueItem + if QueueItem.AttackerDetection and QueueItem.AttackerDetection.ItemID == AttackerDetection.ItemID then + DefendersMissing = DefendersMissing - QueueItem.DefendersNeeded / QueueItem.DefenderSquadron.Overhead + --DefendersEngaged = DefendersEngaged + QueueItem.DefenderGrouping + self:F( { QueueItemName = QueueItem.Defense, QueueItem_ItemID = QueueItem.AttackerDetection.ItemID, DetectedItem = AttackerDetection.ItemID, DefendersMissing = DefendersMissing } ) + end + end + + self:F( { DefenderCount = DefendersEngaged } ) + + return DefendersTotal, DefendersEngaged, DefendersMissing + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_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_AIR_DISPATCHER self + function AI_AIR_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 + 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 + 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 ) + Defender:Activate() + 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_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:onafterPatrol( From, Event, To, SquadronName, DefenseTaskType ) + + local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType ) + + -- Determine if there are sufficient resources to form a complete group for patrol. + if DefenderSquadron then + local DefendersNeeded + local DefendersGrouping = ( DefenderSquadron.Grouping or self.DefenderDefault.Grouping ) + if DefenderSquadron.ResourceCount == nil then + DefendersNeeded = DefendersGrouping + else + if DefenderSquadron.ResourceCount >= DefendersGrouping then + DefendersNeeded = DefendersGrouping + else + DefendersNeeded = DefenderSquadron.ResourceCount + end + end + + if Patrol then + self:ResourceQueue( true, DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, nil, SquadronName ) + end + end + + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:ResourceQueue( Patrol, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName ) + + self:F( { DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName } ) + + local DefenseQueueItem = {} -- #AI_AIR_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_AIR_DISPATCHER self + function AI_AIR_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_AIR_DISPATCHER self + function AI_AIR_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_AIR_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_AIR_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) + + Dispatcher:ClearDefenderTaskTarget( Defender ) + end + + --- @param #AI_AIR_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_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + Dispatcher:ResourcePark( Squadron, Defender ) + end + end + end + + end + + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_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_AIR_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:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit + end + end + + function Fsm:OnAfterEngageRoute( Defender, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", Defender:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = Fsm:GetDispatcher() -- #AI_AIR_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route, bearing " .. Coordinate:ToString( Defender ) ) + end + + function Fsm:OnAfterEngage( Defender, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", Defender:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetName() + local Dispatcher = Fsm:GetDispatcher() -- #AI_AIR_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() + + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging target, bearing " .. Coordinate:ToString( Defender ) ) + end + + function Fsm:onafterRTB( Defender, From, Event, To ) + self:F({"Defender RTB", Defender:GetName()}) + + local DefenderName = Defender:GetName() + local Dispatcher = self:GetDispatcher() -- #AI_AIR_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " RTB." ) + + self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + + Dispatcher:ClearDefenderTaskTarget( Defender ) + end + + --- @param #AI_AIR_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_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + Dispatcher:ResourcePark( Squadron, Defender ) + end + end + end + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_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( AttackerDetection.Set ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( Defender, AttackerDetection ) + + end + end + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:HasDefenseLine( DefenseCoordinate, DetectedItem ) + + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) + + -- Now check if this coordinate is not in a danger zone, meaning, that the attack line is not crossing other coordinates. + -- (y1 – y2)x + (x2 – x1)y + (x1y2 – x2y1) = 0 + + local c1 = DefenseCoordinate + local c2 = AttackCoordinate + + local a = c1.z - c2.z -- Calculate a + local b = c2.x - c1.x -- Calculate b + local c = c1.x * c2.z - c2.x * c1.z -- calculate c + + local ok = true + + -- Now we check if each coordinate radius of about 30km of each attack is crossing a defense line. If yes, then this is not a good attack! + for AttackItemID, CheckAttackItem in pairs( self.Detection:GetDetectedItems() ) do + + -- Only compare other detected coordinates. + if AttackItemID ~= DetectedItem.ID then + + local CheckAttackCoordinate = self.Detection:GetDetectedItemCoordinate( CheckAttackItem ) + + local x = CheckAttackCoordinate.x + local y = CheckAttackCoordinate.z + local r = 5000 + + -- now we check if the coordinate is intersecting with the defense line. + + local IntersectDistance = ( math.abs( a * x + b * y + c ) ) / math.sqrt( a * a + b * b ) + self:F( { IntersectDistance = IntersectDistance, x = x, y = y } ) + + local IntersectAttackDistance = CheckAttackCoordinate:Get2DDistance( DefenseCoordinate ) + + self:F( { IntersectAttackDistance=IntersectAttackDistance, EvaluateDistance=EvaluateDistance } ) + + -- If the distance of the attack coordinate is larger than the test radius; then the line intersects, and this is not a good coordinate. + if IntersectDistance < r and IntersectAttackDistance < EvaluateDistance then + ok = false + break + end + end + end + + return ok + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:onafterDefend( From, Event, To, DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, DefenderFriendlies, DefenseTaskType ) + + self:F( { From, Event, To, DetectedItem.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } ) + + DetectedItem.Type = DefenseTaskType -- This is set to report the task type in the status panel. + + local AttackerSet = DetectedItem.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 + + -- Here we check if the defenders have a defense line to the attackers. + -- If the attackers are behind enemy lines or too close to an other defense line; then don´t engage. + local DefenseCoordinate = DefenderGroup:GetCoordinate() + local HasDefenseLine = self:HasDefenseLine( DefenseCoordinate, DetectedItem ) + + if HasDefenseLine == true then + local SquadronName = self:GetDefenderTask( DefenderGroup ).SquadronName + local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) + + local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) + Fsm:EngageRoute( AttackerSet ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( DefenderGroup, DetectedItem ) + + local DefenderGroupSize = DefenderGroup:GetSize() + DefendersMissing = DefendersMissing - DefenderGroupSize / SquadronOverhead + DefendersTotal = DefendersTotal + DefenderGroupSize / SquadronOverhead + end + + 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 AirbaseCoordinate = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE + local AttackerCoord = AttackerUnit:GetCoordinate() + local InterceptCoord = DetectedItem.InterceptCoord + self:F( { InterceptCoord = InterceptCoord } ) + if InterceptCoord then + local InterceptDistance = AirbaseCoordinate:Get2DDistance( InterceptCoord ) + local AirbaseDistance = AirbaseCoordinate: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 + + -- Check if there is a defense line... + local HasDefenseLine = self:HasDefenseLine( AirbaseCoordinate, DetectedItem ) + if HasDefenseLine == true then + ClosestDistance = InterceptDistance + ClosestDefenderSquadronName = SquadronName + end + 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 + self:ResourceQueue( false, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, DetectedItem, ClosestDefenderSquadronName ) + DefendersNeeded = DefendersNeeded - DefenderGrouping + DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead + 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_AIR_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_AIR_DISPATCHER:Evaluate_SEAD( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group, then the amount of radar emitters will be returned; that need to be attacked. + + if ( AttackerCount > 0 ) then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) + + 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_AIR_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_AIR_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? + + if IsCas == true then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) + + 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_AIR_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_AIR_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, AttackerCount ) + + 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_AIR_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_AIR_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 + 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 + 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 TargetCount = DefenderTask.Target.Set:Count() + if TargetCount == 0 then + self:F( { "All Targets destroyed in Target, removing:", DefenderTask.Target.Index } ) + self:ClearDefenderTask( DefenderGroup ) + end + end + end + end + end + end + + local Report = REPORT:New( "\nTactical Overview" ) + + local DefenderGroupCount = 0 + + 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 AttackCoordinate = 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 EngageCoordinate = nil + + for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do + local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE + + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) + + 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 + EngageCoordinate = DefenseCoordinate + break + end + end + end + + if EngageCoordinate 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", EngageCoordinate ) + 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", EngageCoordinate ) + 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", EngageCoordinate ) + 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 + local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() + Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "■", ThreatLevel ) ) ) + 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 ) ) + + Report:Add( string.format( "\n - %d Queued Aircraft Launches", #self.DefenseQueue ) ) + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + local DefenseQueueItem = DefenseQueueItem -- #AI_AIR_DISPATCHER.DefenseQueueItem + Report:Add( string.format( " - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) ) + + end + + Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) ) + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + Report:Add( string.format( " - %s - %d", DefenderSquadronName, DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount or "n/a" ) ) + end + + 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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + function AI_AIR_DISPATCHER:SchedulerPatrol( SquadronName ) + local PatrolTaskTypes = { "SEAD", "CAS", "BAI" } + local PatrolTaskType = PatrolTaskTypes[math.random(1,3)] + self:Patrol( SquadronName, PatrolTaskType ) + end + +end + + diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 9070b0087..873b1b4b3 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -519,6 +519,31 @@ function UTILS.spairs( t, order ) end end +-- Here is a customized version of pairs, which I called rpairs because it iterates over the table in a random order. +function UTILS.rpairs( t ) + -- collect the keys + + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + + local random = {} + local j = #keys + for i = 1, j do + local k = math.random( 1, #keys ) + random[i] = keys[k] + table.remove( keys, k ) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if random[i] then + return random[i], t[random[i]] + end + end +end + -- get a new mark ID for markings function UTILS.GetMarkID() From afc918c5e56d8932629484dc37c1a1c939d5435c Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 9 Mar 2019 17:29:18 +0100 Subject: [PATCH 196/485] Updates in defense system. Added SetSquadronEngageProbability method to decide per squadron what is the probability when it will engage. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 684 ++---------------- .../Moose/AI/AI_Air_Dispatcher.lua | 5 +- 2 files changed, 55 insertions(+), 634 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 9ab518dbd..0e64be634 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -1265,39 +1265,6 @@ do -- AI_A2G_DISPATCHER 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 @@ -2518,6 +2485,30 @@ do -- AI_A2G_DISPATCHER end + --- Sets the engage probability if the squadron will engage on a detected target. + -- This can be configured per squadron, to ensure that each squadron as a specific defensive probability setting. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number EngageProbability The probability when the squadron will consider to engage the detected target. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set an defense probability for squadron SquadronName of 50%. + -- -- This will result that this squadron has 50% chance to engage on a detected target. + -- A2GDispatcher:SetSquadronEngageProbability( "SquadronName", 0.5 ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronEngageProbability( SquadronName, EngageProbability ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.EngageProbability = EngageProbability + + 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. @@ -3580,10 +3571,13 @@ do -- AI_A2G_DISPATCHER local DefenderName = Defender:GetName() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - local FirstUnit = AttackSetUnit:GetFirst() - local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route, bearing " .. Coordinate:ToString( Defender ) ) + + if FirstUnit then + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route, bearing " .. Coordinate:ToString( Defender ) ) + end end function Fsm:OnAfterEngage( Defender, From, Event, To, AttackSetUnit ) @@ -3594,9 +3588,11 @@ do -- AI_A2G_DISPATCHER local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) local FirstUnit = AttackSetUnit:GetFirst() - local Coordinate = FirstUnit:GetCoordinate() - - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging target, bearing " .. Coordinate:ToString( Defender ) ) + if FirstUnit then + local Coordinate = FirstUnit:GetCoordinate() + + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging target, bearing " .. Coordinate:ToString( Defender ) ) + end end function Fsm:onafterRTB( Defender, From, Event, To ) @@ -3765,7 +3761,7 @@ do -- AI_A2G_DISPATCHER DefenderCount = DefendersMissing local ClosestDistance = 0 - local ClosestDefenderSquadronName = nil + local EngageSquadronName = nil local BreakLoop = false @@ -3786,20 +3782,17 @@ do -- AI_A2G_DISPATCHER local AirbaseDistance = AirbaseCoordinate: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 - - -- Check if there is a defense line... - local HasDefenseLine = self:HasDefenseLine( AirbaseCoordinate, DetectedItem ) - if HasDefenseLine == true then - local ProbabilityRange = ( self.DefenseRadius - InterceptDistance ) / self.DefenseRadius - local Probability = math.random() - if Probability > ProbabilityRange then - ClosestDistance = InterceptDistance - ClosestDefenderSquadronName = SquadronName - end + -- Only intercept if the distance to target is smaller or equal to the GciRadius limit. + if AirbaseDistance <= self.DefenseRadius then + + -- Check if there is a defense line... + local HasDefenseLine = self:HasDefenseLine( AirbaseCoordinate, DetectedItem ) + if HasDefenseLine == true then + local EngageProbability = ( DefenderSquadron.EngageProbability or 1 ) + local Probability = math.random() + if Probability < EngageProbability then + EngageSquadronName = SquadronName + break end end end @@ -3807,9 +3800,9 @@ do -- AI_A2G_DISPATCHER end end - if ClosestDefenderSquadronName then + if EngageSquadronName then - local DefenderSquadron, Defense = self:CanDefend( ClosestDefenderSquadronName, DefenseTaskType ) + local DefenderSquadron, Defense = self:CanDefend( EngageSquadronName, DefenseTaskType ) if Defense then @@ -3845,7 +3838,7 @@ do -- AI_A2G_DISPATCHER end while ( DefendersNeeded > 0 ) do - self:ResourceQueue( false, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, DetectedItem, ClosestDefenderSquadronName ) + self:ResourceQueue( false, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, DetectedItem, EngageSquadronName ) DefendersNeeded = DefendersNeeded - DefenderGrouping DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead end -- while ( DefendersNeeded > 0 ) do @@ -4264,578 +4257,3 @@ do 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_Air_Dispatcher.lua b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua index ab5adbe5f..c802521dd 100644 --- a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua @@ -111,6 +111,7 @@ do -- AI_AIR_DISPATCHER self:SetDefaultTakeoff( AI_AIR_DISPATCHER.Takeoff.Air ) self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. self:SetDefaultLanding( AI_AIR_DISPATCHER.Landing.NearAirbase ) + self:SetDefaultEngageRadius( 150000 ) self:SetDefaultOverhead( 1 ) self:SetDefaultGrouping( 1 ) @@ -382,6 +383,7 @@ do -- AI_AIR_DISPATCHER return self end + --- Define a border area to simulate a **cold war** scenario. @@ -508,7 +510,7 @@ do -- AI_AIR_DISPATCHER --- 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. + -- The default eatrol limit is 1, which means one patrol group maximum per squadron. -- @param #AI_AIR_DISPATCHER self -- @param #number EngageLimit The maximum engages that can be done at the same time per squadron. -- @return #AI_AIR_DISPATCHER @@ -528,6 +530,7 @@ do -- AI_AIR_DISPATCHER end + function AI_AIR_DISPATCHER:SetIntercept( InterceptDelay ) self.DefenderDefault.InterceptDelay = InterceptDelay From 925ce3ad63d4dac9d687a736da01f1ca80c95817 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 10 Mar 2019 16:41:53 +0100 Subject: [PATCH 197/485] Auto assignment of tasks with prioritization of an auto assign method. The default assign method is random, which is set at the command center. Each task type implements a prioritization mechanism which calculates the priotity of the task based on various methods: random, distance or priority. The distance is calculated from the task coordinate from the command center location. Also fixed in a first try the route bug on Controllables. --- .../Moose/Tasking/CommandCenter.lua | 48 +++++++++++++++++-- Moose Development/Moose/Tasking/TaskInfo.lua | 9 ++++ Moose Development/Moose/Tasking/Task_A2A.lua | 20 ++++++++ Moose Development/Moose/Tasking/Task_A2G.lua | 20 ++++++++ .../Moose/Tasking/Task_CARGO.lua | 17 +++++++ .../Moose/Tasking/Task_Capture_Zone.lua | 25 ++++++++-- .../Moose/Wrapper/Controllable.lua | 4 +- 7 files changed, 133 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index bbb159668..ac1f310c8 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -175,6 +175,14 @@ COMMANDCENTER = { CommunicationMode = "80", } + +--- @type COMMANDCENTER.AutoAssignMethods +COMMANDCENTER.AutoAssignMethods = { + ["Random"] = 1, + ["Distance"] = 2, + ["Priority"] = 3 + } + --- The constructor takes an IDENTIFIABLE as the HQ command center. -- @param #COMMANDCENTER self -- @param Wrapper.Positionable#POSITIONABLE CommandCenterPositionable @@ -192,6 +200,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) self:SetAutoAssignTasks( false ) self:SetAutoAcceptTasks( true ) + self:SetAutoAssignMethod( COMMANDCENTER.AutoAssignMethods.Random ) self:HandleEvent( EVENTS.Birth, --- @param #COMMANDCENTER self @@ -462,7 +471,7 @@ function COMMANDCENTER:GetMenu( TaskGroup ) self.CommandCenterMenus[TaskGroup] = CommandCenterMenu if self.AutoAssignTasks == false then - local AssignTaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Assign Task", CommandCenterMenu, self.AssignRandomTask, self, TaskGroup ):SetTime(MenuTime):SetTag("AutoTask") + local AssignTaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Assign Task", CommandCenterMenu, self.AssignTask, self, TaskGroup ):SetTime(MenuTime):SetTag("AutoTask") end CommandCenterMenu:Remove( MenuTime, "AutoTask" ) @@ -473,22 +482,33 @@ end --- Assigns a random task to a TaskGroup. -- @param #COMMANDCENTER self -- @return #COMMANDCENTER -function COMMANDCENTER:AssignRandomTask( TaskGroup ) +function COMMANDCENTER:AssignTask( TaskGroup ) local Tasks = {} + local AssignPriority = 99999999 + local AutoAssignMethod = self.AutoAssignMethod for MissionID, Mission in pairs( self:GetMissions() ) do local Mission = Mission -- Tasking.Mission#MISSION local MissionTasks = Mission:GetGroupTasks( TaskGroup ) for MissionTaskName, MissionTask in pairs( MissionTasks or {} ) do - Tasks[#Tasks+1] = MissionTask + local TaskPriority = MissionTask:GetAutoAssignPriority( self.AutoAssignMethod, self, TaskGroup ) + if TaskPriority < AssignPriority then + AssignPriority = TaskPriority + Tasks = {} + end + if TaskPriority == AssignPriority then + Tasks[#Tasks+1] = MissionTask + end end end local Task = Tasks[ math.random( 1, #Tasks ) ] -- Tasking.Task#TASK + + self:I( "Assigning task " .. Task:GetName() .. " using auto assign method " .. self.AutoAssignMethod .. " to " .. TaskGroup:GetName() .. " with task priority " .. AssignPriority ) if not self.AutoAcceptTasks == true then - Task:SetAssignMethod( ACT_ASSIGN_MENU_ACCEPT:New( Task.TaskBriefing ) ) + Task:SetAutoAssignMethod( ACT_ASSIGN_MENU_ACCEPT:New( Task.TaskBriefing ) ) end Task:AssignToGroup( TaskGroup ) @@ -551,6 +571,24 @@ function COMMANDCENTER:SetAutoAcceptTasks( AutoAccept ) end + +--- Define the method to be used to assign automatically a task from the available tasks in the mission. +-- There are 3 types of methods that can be applied for the moment: +-- +-- 1. Random - assigns a random task in the mission to the player. +-- 2. Distance - assigns a task based on a distance evaluation from the player. The closest are to be assigned first. +-- 3. Priority - assigns a task based on the priority as defined by the mission designer, using the SetTaskPriority parameter. +-- +-- The different task classes implement the logic to determine the priority of automatic task assignment to a player, depending on one of the above methods. +-- The method @{Tasking.Task#TASK.GetAutoAssignPriority} calculate the priority of the tasks to be assigned. +-- @param #COMMANDCENTER self +-- @param #COMMANDCENTER.AutoAssignMethods AutoAssignMethod A selection of an assign method from the COMMANDCENTER.AutoAssignMethods enumeration. +function COMMANDCENTER:SetAutoAssignMethod( AutoAssignMethod ) + + self.AutoAssignMethod = AutoAssignMethod or COMMANDCENTER.AutoAssignMethods.Random + +end + --- Automatically assigns tasks to all TaskGroups. -- @param #COMMANDCENTER self function COMMANDCENTER:AssignTasks() @@ -568,7 +606,7 @@ function COMMANDCENTER:AssignTasks() -- Only groups with planes or helicopters will receive automatic tasks. -- TODO Workaround DCS-BUG-3 - https://github.com/FlightControl-Master/MOOSE/issues/696 if TaskGroup:IsAir() then - self:AssignRandomTask( TaskGroup ) + self:AssignTask( TaskGroup ) end end end diff --git a/Moose Development/Moose/Tasking/TaskInfo.lua b/Moose Development/Moose/Tasking/TaskInfo.lua index e5b23aa16..95c14ee58 100644 --- a/Moose Development/Moose/Tasking/TaskInfo.lua +++ b/Moose Development/Moose/Tasking/TaskInfo.lua @@ -130,6 +130,15 @@ function TASKINFO:AddCoordinate( Coordinate, Order, Detail, Keep ) end +--- Get the Coordinate. +-- @param #TASKINFO self +-- @return Core.Point#COORDINATE Coordinate +function TASKINFO:GetCoordinate() + return self:GetData( "Coordinate" ) +end + + + --- Add Coordinates. -- @param #TASKINFO self -- @param #list Coordinates diff --git a/Moose Development/Moose/Tasking/Task_A2A.lua b/Moose Development/Moose/Tasking/Task_A2A.lua index bd12b7e43..bb91c02bb 100644 --- a/Moose Development/Moose/Tasking/Task_A2A.lua +++ b/Moose Development/Moose/Tasking/Task_A2A.lua @@ -351,6 +351,26 @@ do -- TASK_A2A end end + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. + -- @param #TASK_A2A self + -- @param #number AutoAssignMethod The method to be applied to the task. + -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. + -- @param Wrapper.Group#GROUP TaskGroup The player group. + function TASK_A2A:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup ) + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + return math.random( 1, 9 ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) + local Distance = TaskGroup:GetCoordinate():Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + return math.floor( Distance ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then + return 1 + end + + return 0 + end + end diff --git a/Moose Development/Moose/Tasking/Task_A2G.lua b/Moose Development/Moose/Tasking/Task_A2G.lua index 634234654..af0d25160 100644 --- a/Moose Development/Moose/Tasking/Task_A2G.lua +++ b/Moose Development/Moose/Tasking/Task_A2G.lua @@ -355,6 +355,26 @@ do -- TASK_A2G end end + + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. + -- @param #TASK_A2G self + -- @param #number AutoAssignMethod The method to be applied to the task. + -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. + -- @param Wrapper.Group#GROUP TaskGroup The player group. + function TASK_A2G:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup ) + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + return math.random( 1, 9 ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) + local Distance = TaskGroup:GetCoordinate():Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + return math.floor( Distance ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then + return 1 + end + + return 0 + end end diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index 97a067220..b327ab6a3 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -1381,6 +1381,23 @@ do -- TASK_CARGO return 0 end + + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. + -- @param #TASK_CARGO self + -- @param #number AutoAssignMethod The method to be applied to the task. + -- @param Wrapper.Group#GROUP TaskGroup The player group. + function TASK_CARGO:GetAutoAssignPriority( AutoAssignMethod, TaskGroup ) + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + return math.random( 1, 9 ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then + return 0 + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then + return 1 + end + + return 0 + end diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index 0745ea150..8a56784ea 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -221,7 +221,6 @@ do -- TASK_CAPTURE_ZONE -- @param #TASK_CAPTURE_ZONE self function TASK_CAPTURE_ZONE:UpdateTaskInfo() - local ZoneCoordinate = self.ZoneGoal:GetZone():GetCoordinate() self.TaskInfo:AddCoordinate( ZoneCoordinate, 0, "SOD" ) self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD" ) @@ -230,8 +229,8 @@ do -- TASK_CAPTURE_ZONE function TASK_CAPTURE_ZONE:ReportOrder( ReportGroup ) - local Coordinate = self.TaskInfo:GetData( "Coordinate" ) - --local Coordinate = self.TaskInfo.Coordinates.TaskInfoText + + local Coordinate = self.TaskInfo:GetCoordinate() local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) return Distance @@ -262,5 +261,25 @@ do -- TASK_CAPTURE_ZONE self:__Goal( -10, PlayerUnit, PlayerName ) end + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. + -- @param #TASK_CAPTURE_ZONE self + -- @param #number AutoAssignMethod The method to be applied to the task. + -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. + -- @param Wrapper.Group#GROUP TaskGroup The player group. + function TASK_CAPTURE_ZONE:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup ) + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + return math.random( 1, 9 ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then + local Coordinate = self.TaskInfo:GetCoordinate() + local Distance = TaskGroup:GetCoordinate():Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + return math.floor( Distance ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then + return 1 + end + + return 0 + end + end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index a3462e786..a068d6a72 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -2355,7 +2355,7 @@ do -- Route methods -- Calculate the direct distance between the initial and final points. local LengthDirect=FromCoordinate:Get2DDistance(ToCoordinate) - if GotPath then + if GotPath and LengthRoad then -- Off road part of the rout: Total=OffRoad+OnRoad. LengthOffRoad=LengthOnRoad-LengthRoad @@ -2378,7 +2378,7 @@ do -- Route methods local canroad=false -- Check if a valid path on road could be found. - if GotPath and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. + if GotPath and LengthRoad 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 From e19f0402da30c7cfd807e9bbf0b1a9d36cf41940 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 10 Mar 2019 16:48:38 +0100 Subject: [PATCH 198/485] Only auto assign tasks to planned; replanned or assigned tasks. --- .../Moose/Tasking/CommandCenter.lua | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index ac1f310c8..5c98fc1e0 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -492,13 +492,16 @@ function COMMANDCENTER:AssignTask( TaskGroup ) local Mission = Mission -- Tasking.Mission#MISSION local MissionTasks = Mission:GetGroupTasks( TaskGroup ) for MissionTaskName, MissionTask in pairs( MissionTasks or {} ) do - local TaskPriority = MissionTask:GetAutoAssignPriority( self.AutoAssignMethod, self, TaskGroup ) - if TaskPriority < AssignPriority then - AssignPriority = TaskPriority - Tasks = {} - end - if TaskPriority == AssignPriority then - Tasks[#Tasks+1] = MissionTask + local MissionTask = MissionTask -- Tasking.Task#TASK + if MissionTask:IsStatePlanned() or MissionTask:IsStateReplanned() or MissionTask:IsStateAssigned() then + local TaskPriority = MissionTask:GetAutoAssignPriority( self.AutoAssignMethod, self, TaskGroup ) + if TaskPriority < AssignPriority then + AssignPriority = TaskPriority + Tasks = {} + end + if TaskPriority == AssignPriority then + Tasks[#Tasks+1] = MissionTask + end end end end From 1e0b51a0a9a4110af95a9066b20ae82bcffc265f Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 13 Mar 2019 20:52:57 +0100 Subject: [PATCH 199/485] AIRBOSS v0.9.9.3 - Added F-14 support. --- Moose Development/Moose/Functional/Range.lua | 7 +- Moose Development/Moose/Ops/Airboss.lua | 124 ++++++++++++------- Moose Development/Moose/Wrapper/Group.lua | 41 ++++++ Moose Development/Moose/Wrapper/Unit.lua | 3 +- 4 files changed, 127 insertions(+), 48 deletions(-) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index a3e73b21a..65c591676 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -282,7 +282,7 @@ RANGE.id="RANGE | " --- Range script version. -- @field #string version -RANGE.version="1.2.4" +RANGE.version="1.2.5" --TODO list: --TODO: Verbosity level for messages. @@ -1054,8 +1054,9 @@ function RANGE:OnEventBirth(EventData) -- Reset current strafe status. self.strafeStatus[_uid] = nil - -- Add Menu commands. - self:_AddF10Commands(_unitName) + -- Add Menu commands after a delay of 0.1 seconds. + --self:_AddF10Commands(_unitName) + SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) -- By default, some bomb impact points and do not flare each hit on target. self.PlayerSettings[_playername]={} diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 996ddff0d..a271598ac 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -32,6 +32,7 @@ -- **Supported Aircraft:** -- -- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI) +-- * [F-14B Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI) [**WIP**] -- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI) -- * [AV-8B N/A Harrier](https://forums.eagle.ru/forumdisplay.php?f=555) (Player & AI) [**WIP**] -- * F/A-18C Hornet (AI) @@ -45,16 +46,15 @@ -- the no other fixed wing aircraft (human or AI controlled) are supposed to land on the Tarawa. Currently only Case I is supported. Case II/III take slightly steps from the CVN carrier. -- However, the two Case II/III pattern are very similar so this is not a big drawback. -- --- The implementation is kept general. Therefore, other aircraft and carriers can be added 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. +-- Heatblur's mighty F-14B Tomcat has just been added (March 13th 2019). Beware that this is currently WIP - both the module and the AIRBOSS implementation. -- -- **PLEASE NOTE** that his class is work in progress. 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**. +-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-airboss channel. +-- There you also find an example mission and the necessary voice over sound files. Check out the **pinned messages**. -- -- ## IMPORTANT -- @@ -67,7 +67,7 @@ -- -- ## Youtube Videos -- --- ### Early AIRBOSS Groove Testing: +-- ### AIRBOSS 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) @@ -1210,7 +1210,7 @@ AIRBOSS.AircraftCarrier={ HORNET="FA-18C_hornet", A4EC="A-4E-C", F14A="F-14A_tomcat", - F14B="F-14B_tomcat", + F14B="F-14B", F14A_AI="F-14A", FA18C="F/A-18C", S3B="S-3B", @@ -1621,7 +1621,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.2" +AIRBOSS.version="0.9.9.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -4720,23 +4720,23 @@ 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 + 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 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 + aoa.SLOW = self:_AoAUnit2Deg(playerData, 17.0) --18.33 --17.0 units + aoa.Slow = self:_AoAUnit2Deg(playerData, 16.0) --16.67 --16.0 units + aoa.OnSpeedMax = self:_AoAUnit2Deg(playerData, 15.5) --15.83 --15.5 units + aoa.OnSpeed = self:_AoAUnit2Deg(playerData, 15.0) --15.0 --15.0 units + aoa.OnSpeedMin = self:_AoAUnit2Deg(playerData, 14.5) --14.17 --14.5 units + aoa.Fast = self:_AoAUnit2Deg(playerData, 14.0) --13.33 --14.0 units + aoa.FAST = self:_AoAUnit2Deg(playerData, 13.0) --11.67 --13.0 units elseif skyhawk then -- A-4E-C Skyhawk parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 -- Note that these are arbitrary UNITS and not degrees. We need a conversion formula! @@ -4750,13 +4750,13 @@ function AIRBOSS:_GetAircraftAoA(playerData) aoa.FAST = 8.00 --=16.5/2 elseif harrier then -- AV-8B Harrier parameters. This might need further tuning. - 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 + 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 @@ -4784,8 +4784,13 @@ function AIRBOSS:_AoAUnit2Deg(playerData, aoaunits) -- unit=30 ==> alpha=+40 degrees. -- Assuming a linear relationship between these to points of the graph. + -- However: AoA=15 Units ==> 15 degrees, which is too much. degrees=-10+50/30*aoaunits + -- HB Facebook page https://www.facebook.com/heatblur/photos/a.683612385159716/754368278084126 + -- AoA=15 Units <==> AoA=10.359 degrees. + degrees=0.918*aoaunits-3.411 + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then ---------- @@ -4824,6 +4829,10 @@ function AIRBOSS:_AoADeg2Units(playerData, degrees) -- Assuming a linear relationship between these to points of the graph. aoaunits=(degrees+10)*30/50 + -- HB Facebook page https://www.facebook.com/heatblur/photos/a.683612385159716/754368278084126 + -- AoA=15 Units <==> AoA=10.359 degrees. + aoaunits=1.089*degrees+3.715 + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then ---------- @@ -4870,25 +4879,31 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) alt=UTILS.FeetToMeters(5000) - dist=UTILS.NMToMeters(20) + --dist=UTILS.NMToMeters(20) speed=UTILS.KnotsToMps(250) elseif step==AIRBOSS.PatternStep.ARCIN then - speed=UTILS.KnotsToMps(250) + if tomcat then + speed=UTILS.KnotsToMps(150) + else + speed=UTILS.KnotsToMps(250) + end elseif step==AIRBOSS.PatternStep.ARCOUT then - speed=UTILS.KnotsToMps(250) + if tomcat then + speed=UTILS.KnotsToMps(150) + else + speed=UTILS.KnotsToMps(250) + end elseif step==AIRBOSS.PatternStep.DIRTYUP then - alt=UTILS.FeetToMeters(1200) + alt=UTILS.FeetToMeters(1200) - dist=UTILS.NMToMeters(12) - - speed=UTILS.KnotsToMps(250) + --speed=UTILS.KnotsToMps(250) elseif step==AIRBOSS.PatternStep.BULLSEYE then @@ -4965,8 +4980,10 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.WAKE then - if hornet or tomcat then + if hornet then alt=UTILS.FeetToMeters(370) + elseif tomcat then + alt=UTILS.FeetToMeters(430) -- Tomcat should be a bit higher as it intercepts the GS a bit higher. elseif skyhawk then alt=UTILS.FeetToMeters(370) --? end @@ -4976,8 +4993,10 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) elseif step==AIRBOSS.PatternStep.FINAL then - if hornet or tomcat then + if hornet then alt=UTILS.FeetToMeters(300) + elseif tomcat then + alt=UTILS.FeetToMeters(360) elseif skyhawk then alt=UTILS.FeetToMeters(300) --? elseif harrier then @@ -8274,7 +8293,11 @@ function AIRBOSS:_Initial(playerData) -- Hook down for students. if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then - hint=hint.." - Hook down!" + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + hint=hint.." - Hook down, SAS on, Wing Sweep 68°!" + else + hint=hint.." - Hook down!" + end end self:MessageToPlayer(playerData, hint, "MARSHAL") @@ -8423,7 +8446,7 @@ function AIRBOSS:_DirtyUp(playerData) self:_PlayerHint(playerData) -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. - if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then + if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then local callsay=self:_NewRadioCall(self.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard) local callfly=self:_NewRadioCall(self.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard) self:RadioTransmission(self.MarshalRadio, callsay, false, 50, nil, true) @@ -9432,7 +9455,9 @@ function AIRBOSS:_Trapped(playerData) local dcorr=100 if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then dcorr=100 + elseif playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then -- TODO: Check Tomcat. + dcorr=100 elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then -- A-4E gets slowed down much faster the the F/A-18C! dcorr=56 @@ -10179,7 +10204,7 @@ function AIRBOSS:_AttitudeMonitor(playerData) -- Output local text=string.format("Pattern step: %s", step) - text=text..string.format("\nAoA=%.1f° | |V|=%.1f knots", aoa, UTILS.MpsToKnots(vabs)) + text=text..string.format("\nAoA=%.1f° = %.1f Units | |V|=%.1f knots", aoa, self:_AoADeg2Units(playerData, 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) @@ -10208,7 +10233,7 @@ function AIRBOSS:_AttitudeMonitor(playerData) local dv=math.abs(vplayer-vcarrier) text=text..string.format("\nDist=%.1f m Alt=%.1f m delta|V|=%.1f km/h", dist, self:_GetAltCarrier(playerData.unit), dv) text=text..string.format("\nGamma=%.1f° | Rho=%.1f°", relhead, phi) - text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f", lue, gle, self:_AoADeg2Units(playerData, aoa)) + text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f Units", lue, gle, self:_AoADeg2Units(playerData, aoa)) local grade, points, analysis=self:_LSOgrade(playerData) text=text..string.format("\nTgroove=%.1f sec", self:_GetTimeInGroove(playerData)) text=text..string.format("\nGrade: %s %.1f PT - %s", grade, points, analysis) @@ -11459,6 +11484,7 @@ function AIRBOSS:_PlayerHint(playerData, delay, soundoff) if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then hint=hint.."\nFAF! Checks completed. Nozzles 50°." else + --TODO: Tomcat? hint=hint.."\nDirty up! Hook, gear and flaps down." end end @@ -11468,8 +11494,10 @@ function AIRBOSS:_PlayerHint(playerData, delay, soundoff) if playerData.step==AIRBOSS.PatternStep.BULLSEYE then -- Hint follow the needles. if playerData.difficulty==AIRBOSS.Difficulty.EASY then - if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then + if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then hint=hint..string.format("\nIntercept glideslope and follow the needles.") + else + hint=hint..string.format("\nIntercept glideslope.") end end end @@ -11507,7 +11535,7 @@ function AIRBOSS:_StepHint(playerData, step) -- AoA. if aoa then - hint=hint..string.format("\nAoA %.1f", aoa) + hint=hint..string.format("\nAoA %.1f", self:_AoADeg2Units(playerData, aoa)) end -- Speed. @@ -11520,9 +11548,19 @@ function AIRBOSS:_StepHint(playerData, step) hint=hint..string.format("\nDistance to the boat %.1f NM", UTILS.MetersToNM(dist)) end + -- Late break. + if step==AIRBOSS.PatternStep.LATEBREAK then + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + hint=hint.."\nWing Sweep 20°, Gear DOWN < 280 KIAS." + end + end + + -- Abeam. if step==AIRBOSS.PatternStep.ABEAM then if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then hint=hint.."\nNozzles 50°-60°. Antiskid OFF. Lights OFF." + elseif playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + hint=hint.."\nSlats/Flaps EXTENDED < 225 KIAS. DLC SELECTED. Auto Throttle IF DESIRED." else hint=hint.."\nDirty up! Gear DOWN, flaps DOWN. Check hook down." end diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 7388259af..a790deddd 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1923,6 +1923,47 @@ function GROUP:InAir() return nil end +--- Checks whether any unit (or optionally) all units of a group is(are) airbore or not. +-- @param Wrapper.Group#GROUP self +-- @param #boolean AllUnits (Optional) If true, check whether all units of the group are airborne. +-- @return #boolean True if at least one (optionally all) unit(s) is(are) airborne or false otherwise. Nil if no unit exists or is alive. +function GROUP:IsAirborne(AllUnits) + self:F2( self.GroupName ) + + -- Get all units of the group. + local units=self:GetUnits() + + if units then + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit then + + -- Unit in air or not. + local inair=unit:InAir() + + -- Unit is not in air and we wanted to know whether ALL units are ==> return false + if inair==false and AllUnits==true then + return false + end + + -- At least one unit is in are and we did not care which one. + if inair==true and not AllUnits then + return true + end + + end + -- At least one unit is in the air. + return true + end + end + + return nil +end + + + --- Returns the DCS descriptor table of the nth unit of the group. -- @param #GROUP self -- @param #number n (Optional) The number of the unit for which the dscriptor is returned. diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 4bbcaac95..91d157515 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -918,8 +918,7 @@ end --- Returns true if the UNIT is in the air. -- @param #UNIT self --- @return #boolean true if in the air. --- @return #nil The UNIT is not existing or alive. +-- @return #boolean Return true if in the air or #nil if the UNIT is not existing or alive. function UNIT:InAir() self:F2( self.UnitName ) From 0f9591464a67d9ff87e207d734033318b1649482 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 15 Mar 2019 09:57:50 +0100 Subject: [PATCH 200/485] First version --- Moose Development/Moose/Core/Zone.lua | 4 +- .../Moose/Functional/Detection.lua | 647 +++++++++++++++++- 2 files changed, 636 insertions(+), 15 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 2c00f48f5..70fb83ca3 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -631,8 +631,8 @@ end -- * @{ZONE_RADIUS.IsNoneInZone}(): Scan if the zone is empty. -- @{#ZONE_RADIUS. -- @param #ZONE_RADIUS self --- @param ObjectCategories --- @param UnitCategories +-- @param ObjectCategories An array of categories of the objects to find in the zone. +-- @param UnitCategories An array of unit categories of the objects to find in the zone. -- @usage -- self.Zone:Scan() -- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index c3eb65ccd..31f148f36 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -305,9 +305,9 @@ do -- DETECTION_BASE --- DETECTION constructor. -- @param #DETECTION_BASE self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. + -- @param Core.Set#SET_BASE DetectionSet The @{Set} that is used to detect the units. -- @return #DETECTION_BASE self - function DETECTION_BASE:New( DetectionSetGroup ) + function DETECTION_BASE:New( DetectionSet ) -- Inherits from BASE local self = BASE:Inherit( self, FSM:New() ) -- #DETECTION_BASE @@ -316,7 +316,7 @@ do -- DETECTION_BASE self.DetectedItemMax = 0 self.DetectedItems = {} - self.DetectionSetGroup = DetectionSetGroup + self.DetectionSet = DetectionSet self.RefreshTimeInterval = 30 @@ -398,7 +398,7 @@ do -- DETECTION_BASE -- @param #string To The To State string. self:AddTransition( "Detecting", "Detect", "Detecting" ) - self:AddTransition( "Detecting", "DetectionGroup", "Detecting" ) + self:AddTransition( "Detecting", "Detection", "Detecting" ) --- OnBefore Transition Handler for Event Detect. -- @function [parent=#DETECTION_BASE] OnBeforeDetect @@ -540,10 +540,11 @@ do -- DETECTION_BASE self.DetectedObjects[DetectionObjectName].Distance = 10000000 end - for DetectionGroupID, DetectionGroupData in pairs( self.DetectionSetGroup:GetSet() ) do + + for DetectionID, DetectionData in pairs( self.DetectionSet:GetSet() ) do --self:F( { DetectionGroupData } ) - self:F( { DetectionGroup = DetectionGroupData:GetName() } ) - self:__DetectionGroup( DetectDelay, DetectionGroupData, DetectionTimeStamp ) -- Process each detection asynchronously. + self:F( { DetectionGroup = DetectionData:GetName() } ) + self:__Detection( DetectDelay, DetectionData, DetectionTimeStamp ) -- Process each detection asynchronously. self.DetectionCount = self.DetectionCount + 1 DetectDelay = DetectDelay + 1 end @@ -555,7 +556,7 @@ do -- DETECTION_BASE -- @param #string To The To State string. -- @param Wrapper.Group#GROUP DetectionGroup The Group detecting. -- @param #number DetectionTimeStamp Time stamp of detection event. - function DETECTION_BASE:onafterDetectionGroup( From, Event, To, DetectionGroup, DetectionTimeStamp ) + function DETECTION_BASE:onafterDetection( From, Event, To, Detection, DetectionTimeStamp ) --self:F( { DetectedObjects = self.DetectedObjects } ) @@ -563,16 +564,16 @@ do -- DETECTION_BASE local HasDetectedObjects = false - if DetectionGroup:IsAlive() then + if Detection:IsAlive() then --self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) - local DetectionGroupName = DetectionGroup:GetName() - local DetectionUnit = DetectionGroup:GetUnit(1) + local DetectionGroupName = Detection:GetName() + local DetectionUnit = Detection:GetUnit(1) local DetectedUnits = {} - local DetectedTargets = DetectionGroup:GetDetectedTargets( + local DetectedTargets = Detection:GetDetectedTargets( self.DetectVisual, self.DetectOptical, self.DetectRadar, @@ -624,7 +625,7 @@ do -- DETECTION_BASE local DetectedObjectVec3 = DetectedObject:getPoint() local DetectedObjectVec2 = { x = DetectedObjectVec3.x, y = DetectedObjectVec3.z } - local DetectionGroupVec3 = DetectionGroup:GetVec3() + local DetectionGroupVec3 = Detection:GetVec3() local DetectionGroupVec2 = { x = DetectionGroupVec3.x, y = DetectionGroupVec3.z } local Distance = ( ( DetectedObjectVec3.x - DetectionGroupVec3.x )^2 + @@ -2819,3 +2820,623 @@ do -- DETECTION_AREAS end end + +do -- DETECTION_ZONES + + --- @type DETECTION_ZONES + -- @field DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. + -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. + -- @extends Functional.Detection#DETECTION_BASE + + --- Detect units within the battle zone for a list of @{Wrapper.Group}s detecting targets following (a) detection method(s), + -- and will build a list (table) of @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected. + -- The class is group the detected units within zones given a DetectedZoneRange parameter. + -- A set with multiple detected zones will be created as there are groups of units detected. + -- + -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones + -- + -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Functional.Detection#DECTECTION_BASE} and + -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Functional.Detection#DETECTION_ZONES}. + -- + -- Retrieve the DetectedItems[].Set with the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}(). A @{Core.Set#SET_UNIT} object will be returned. + -- + -- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZones}(). + -- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZoneCount}(). + -- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZone}() with a given index. + -- + -- ## 4.4) Flare or Smoke detected units + -- + -- Use the methods @{Functional.Detection#DETECTION_ZONES.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_ZONES.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. + -- + -- ## 4.5) Flare or Smoke or Bound detected zones + -- + -- Use the methods: + -- + -- * @{Functional.Detection#DETECTION_ZONES.FlareDetectedZones}() to flare in a color + -- * @{Functional.Detection#DETECTION_ZONES.SmokeDetectedZones}() to smoke in a color + -- * @{Functional.Detection#DETECTION_ZONES.SmokeDetectedZones}() to bound with a tire with a white flag + -- + -- the detected zones when a new detection has taken place. + -- + -- @field #DETECTION_ZONES + DETECTION_ZONES = { + ClassName = "DETECTION_ZONES", + DetectionZoneRange = nil, + } + + + --- DETECTION_ZONES constructor. + -- @param #DETECTION_ZONES self + -- @param Core.Set#SET_ZONE_RADIUS DetectionSetZoneRadius The @{Set} of ZONE_RADIUS. + -- @param DCS#Coalition.side DetectionCoalition The coalition of the detection. + -- @return #DETECTION_ZONES + function DETECTION_ZONES:New( DetectionSetZoneRadius, DetectionCoalition ) + + -- Inherits from DETECTION_BASE + local self = BASE:Inherit( self, DETECTION_BASE:New() ) + + self.DetectionSetZoneRadius = DetectionSetZoneRadius + self.DetectionCoalition = DetectionCoalition + + self._SmokeDetectedUnits = false + self._FlareDetectedUnits = false + self._SmokeDetectedZones = false + self._FlareDetectedZones = false + self._BoundDetectedZones = false + + return self + end + + + --- Report summary of a detected item using a given numeric index. + -- @param #DETECTION_ZONES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for. + -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use. + -- @return Core.Report#REPORT The report of the detection items. + function DETECTION_ZONES:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) + self:F( { DetectedItem = DetectedItem } ) + + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) + + if DetectedItem then + local DetectedSet = self:GetDetectedSet( DetectedItem ) + local ReportSummaryItem + + local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + local DetectedItemCoordinate = DetectedZone:GetCoordinate() + local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) + + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) + local DetectedItemsCount = DetectedSet:Count() + local DetectedItemsTypes = DetectedSet:GetTypeNames() + + local Report = REPORT:New() + Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) + Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) + Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) ) + Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) + + return Report + end + + return nil + end + + --- Report detailed of a detection result. + -- @param #DETECTION_ZONES self + -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. + -- @return #string + function DETECTION_ZONES:DetectedReportDetailed( AttackGroup ) --R2.1 Fixed missing report + self:F() + + local Report = REPORT:New() + for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do + local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem + local ReportSummary = self:DetectedItemReportSummary( DetectedItem, AttackGroup ) + Report:SetTitle( "Detected areas:" ) + Report:Add( ReportSummary:Text() ) + end + + local ReportText = Report:Text() + + return ReportText + end + + + --- Calculate the optimal intercept point of the DetectedItem. + -- @param #DETECTION_ZONES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + function DETECTION_ZONES:CalculateIntercept( DetectedItem ) + + local DetectedCoord = DetectedItem.Coordinate + local DetectedSpeed = DetectedCoord:GetVelocity() + local DetectedHeading = DetectedCoord:GetHeading() + + if self.Intercept then + local DetectedSet = DetectedItem.Set + -- todo: speed + + local TranslateDistance = DetectedSpeed * self.InterceptDelay + + local InterceptCoord = DetectedCoord:Translate( TranslateDistance, DetectedHeading ) + + DetectedItem.InterceptCoord = InterceptCoord + else + DetectedItem.InterceptCoord = DetectedCoord + end + + end + + + + --- Smoke the detected units + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:SmokeDetectedUnits() + self:F2() + + self._SmokeDetectedUnits = true + return self + end + + --- Flare the detected units + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:FlareDetectedUnits() + self:F2() + + self._FlareDetectedUnits = true + return self + end + + --- Smoke the detected zones + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:SmokeDetectedZones() + self:F2() + + self._SmokeDetectedZones = true + return self + end + + --- Flare the detected zones + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:FlareDetectedZones() + self:F2() + + self._FlareDetectedZones = true + return self + end + + --- Bound the detected zones + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:BoundDetectedZones() + self:F2() + + self._BoundDetectedZones = true + return self + end + + --- Make text documenting the changes of the detected zone. + -- @param #DETECTION_ZONES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @return #string The Changes text + function DETECTION_ZONES:GetChangeText( DetectedItem ) + self:F( DetectedItem ) + + local MT = {} + + for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do + + if ChangeCode == "AA" then + MT[#MT+1] = "Detected new area " .. ChangeData.ID .. ". The center target is a " .. ChangeData.ItemUnitType .. "." + end + + if ChangeCode == "RAU" then + MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". Removed the center target." + end + + if ChangeCode == "AAU" then + MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "." + end + + if ChangeCode == "RA" then + MT[#MT+1] = "Removed old area " .. ChangeData.ID .. ". No more targets in this area." + end + + if ChangeCode == "AU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Detected for area " .. ChangeData.ID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + if ChangeCode == "RU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Removed for area " .. ChangeData.ID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + end + + return table.concat( MT, "\n" ) + + end + + + --- Make a DetectionSet table. This function will be overridden in the derived clsses. + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:CreateDetectionItems() + + + self:F( "Checking Detected Items for new Detected Units ..." ) + --self:F( { DetectedObjects = self.DetectedObjects } ) + + -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. + -- Regroup when needed, split groups when needed. + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do + + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem + + if DetectedItem then + + self:T2( { "Detected Item ID: ", DetectedItemID } ) + + local Zone = DetectedItem.Zone -- Core.Zone#ZONE_RADIUS + + -- Scan the zone + Zone:Scan( { Object.Category.UNIT }, { Unit.Category.GROUND_UNIT } ) + + local ZoneUnits = Zone:GetScannedUnits() + for DCSUnitID, DCSUnit in pairs( ZoneUnits ) do + local UnitName = DCSUnit:getName() + local ZoneUnit = UNIT:FindByName( UnitName ) + local ZoneUnitCoalition = ZoneUnit:GetCoalition() + if ZoneUnitCoalition == self.DetectionCoalition then + DetectedItem.Set:AddUnit( ZoneUnit ) + end + end + end + end + + + -- Now all the tests should have been build, now make some smoke and flares... + -- We also report here the friendlies within the detected areas. + + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do + + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set + local DetectedFirstUnit = DetectedSet:GetFirst() + local DetectedZone = DetectedItem.Zone + + -- Set the last known coordinate to the detection item. + local DetectedZoneCoord = DetectedZone:GetCoordinate() + self:SetDetectedItemCoordinate( DetectedItem, DetectedZoneCoord, DetectedFirstUnit ) + + self:CalculateIntercept( DetectedItem ) + + -- We search for friendlies nearby. + -- If there weren't any friendlies nearby, and now there are friendlies nearby, we flag the area as "changed". + -- If there were friendlies nearby, and now there aren't any friendlies nearby, we flag the area as "changed". + -- This is for the A2G dispatcher to detect if there is a change in the tactical situation. + local OldFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table + local NewFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + if OldFriendliesNearbyGround ~= NewFriendliesNearbyGround then + DetectedItem.Changed = true + end + + self:SetDetectedItemThreatLevel( DetectedItem ) -- Calculate A2G threat level + self:NearestRecce( DetectedItem ) + + + if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedZone.ZoneUNIT:SmokeRed() + end + + --DetectedSet:Flush( self ) + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit ) + if DetectedUnit:IsAlive() then + --self:T( "Detected Set #" .. DetectedItem.ID .. ":" .. DetectedUnit:GetName() ) + if DETECTION_ZONES._FlareDetectedUnits or self._FlareDetectedUnits then + DetectedUnit:FlareGreen() + end + if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedUnit:SmokeGreen() + end + end + end + ) + if DETECTION_ZONES._FlareDetectedZones or self._FlareDetectedZones then + DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) ) + end + if DETECTION_ZONES._SmokeDetectedZones or self._SmokeDetectedZones then + DetectedZone:SmokeZone( SMOKECOLOR.White, 30 ) + end + + if DETECTION_ZONES._BoundDetectedZones or self._BoundDetectedZones then + self.CountryID = DetectedSet:GetFirst():GetCountry() + DetectedZone:BoundZone( 12, self.CountryID ) + end + end + + end + + --- @param #DETECTION_ZONES self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Detection The element on which the detection is based. + -- @param #number DetectionTimeStamp Time stamp of detection event. + function DETECTION_ZONES:onafterDetection( From, Event, To, Detection, DetectionTimeStamp ) + + --self:F( { DetectedObjects = self.DetectedObjects } ) + + self.DetectionRun = self.DetectionRun + 1 + + local HasDetectedObjects = false + + if Detection:IsAlive() then + + --self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) + + local DetectionGroupName = Detection:GetName() + local DetectionUnit = Detection:GetUnit(1) + + local DetectedUnits = {} + + local DetectedTargets = Detection:GetDetectedTargets( + self.DetectVisual, + self.DetectOptical, + self.DetectRadar, + self.DetectIRST, + self.DetectRWR, + self.DetectDLINK + ) + + self:F( { DetectedTargets = DetectedTargets } ) + + for DetectionObjectID, Detection in pairs( DetectedTargets ) do + local DetectedObject = Detection.object -- DCS#Object + + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then -- and ( DetectedObject:getCategory() == Object.Category.UNIT or DetectedObject:getCategory() == Object.Category.STATIC ) then + local DetectedObjectName = DetectedObject:getName() + if not self.DetectedObjects[DetectedObjectName] then + self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} + self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName + self.DetectedObjects[DetectedObjectName].Object = DetectedObject + end + end + end + + for DetectionObjectName, DetectedObjectData in pairs( self.DetectedObjects ) do + + local DetectedObject = DetectedObjectData.Object + + if DetectedObject:isExist() then + + local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity = DetectionUnit:IsTargetDetected( + DetectedObject, + self.DetectVisual, + self.DetectOptical, + self.DetectRadar, + self.DetectIRST, + self.DetectRWR, + self.DetectDLINK + ) + + --self:T2( { TargetIsDetected = TargetIsDetected, TargetIsVisible = TargetIsVisible, TargetLastTime = TargetLastTime, TargetKnowType = TargetKnowType, TargetKnowDistance = TargetKnowDistance, TargetLastPos = TargetLastPos, TargetLastVelocity = TargetLastVelocity } ) + + -- Only process if the target is visible. Detection also returns invisible units. + --if Detection.visible == true then + + local DetectionAccepted = true + + local DetectedObjectName = DetectedObject:getName() + local DetectedObjectType = DetectedObject:getTypeName() + + local DetectedObjectVec3 = DetectedObject:getPoint() + local DetectedObjectVec2 = { x = DetectedObjectVec3.x, y = DetectedObjectVec3.z } + local DetectionGroupVec3 = Detection:GetVec3() + local DetectionGroupVec2 = { x = DetectionGroupVec3.x, y = DetectionGroupVec3.z } + + local Distance = ( ( DetectedObjectVec3.x - DetectionGroupVec3.x )^2 + + ( DetectedObjectVec3.y - DetectionGroupVec3.y )^2 + + ( DetectedObjectVec3.z - DetectionGroupVec3.z )^2 + ) ^ 0.5 / 1000 + + local DetectedUnitCategory = DetectedObject:getDesc().category + + --self:F( { "Detected Target:", DetectionGroupName, DetectedObjectName, DetectedObjectType, Distance, DetectedUnitCategory } ) + + -- Calculate Acceptance + + DetectionAccepted = self._.FilterCategories[DetectedUnitCategory] ~= nil and DetectionAccepted or false + + -- if Distance > 15000 then + -- if DetectedUnitCategory == Unit.Category.GROUND_UNIT or DetectedUnitCategory == Unit.Category.SHIP then + -- if DetectedObject:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) == false then + -- DetectionAccepted = false + -- end + -- end + -- end + + if self.AcceptRange and Distance * 1000 > self.AcceptRange then + DetectionAccepted = false + end + + if self.AcceptZones then + local AnyZoneDetection = false + for AcceptZoneID, AcceptZone in pairs( self.AcceptZones ) do + local AcceptZone = AcceptZone -- Core.Zone#ZONE_BASE + if AcceptZone:IsVec2InZone( DetectedObjectVec2 ) then + AnyZoneDetection = true + end + end + if not AnyZoneDetection then + DetectionAccepted = false + end + end + + if self.RejectZones then + for RejectZoneID, RejectZone in pairs( self.RejectZones ) do + local RejectZone = RejectZone -- Core.Zone#ZONE_BASE + if RejectZone:IsPointVec2InZone( DetectedObjectVec2 ) == true then + DetectionAccepted = false + end + end + end + + -- Calculate additional probabilities + + if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.DistanceProbability then + local DistanceFactor = Distance / 4 + local DistanceProbabilityReversed = ( 1 - self.DistanceProbability ) * DistanceFactor + local DistanceProbability = 1 - DistanceProbabilityReversed + DistanceProbability = DistanceProbability * 30 / 300 + local Probability = math.random() -- Selects a number between 0 and 1 + --self:T( { Probability, DistanceProbability } ) + if Probability > DistanceProbability then + DetectionAccepted = false + end + end + + if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.AlphaAngleProbability then + local NormalVec2 = { x = DetectedObjectVec2.x - DetectionGroupVec2.x, y = DetectedObjectVec2.y - DetectionGroupVec2.y } + local AlphaAngle = math.atan2( NormalVec2.y, NormalVec2.x ) + local Sinus = math.sin( AlphaAngle ) + local AlphaAngleProbabilityReversed = ( 1 - self.AlphaAngleProbability ) * ( 1 - Sinus ) + local AlphaAngleProbability = 1 - AlphaAngleProbabilityReversed + + AlphaAngleProbability = AlphaAngleProbability * 30 / 300 + + local Probability = math.random() -- Selects a number between 0 and 1 + --self:T( { Probability, AlphaAngleProbability } ) + if Probability > AlphaAngleProbability then + DetectionAccepted = false + end + + end + + if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.ZoneProbability then + + for ZoneDataID, ZoneData in pairs( self.ZoneProbability ) do + self:F({ZoneData}) + local ZoneObject = ZoneData[1] -- Core.Zone#ZONE_BASE + local ZoneProbability = ZoneData[2] -- #number + ZoneProbability = ZoneProbability * 30 / 300 + + if ZoneObject:IsPointVec2InZone( DetectedObjectVec2 ) == true then + local Probability = math.random() -- Selects a number between 0 and 1 + --self:T( { Probability, ZoneProbability } ) + if Probability > ZoneProbability then + DetectionAccepted = false + break + end + end + end + end + + if DetectionAccepted then + + HasDetectedObjects = true + + self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} + self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName + + if TargetIsDetected and TargetIsDetected == true then + self.DetectedObjects[DetectedObjectName].IsDetected = TargetIsDetected + end + + if TargetIsDetected and TargetIsVisible and TargetIsVisible == true then + self.DetectedObjects[DetectedObjectName].IsVisible = TargetIsDetected and TargetIsVisible + end + + if TargetIsDetected and not self.DetectedObjects[DetectedObjectName].KnowType then + self.DetectedObjects[DetectedObjectName].KnowType = TargetIsDetected and TargetKnowType + end + self.DetectedObjects[DetectedObjectName].KnowDistance = TargetKnowDistance -- Detection.distance -- TargetKnowDistance + self.DetectedObjects[DetectedObjectName].LastTime = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastTime + self.DetectedObjects[DetectedObjectName].LastPos = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastPos + self.DetectedObjects[DetectedObjectName].LastVelocity = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastVelocity + + if not self.DetectedObjects[DetectedObjectName].Distance or ( Distance and self.DetectedObjects[DetectedObjectName].Distance > Distance ) then + self.DetectedObjects[DetectedObjectName].Distance = Distance + end + + self.DetectedObjects[DetectedObjectName].DetectionTimeStamp = DetectionTimeStamp + + self:F( { DetectedObject = self.DetectedObjects[DetectedObjectName] } ) + + local DetectedUnit = UNIT:FindByName( DetectedObjectName ) + + DetectedUnits[DetectedObjectName] = DetectedUnit + else + -- if beyond the DetectionRange then nullify... + self:F( { DetectedObject = "No more detection for " .. DetectedObjectName } ) + if self.DetectedObjects[DetectedObjectName] then + self.DetectedObjects[DetectedObjectName] = nil + end + end + + --self:T2( self.DetectedObjects ) + else + -- The previously detected object does not exist anymore, delete from the cache. + self:F( "Removing from DetectedObjects: " .. DetectionObjectName ) + self.DetectedObjects[DetectionObjectName] = nil + end + end + + if HasDetectedObjects then + self:__Detected( 0.1, DetectedUnits ) + end + + end + + if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then + + -- First check if all DetectedObjects were detected. + -- This is important. When there are DetectedObjects in the list, but were not detected, + -- And these remain undetected for more than 60 seconds, then these DetectedObjects will be flagged as not Detected. + -- IsDetected = false! + -- This is used in A2A_TASK_DISPATCHER to initiate fighter sweeping! The TASK_A2A_INTERCEPT tasks will be replaced with TASK_A2A_SWEEP tasks. + for DetectedObjectName, DetectedObject in pairs( self.DetectedObjects ) do + if self.DetectedObjects[DetectedObjectName].IsDetected == true and self.DetectedObjects[DetectedObjectName].DetectionTimeStamp + 300 <= DetectionTimeStamp then + self.DetectedObjects[DetectedObjectName].IsDetected = false + end + end + + self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method. + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:UpdateDetectedItemDetection( DetectedItem ) + self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list. + if DetectedItem then + self:__DetectedItem( 0.1, DetectedItem ) + end + end + + self:__Detect( self.RefreshTimeInterval ) + end + + end + + + end + + +end From 044b5cba7ca7ff8dfbf11d6a4306ec33896c3ef6 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 15 Mar 2019 11:01:06 +0100 Subject: [PATCH 201/485] Updates --- .../Moose/Functional/Detection.lua | 322 +++--------------- 1 file changed, 45 insertions(+), 277 deletions(-) diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 31f148f36..9367f4493 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1550,9 +1550,9 @@ do -- DETECTION_BASE -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. -- @param Core.Zone#ZONE_UNIT Zone (optional) The Zone to be added where the Units are located. -- @return #DETECTION_BASE.DetectedItem - function DETECTION_BASE:AddDetectedItemZone( DetectedItemKey, Set, Zone ) + function DETECTION_BASE:AddDetectedItemZone( ItemPrefix, DetectedItemKey, Set, Zone ) - local DetectedItem = self:AddDetectedItem( "AREA", DetectedItemKey, Set ) + local DetectedItem = self:AddDetectedItem( ItemPrefix, DetectedItemKey, Set ) DetectedItem.Zone = Zone @@ -2742,7 +2742,7 @@ do -- DETECTION_AREAS if AddedToDetectionArea == false then -- New detection area - local DetectedItem = self:AddDetectedItemZone( nil, + local DetectedItem = self:AddDetectedItemZone( "AREA", nil, SET_UNIT:New():FilterDeads():FilterCrashes(), ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) ) @@ -2867,15 +2867,15 @@ do -- DETECTION_ZONES --- DETECTION_ZONES constructor. -- @param #DETECTION_ZONES self - -- @param Core.Set#SET_ZONE_RADIUS DetectionSetZoneRadius The @{Set} of ZONE_RADIUS. + -- @param Core.Set#SET_ZONE_RADIUS DetectionSetZone The @{Set} of ZONE_RADIUS. -- @param DCS#Coalition.side DetectionCoalition The coalition of the detection. -- @return #DETECTION_ZONES - function DETECTION_ZONES:New( DetectionSetZoneRadius, DetectionCoalition ) + function DETECTION_ZONES:New( DetectionSetZone, DetectionCoalition ) -- Inherits from DETECTION_BASE local self = BASE:Inherit( self, DETECTION_BASE:New() ) - self.DetectionSetZoneRadius = DetectionSetZoneRadius + self.DetectionSetZone = DetectionSetZone self.DetectionCoalition = DetectionCoalition self._SmokeDetectedUnits = false @@ -3081,33 +3081,37 @@ do -- DETECTION_ZONES self:F( "Checking Detected Items for new Detected Units ..." ) - --self:F( { DetectedObjects = self.DetectedObjects } ) + + local DetectedUnits = SET_UNIT:New() -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. -- Regroup when needed, split groups when needed. - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do + for ZoneName, DetectionZone in pairs( self.DetectionSetZones:GetSet() ) do - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem + local DetectedItem = self:GetDetectedItemByKey( ZoneName ) + + if DetectedItem == nil then + DetectedItem = self:AddDetectedItemZone( "ZONE", ZoneName, nil, DetectionZone ) + end + + local DetectedItemSetUnit = self - if DetectedItem then + -- Scan the zone + DetectionZone:Scan( { Object.Category.UNIT }, { Unit.Category.GROUND_UNIT } ) - self:T2( { "Detected Item ID: ", DetectedItemID } ) - - local Zone = DetectedItem.Zone -- Core.Zone#ZONE_RADIUS - - -- Scan the zone - Zone:Scan( { Object.Category.UNIT }, { Unit.Category.GROUND_UNIT } ) - - local ZoneUnits = Zone:GetScannedUnits() - for DCSUnitID, DCSUnit in pairs( ZoneUnits ) do - local UnitName = DCSUnit:getName() - local ZoneUnit = UNIT:FindByName( UnitName ) - local ZoneUnitCoalition = ZoneUnit:GetCoalition() - if ZoneUnitCoalition == self.DetectionCoalition then - DetectedItem.Set:AddUnit( ZoneUnit ) + local ZoneUnits = DetectionZone:GetScannedUnits() + for DCSUnitID, DCSUnit in pairs( ZoneUnits ) do + local UnitName = DCSUnit:getName() + local ZoneUnit = UNIT:FindByName( UnitName ) + local ZoneUnitCoalition = ZoneUnit:GetCoalition() + if ZoneUnitCoalition == self.DetectionCoalition then + if DetectedUnits:FindUnit( UnitName ) ~= nil then + DetectedItemSetUnit:AddUnit( ZoneUnit ) + DetectedUnits:AddUnit( ZoneUnit ) end end end + end @@ -3177,266 +3181,30 @@ do -- DETECTION_ZONES end - --- @param #DETECTION_ZONES self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Detection The element on which the detection is based. - -- @param #number DetectionTimeStamp Time stamp of detection event. - function DETECTION_ZONES:onafterDetection( From, Event, To, Detection, DetectionTimeStamp ) - - --self:F( { DetectedObjects = self.DetectedObjects } ) - - self.DetectionRun = self.DetectionRun + 1 - - local HasDetectedObjects = false - - if Detection:IsAlive() then - - --self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) - - local DetectionGroupName = Detection:GetName() - local DetectionUnit = Detection:GetUnit(1) - - local DetectedUnits = {} - - local DetectedTargets = Detection:GetDetectedTargets( - self.DetectVisual, - self.DetectOptical, - self.DetectRadar, - self.DetectIRST, - self.DetectRWR, - self.DetectDLINK - ) - - self:F( { DetectedTargets = DetectedTargets } ) - - for DetectionObjectID, Detection in pairs( DetectedTargets ) do - local DetectedObject = Detection.object -- DCS#Object - - if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then -- and ( DetectedObject:getCategory() == Object.Category.UNIT or DetectedObject:getCategory() == Object.Category.STATIC ) then - local DetectedObjectName = DetectedObject:getName() - if not self.DetectedObjects[DetectedObjectName] then - self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} - self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName - self.DetectedObjects[DetectedObjectName].Object = DetectedObject - end - end - end - - for DetectionObjectName, DetectedObjectData in pairs( self.DetectedObjects ) do - - local DetectedObject = DetectedObjectData.Object - - if DetectedObject:isExist() then - - local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity = DetectionUnit:IsTargetDetected( - DetectedObject, - self.DetectVisual, - self.DetectOptical, - self.DetectRadar, - self.DetectIRST, - self.DetectRWR, - self.DetectDLINK - ) - - --self:T2( { TargetIsDetected = TargetIsDetected, TargetIsVisible = TargetIsVisible, TargetLastTime = TargetLastTime, TargetKnowType = TargetKnowType, TargetKnowDistance = TargetKnowDistance, TargetLastPos = TargetLastPos, TargetLastVelocity = TargetLastVelocity } ) - - -- Only process if the target is visible. Detection also returns invisible units. - --if Detection.visible == true then - - local DetectionAccepted = true - - local DetectedObjectName = DetectedObject:getName() - local DetectedObjectType = DetectedObject:getTypeName() - - local DetectedObjectVec3 = DetectedObject:getPoint() - local DetectedObjectVec2 = { x = DetectedObjectVec3.x, y = DetectedObjectVec3.z } - local DetectionGroupVec3 = Detection:GetVec3() - local DetectionGroupVec2 = { x = DetectionGroupVec3.x, y = DetectionGroupVec3.z } - - local Distance = ( ( DetectedObjectVec3.x - DetectionGroupVec3.x )^2 + - ( DetectedObjectVec3.y - DetectionGroupVec3.y )^2 + - ( DetectedObjectVec3.z - DetectionGroupVec3.z )^2 - ) ^ 0.5 / 1000 - - local DetectedUnitCategory = DetectedObject:getDesc().category - - --self:F( { "Detected Target:", DetectionGroupName, DetectedObjectName, DetectedObjectType, Distance, DetectedUnitCategory } ) - - -- Calculate Acceptance - - DetectionAccepted = self._.FilterCategories[DetectedUnitCategory] ~= nil and DetectionAccepted or false - - -- if Distance > 15000 then - -- if DetectedUnitCategory == Unit.Category.GROUND_UNIT or DetectedUnitCategory == Unit.Category.SHIP then - -- if DetectedObject:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) == false then - -- DetectionAccepted = false - -- end - -- end - -- end - - if self.AcceptRange and Distance * 1000 > self.AcceptRange then - DetectionAccepted = false - end - - if self.AcceptZones then - local AnyZoneDetection = false - for AcceptZoneID, AcceptZone in pairs( self.AcceptZones ) do - local AcceptZone = AcceptZone -- Core.Zone#ZONE_BASE - if AcceptZone:IsVec2InZone( DetectedObjectVec2 ) then - AnyZoneDetection = true - end - end - if not AnyZoneDetection then - DetectionAccepted = false - end - end - - if self.RejectZones then - for RejectZoneID, RejectZone in pairs( self.RejectZones ) do - local RejectZone = RejectZone -- Core.Zone#ZONE_BASE - if RejectZone:IsPointVec2InZone( DetectedObjectVec2 ) == true then - DetectionAccepted = false - end - end - end - - -- Calculate additional probabilities - - if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.DistanceProbability then - local DistanceFactor = Distance / 4 - local DistanceProbabilityReversed = ( 1 - self.DistanceProbability ) * DistanceFactor - local DistanceProbability = 1 - DistanceProbabilityReversed - DistanceProbability = DistanceProbability * 30 / 300 - local Probability = math.random() -- Selects a number between 0 and 1 - --self:T( { Probability, DistanceProbability } ) - if Probability > DistanceProbability then - DetectionAccepted = false - end - end - - if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.AlphaAngleProbability then - local NormalVec2 = { x = DetectedObjectVec2.x - DetectionGroupVec2.x, y = DetectedObjectVec2.y - DetectionGroupVec2.y } - local AlphaAngle = math.atan2( NormalVec2.y, NormalVec2.x ) - local Sinus = math.sin( AlphaAngle ) - local AlphaAngleProbabilityReversed = ( 1 - self.AlphaAngleProbability ) * ( 1 - Sinus ) - local AlphaAngleProbability = 1 - AlphaAngleProbabilityReversed - - AlphaAngleProbability = AlphaAngleProbability * 30 / 300 - - local Probability = math.random() -- Selects a number between 0 and 1 - --self:T( { Probability, AlphaAngleProbability } ) - if Probability > AlphaAngleProbability then - DetectionAccepted = false - end - - end - - if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.ZoneProbability then - - for ZoneDataID, ZoneData in pairs( self.ZoneProbability ) do - self:F({ZoneData}) - local ZoneObject = ZoneData[1] -- Core.Zone#ZONE_BASE - local ZoneProbability = ZoneData[2] -- #number - ZoneProbability = ZoneProbability * 30 / 300 - - if ZoneObject:IsPointVec2InZone( DetectedObjectVec2 ) == true then - local Probability = math.random() -- Selects a number between 0 and 1 - --self:T( { Probability, ZoneProbability } ) - if Probability > ZoneProbability then - DetectionAccepted = false - break - end - end - end - end - - if DetectionAccepted then - - HasDetectedObjects = true - - self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} - self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName - - if TargetIsDetected and TargetIsDetected == true then - self.DetectedObjects[DetectedObjectName].IsDetected = TargetIsDetected - end - - if TargetIsDetected and TargetIsVisible and TargetIsVisible == true then - self.DetectedObjects[DetectedObjectName].IsVisible = TargetIsDetected and TargetIsVisible - end - - if TargetIsDetected and not self.DetectedObjects[DetectedObjectName].KnowType then - self.DetectedObjects[DetectedObjectName].KnowType = TargetIsDetected and TargetKnowType - end - self.DetectedObjects[DetectedObjectName].KnowDistance = TargetKnowDistance -- Detection.distance -- TargetKnowDistance - self.DetectedObjects[DetectedObjectName].LastTime = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastTime - self.DetectedObjects[DetectedObjectName].LastPos = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastPos - self.DetectedObjects[DetectedObjectName].LastVelocity = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastVelocity - - if not self.DetectedObjects[DetectedObjectName].Distance or ( Distance and self.DetectedObjects[DetectedObjectName].Distance > Distance ) then - self.DetectedObjects[DetectedObjectName].Distance = Distance - end - - self.DetectedObjects[DetectedObjectName].DetectionTimeStamp = DetectionTimeStamp - - self:F( { DetectedObject = self.DetectedObjects[DetectedObjectName] } ) - - local DetectedUnit = UNIT:FindByName( DetectedObjectName ) - - DetectedUnits[DetectedObjectName] = DetectedUnit - else - -- if beyond the DetectionRange then nullify... - self:F( { DetectedObject = "No more detection for " .. DetectedObjectName } ) - if self.DetectedObjects[DetectedObjectName] then - self.DetectedObjects[DetectedObjectName] = nil - end - end - - --self:T2( self.DetectedObjects ) - else - -- The previously detected object does not exist anymore, delete from the cache. - self:F( "Removing from DetectedObjects: " .. DetectionObjectName ) - self.DetectedObjects[DetectionObjectName] = nil - end - end - - if HasDetectedObjects then - self:__Detected( 0.1, DetectedUnits ) - end - - end - - if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then - - -- First check if all DetectedObjects were detected. - -- This is important. When there are DetectedObjects in the list, but were not detected, - -- And these remain undetected for more than 60 seconds, then these DetectedObjects will be flagged as not Detected. - -- IsDetected = false! - -- This is used in A2A_TASK_DISPATCHER to initiate fighter sweeping! The TASK_A2A_INTERCEPT tasks will be replaced with TASK_A2A_SWEEP tasks. - for DetectedObjectName, DetectedObject in pairs( self.DetectedObjects ) do - if self.DetectedObjects[DetectedObjectName].IsDetected == true and self.DetectedObjects[DetectedObjectName].DetectionTimeStamp + 300 <= DetectionTimeStamp then - self.DetectedObjects[DetectedObjectName].IsDetected = false - end - end + --- @param #DETECTION_ZONES self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Detection The element on which the detection is based. + -- @param #number DetectionTimeStamp Time stamp of detection event. + function DETECTION_ZONES:onafterDetection( From, Event, To, Detection, DetectionTimeStamp ) - self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method. - for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do - self:UpdateDetectedItemDetection( DetectedItem ) - self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list. - if DetectedItem then - self:__DetectedItem( 0.1, DetectedItem ) - end + self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method. + + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:UpdateDetectedItemDetection( DetectedItem ) + self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list. + if DetectedItem then + self:__DetectedItem( 0.1, DetectedItem ) end - - self:__Detect( self.RefreshTimeInterval ) end + + self:__Detect( self.RefreshTimeInterval ) end - end + end From 3b520ab0c413b9fcf230caa8c502160b646e58db Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 15 Mar 2019 12:39:06 +0100 Subject: [PATCH 202/485] New detection method based on zones and scanning. --- .../Moose/AI/AI_A2A_Dispatcher.lua | 2 +- Moose Development/Moose/Core/Set.lua | 2 +- .../Moose/Functional/Designate.lua | 10 +- .../Moose/Functional/Detection.lua | 416 +----------------- .../Moose/Functional/DetectionZones.lua | 403 +++++++++++++++++ Moose Development/Moose/Functional/Escort.lua | 8 +- Moose Development/Moose/Modules.lua | 1 + 7 files changed, 431 insertions(+), 411 deletions(-) create mode 100644 Moose Development/Moose/Functional/DetectionZones.lua diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 9b9370bb3..7396cb7a6 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -1688,7 +1688,7 @@ do -- AI_A2A_DISPATCHER -- Add the CAP to the EWR network. - local RecceSet = self.Detection:GetDetectionSetGroup() + local RecceSet = self.Detection:GetDetectionSet() RecceSet:FilterPrefixes( DefenderSquadron.TemplatePrefixes ) RecceSet:FilterStart() diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index e4e2c3edf..eb210335a 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -148,7 +148,7 @@ do -- SET_BASE function SET_BASE:GetSet() self:F2() - return self.Set + return self.Set or {} end --- Gets a list of the Names of the Objects in the Set. diff --git a/Moose Development/Moose/Functional/Designate.lua b/Moose Development/Moose/Functional/Designate.lua index b8b1c24bb..7d6f09d26 100644 --- a/Moose Development/Moose/Functional/Designate.lua +++ b/Moose Development/Moose/Functional/Designate.lua @@ -469,7 +469,7 @@ do -- DESIGNATE self.CC = CC self.Detection = Detection self.AttackSet = AttackSet - self.RecceSet = Detection:GetDetectionSetGroup() + self.RecceSet = Detection:GetDetectionSet() self.Recces = {} self.Designating = {} self:SetDesignateName() @@ -1182,7 +1182,7 @@ do -- DESIGNATE local DetectedItem = self.Detection:GetDetectedItemByIndex( Index ) - local TargetSetUnit = self.Detection:GetDetectedSet( DetectedItem ) + local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem ) local MarkingCount = 0 local MarkedTypes = {} @@ -1352,7 +1352,7 @@ do -- DESIGNATE end local DetectedItem = self.Detection:GetDetectedItemByIndex( Index ) - local TargetSetUnit = self.Detection:GetDetectedSet( DetectedItem ) + local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem ) local Recces = self.Recces @@ -1377,7 +1377,7 @@ do -- DESIGNATE function DESIGNATE:onafterSmoke( From, Event, To, Index, Color ) local DetectedItem = self.Detection:GetDetectedItemByIndex( Index ) - local TargetSetUnit = self.Detection:GetDetectedSet( DetectedItem ) + local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem ) local TargetSetUnitCount = TargetSetUnit:Count() local MarkedCount = 0 @@ -1422,7 +1422,7 @@ do -- DESIGNATE function DESIGNATE:onafterIlluminate( From, Event, To, Index ) local DetectedItem = self.Detection:GetDetectedItemByIndex( Index ) - local TargetSetUnit = self.Detection:GetDetectedSet( DetectedItem ) + local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem ) local TargetUnit = TargetSetUnit:GetFirst() if TargetUnit then diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 9367f4493..d44edf334 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -541,11 +541,11 @@ do -- DETECTION_BASE end + self.DetectionCount = self.DetectionSet:Count() for DetectionID, DetectionData in pairs( self.DetectionSet:GetSet() ) do --self:F( { DetectionGroupData } ) self:F( { DetectionGroup = DetectionData:GetName() } ) self:__Detection( DetectDelay, DetectionData, DetectionTimeStamp ) -- Process each detection asynchronously. - self.DetectionCount = self.DetectionCount + 1 DetectDelay = DetectDelay + 1 end end @@ -839,7 +839,7 @@ do -- DETECTION_BASE local DetectedItems = self:GetDetectedItems() for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do - local DetectedSet = self:GetDetectedSet( DetectedItem ) + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) if DetectedSet then DetectedSet:RemoveUnitsByName( UnitName ) end @@ -1552,6 +1552,8 @@ do -- DETECTION_BASE -- @return #DETECTION_BASE.DetectedItem function DETECTION_BASE:AddDetectedItemZone( ItemPrefix, DetectedItemKey, Set, Zone ) + self:F( { ItemPrefix, DetectedItemKey, Set, Zone } ) + local DetectedItem = self:AddDetectedItem( ItemPrefix, DetectedItemKey, Set ) DetectedItem.Zone = Zone @@ -1662,7 +1664,7 @@ do -- DETECTION_BASE -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return Core.Set#SET_UNIT DetectedSet - function DETECTION_BASE:GetDetectedSet( DetectedItem ) + function DETECTION_BASE:GetDetectedItemSet( DetectedItem ) local DetectedSetUnit = DetectedItem and DetectedItem.Set if DetectedSetUnit then @@ -1811,13 +1813,13 @@ do -- DETECTION_BASE return nil end - --- Get the detection Groups. + --- Get the Detection Set. -- @param #DETECTION_BASE self - -- @return Core.Set#SET_GROUP - function DETECTION_BASE:GetDetectionSetGroup() + -- @return Core.Set#SET_BASE + function DETECTION_BASE:GetDetectionSet() - local DetectionSetGroup = self.DetectionSetGroup - return DetectionSetGroup + local DetectionSet = self.DetectionSet + return DetectionSet end --- Find the nearest Recce of the DetectedItem. @@ -1829,7 +1831,7 @@ do -- DETECTION_BASE local NearestRecce = nil local DistanceRecce = 1000000000 -- Units are not further than 1000000 km away from an area :-) - for RecceGroupName, RecceGroup in pairs( self.DetectionSetGroup:GetSet() ) do + for RecceGroupName, RecceGroup in pairs( self.DetectionSet:GetSet() ) do if RecceGroup and RecceGroup:IsAlive() then for RecceUnit, RecceUnit in pairs( RecceGroup:GetUnits() ) do if RecceUnit:IsActive() then @@ -2034,7 +2036,7 @@ do -- DETECTION_UNITS local DetectedFirstUnitCoord = DetectedFirstUnit:GetCoordinate() self:SetDetectedItemCoordinate( DetectedItem, DetectedFirstUnitCoord, DetectedFirstUnit ) - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table self:SetDetectedItemThreatLevel( DetectedItem ) self:NearestRecce( DetectedItem ) @@ -2269,7 +2271,7 @@ do -- DETECTION_TYPES local DetectedUnitCoord = DetectedFirstUnit:GetCoordinate() self:SetDetectedItemCoordinate( DetectedItem, DetectedUnitCoord, DetectedFirstUnit ) - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table self:SetDetectedItemThreatLevel( DetectedItem ) self:NearestRecce( DetectedItem ) end @@ -2287,7 +2289,7 @@ do -- DETECTION_TYPES function DETECTION_TYPES:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) self:F( { DetectedItem = DetectedItem } ) - local DetectedSet = self:GetDetectedSet( DetectedItem ) + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) local DetectedItemID = self:GetDetectedItemID( DetectedItem ) self:T( DetectedItem ) @@ -2409,7 +2411,7 @@ do -- DETECTION_AREAS local DetectedItemID = self:GetDetectedItemID( DetectedItem ) if DetectedItem then - local DetectedSet = self:GetDetectedSet( DetectedItem ) + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) local ReportSummaryItem local DetectedZone = self:GetDetectedItemZone( DetectedItem ) @@ -2774,7 +2776,7 @@ do -- DETECTION_AREAS -- If there were friendlies nearby, and now there aren't any friendlies nearby, we flag the area as "changed". -- This is for the A2G dispatcher to detect if there is a change in the tactical situation. local OldFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table local NewFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) if OldFriendliesNearbyGround ~= NewFriendliesNearbyGround then DetectedItem.Changed = true @@ -2821,390 +2823,4 @@ do -- DETECTION_AREAS end -do -- DETECTION_ZONES - - --- @type DETECTION_ZONES - -- @field DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. - -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. - -- @extends Functional.Detection#DETECTION_BASE - - --- Detect units within the battle zone for a list of @{Wrapper.Group}s detecting targets following (a) detection method(s), - -- and will build a list (table) of @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected. - -- The class is group the detected units within zones given a DetectedZoneRange parameter. - -- A set with multiple detected zones will be created as there are groups of units detected. - -- - -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones - -- - -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Functional.Detection#DECTECTION_BASE} and - -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Functional.Detection#DETECTION_ZONES}. - -- - -- Retrieve the DetectedItems[].Set with the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}(). A @{Core.Set#SET_UNIT} object will be returned. - -- - -- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZones}(). - -- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZoneCount}(). - -- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZone}() with a given index. - -- - -- ## 4.4) Flare or Smoke detected units - -- - -- Use the methods @{Functional.Detection#DETECTION_ZONES.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_ZONES.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. - -- - -- ## 4.5) Flare or Smoke or Bound detected zones - -- - -- Use the methods: - -- - -- * @{Functional.Detection#DETECTION_ZONES.FlareDetectedZones}() to flare in a color - -- * @{Functional.Detection#DETECTION_ZONES.SmokeDetectedZones}() to smoke in a color - -- * @{Functional.Detection#DETECTION_ZONES.SmokeDetectedZones}() to bound with a tire with a white flag - -- - -- the detected zones when a new detection has taken place. - -- - -- @field #DETECTION_ZONES - DETECTION_ZONES = { - ClassName = "DETECTION_ZONES", - DetectionZoneRange = nil, - } - - - --- DETECTION_ZONES constructor. - -- @param #DETECTION_ZONES self - -- @param Core.Set#SET_ZONE_RADIUS DetectionSetZone The @{Set} of ZONE_RADIUS. - -- @param DCS#Coalition.side DetectionCoalition The coalition of the detection. - -- @return #DETECTION_ZONES - function DETECTION_ZONES:New( DetectionSetZone, DetectionCoalition ) - - -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New() ) - - self.DetectionSetZone = DetectionSetZone - self.DetectionCoalition = DetectionCoalition - - self._SmokeDetectedUnits = false - self._FlareDetectedUnits = false - self._SmokeDetectedZones = false - self._FlareDetectedZones = false - self._BoundDetectedZones = false - - return self - end - - - --- Report summary of a detected item using a given numeric index. - -- @param #DETECTION_ZONES self - -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. - -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for. - -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use. - -- @return Core.Report#REPORT The report of the detection items. - function DETECTION_ZONES:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) - self:F( { DetectedItem = DetectedItem } ) - - local DetectedItemID = self:GetDetectedItemID( DetectedItem ) - - if DetectedItem then - local DetectedSet = self:GetDetectedSet( DetectedItem ) - local ReportSummaryItem - - local DetectedZone = self:GetDetectedItemZone( DetectedItem ) - local DetectedItemCoordinate = DetectedZone:GetCoordinate() - local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) - - local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) - local DetectedItemsCount = DetectedSet:Count() - local DetectedItemsTypes = DetectedSet:GetTypeNames() - - local Report = REPORT:New() - Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) - Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) - Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) ) - Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) - - return Report - end - - return nil - end - - --- Report detailed of a detection result. - -- @param #DETECTION_ZONES self - -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. - -- @return #string - function DETECTION_ZONES:DetectedReportDetailed( AttackGroup ) --R2.1 Fixed missing report - self:F() - - local Report = REPORT:New() - for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do - local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem - local ReportSummary = self:DetectedItemReportSummary( DetectedItem, AttackGroup ) - Report:SetTitle( "Detected areas:" ) - Report:Add( ReportSummary:Text() ) - end - - local ReportText = Report:Text() - - return ReportText - end - - - --- Calculate the optimal intercept point of the DetectedItem. - -- @param #DETECTION_ZONES self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - function DETECTION_ZONES:CalculateIntercept( DetectedItem ) - - local DetectedCoord = DetectedItem.Coordinate - local DetectedSpeed = DetectedCoord:GetVelocity() - local DetectedHeading = DetectedCoord:GetHeading() - - if self.Intercept then - local DetectedSet = DetectedItem.Set - -- todo: speed - - local TranslateDistance = DetectedSpeed * self.InterceptDelay - - local InterceptCoord = DetectedCoord:Translate( TranslateDistance, DetectedHeading ) - - DetectedItem.InterceptCoord = InterceptCoord - else - DetectedItem.InterceptCoord = DetectedCoord - end - - end - - - - --- Smoke the detected units - -- @param #DETECTION_ZONES self - -- @return #DETECTION_ZONES self - function DETECTION_ZONES:SmokeDetectedUnits() - self:F2() - - self._SmokeDetectedUnits = true - return self - end - - --- Flare the detected units - -- @param #DETECTION_ZONES self - -- @return #DETECTION_ZONES self - function DETECTION_ZONES:FlareDetectedUnits() - self:F2() - - self._FlareDetectedUnits = true - return self - end - - --- Smoke the detected zones - -- @param #DETECTION_ZONES self - -- @return #DETECTION_ZONES self - function DETECTION_ZONES:SmokeDetectedZones() - self:F2() - - self._SmokeDetectedZones = true - return self - end - - --- Flare the detected zones - -- @param #DETECTION_ZONES self - -- @return #DETECTION_ZONES self - function DETECTION_ZONES:FlareDetectedZones() - self:F2() - - self._FlareDetectedZones = true - return self - end - - --- Bound the detected zones - -- @param #DETECTION_ZONES self - -- @return #DETECTION_ZONES self - function DETECTION_ZONES:BoundDetectedZones() - self:F2() - - self._BoundDetectedZones = true - return self - end - - --- Make text documenting the changes of the detected zone. - -- @param #DETECTION_ZONES self - -- @param #DETECTION_BASE.DetectedItem DetectedItem - -- @return #string The Changes text - function DETECTION_ZONES:GetChangeText( DetectedItem ) - self:F( DetectedItem ) - - local MT = {} - - for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do - - if ChangeCode == "AA" then - MT[#MT+1] = "Detected new area " .. ChangeData.ID .. ". The center target is a " .. ChangeData.ItemUnitType .. "." - end - - if ChangeCode == "RAU" then - MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". Removed the center target." - end - - if ChangeCode == "AAU" then - MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "." - end - - if ChangeCode == "RA" then - MT[#MT+1] = "Removed old area " .. ChangeData.ID .. ". No more targets in this area." - end - - if ChangeCode == "AU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = "Detected for area " .. ChangeData.ID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." - end - - if ChangeCode == "RU" then - local MTUT = {} - for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do - if ChangeUnitType ~= "ID" then - MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType - end - end - MT[#MT+1] = "Removed for area " .. ChangeData.ID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." - end - - end - - return table.concat( MT, "\n" ) - - end - - - --- Make a DetectionSet table. This function will be overridden in the derived clsses. - -- @param #DETECTION_ZONES self - -- @return #DETECTION_ZONES self - function DETECTION_ZONES:CreateDetectionItems() - - - self:F( "Checking Detected Items for new Detected Units ..." ) - - local DetectedUnits = SET_UNIT:New() - - -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. - -- Regroup when needed, split groups when needed. - for ZoneName, DetectionZone in pairs( self.DetectionSetZones:GetSet() ) do - - local DetectedItem = self:GetDetectedItemByKey( ZoneName ) - - if DetectedItem == nil then - DetectedItem = self:AddDetectedItemZone( "ZONE", ZoneName, nil, DetectionZone ) - end - - local DetectedItemSetUnit = self - - -- Scan the zone - DetectionZone:Scan( { Object.Category.UNIT }, { Unit.Category.GROUND_UNIT } ) - - local ZoneUnits = DetectionZone:GetScannedUnits() - for DCSUnitID, DCSUnit in pairs( ZoneUnits ) do - local UnitName = DCSUnit:getName() - local ZoneUnit = UNIT:FindByName( UnitName ) - local ZoneUnitCoalition = ZoneUnit:GetCoalition() - if ZoneUnitCoalition == self.DetectionCoalition then - if DetectedUnits:FindUnit( UnitName ) ~= nil then - DetectedItemSetUnit:AddUnit( ZoneUnit ) - DetectedUnits:AddUnit( ZoneUnit ) - end - end - end - - end - - - -- Now all the tests should have been build, now make some smoke and flares... - -- We also report here the friendlies within the detected areas. - - for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do - - local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem - local DetectedSet = DetectedItem.Set - local DetectedFirstUnit = DetectedSet:GetFirst() - local DetectedZone = DetectedItem.Zone - -- Set the last known coordinate to the detection item. - local DetectedZoneCoord = DetectedZone:GetCoordinate() - self:SetDetectedItemCoordinate( DetectedItem, DetectedZoneCoord, DetectedFirstUnit ) - - self:CalculateIntercept( DetectedItem ) - - -- We search for friendlies nearby. - -- If there weren't any friendlies nearby, and now there are friendlies nearby, we flag the area as "changed". - -- If there were friendlies nearby, and now there aren't any friendlies nearby, we flag the area as "changed". - -- This is for the A2G dispatcher to detect if there is a change in the tactical situation. - local OldFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table - local NewFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) - if OldFriendliesNearbyGround ~= NewFriendliesNearbyGround then - DetectedItem.Changed = true - end - - self:SetDetectedItemThreatLevel( DetectedItem ) -- Calculate A2G threat level - self:NearestRecce( DetectedItem ) - - - if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then - DetectedZone.ZoneUNIT:SmokeRed() - end - - --DetectedSet:Flush( self ) - - DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit - function( DetectedUnit ) - if DetectedUnit:IsAlive() then - --self:T( "Detected Set #" .. DetectedItem.ID .. ":" .. DetectedUnit:GetName() ) - if DETECTION_ZONES._FlareDetectedUnits or self._FlareDetectedUnits then - DetectedUnit:FlareGreen() - end - if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then - DetectedUnit:SmokeGreen() - end - end - end - ) - if DETECTION_ZONES._FlareDetectedZones or self._FlareDetectedZones then - DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) ) - end - if DETECTION_ZONES._SmokeDetectedZones or self._SmokeDetectedZones then - DetectedZone:SmokeZone( SMOKECOLOR.White, 30 ) - end - - if DETECTION_ZONES._BoundDetectedZones or self._BoundDetectedZones then - self.CountryID = DetectedSet:GetFirst():GetCountry() - DetectedZone:BoundZone( 12, self.CountryID ) - end - end - - end - - --- @param #DETECTION_ZONES self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Detection The element on which the detection is based. - -- @param #number DetectionTimeStamp Time stamp of detection event. - function DETECTION_ZONES:onafterDetection( From, Event, To, Detection, DetectionTimeStamp ) - - self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method. - - for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do - self:UpdateDetectedItemDetection( DetectedItem ) - self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list. - if DetectedItem then - self:__DetectedItem( 0.1, DetectedItem ) - end - end - - self:__Detect( self.RefreshTimeInterval ) - - end - - - - - -end diff --git a/Moose Development/Moose/Functional/DetectionZones.lua b/Moose Development/Moose/Functional/DetectionZones.lua new file mode 100644 index 000000000..614dce0ec --- /dev/null +++ b/Moose Development/Moose/Functional/DetectionZones.lua @@ -0,0 +1,403 @@ +do -- DETECTION_ZONES + + --- @type DETECTION_ZONES + -- @field DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. + -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. + -- @extends Functional.Detection#DETECTION_BASE + + --- (old, to be revised ) Detect units within the battle zone for a list of @{Core.Zone}s detecting targets following (a) detection method(s), + -- and will build a list (table) of @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected. + -- The class is group the detected units within zones given a DetectedZoneRange parameter. + -- A set with multiple detected zones will be created as there are groups of units detected. + -- + -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones + -- + -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Functional.Detection#DECTECTION_BASE} and + -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Functional.Detection#DETECTION_ZONES}. + -- + -- Retrieve the DetectedItems[].Set with the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}(). A @{Core.Set#SET_UNIT} object will be returned. + -- + -- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZones}(). + -- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZoneCount}(). + -- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZone}() with a given index. + -- + -- ## 4.4) Flare or Smoke detected units + -- + -- Use the methods @{Functional.Detection#DETECTION_ZONES.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_ZONES.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. + -- + -- ## 4.5) Flare or Smoke or Bound detected zones + -- + -- Use the methods: + -- + -- * @{Functional.Detection#DETECTION_ZONES.FlareDetectedZones}() to flare in a color + -- * @{Functional.Detection#DETECTION_ZONES.SmokeDetectedZones}() to smoke in a color + -- * @{Functional.Detection#DETECTION_ZONES.SmokeDetectedZones}() to bound with a tire with a white flag + -- + -- the detected zones when a new detection has taken place. + -- + -- @field #DETECTION_ZONES + DETECTION_ZONES = { + ClassName = "DETECTION_ZONES", + DetectionZoneRange = nil, + } + + + --- DETECTION_ZONES constructor. + -- @param #DETECTION_ZONES self + -- @param Core.Set#SET_ZONE_RADIUS DetectionSetZone The @{Set} of ZONE_RADIUS. + -- @param DCS#Coalition.side DetectionCoalition The coalition of the detection. + -- @return #DETECTION_ZONES + function DETECTION_ZONES:New( DetectionSetZone, DetectionCoalition ) + + -- Inherits from DETECTION_BASE + local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetZone ) ) + + self.DetectionSetZone = DetectionSetZone + self.DetectionCoalition = DetectionCoalition + + self._SmokeDetectedUnits = false + self._FlareDetectedUnits = false + self._SmokeDetectedZones = false + self._FlareDetectedZones = false + self._BoundDetectedZones = false + + return self + end + + + --- Report summary of a detected item using a given numeric index. + -- @param #DETECTION_ZONES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for. + -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use. + -- @return Core.Report#REPORT The report of the detection items. + function DETECTION_ZONES:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) + self:F( { DetectedItem = DetectedItem } ) + + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) + + if DetectedItem then + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) + local ReportSummaryItem + + local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + local DetectedItemCoordinate = DetectedZone:GetCoordinate() + local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) + + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) + local DetectedItemsCount = DetectedSet:Count() + local DetectedItemsTypes = DetectedSet:GetTypeNames() + + local Report = REPORT:New() + Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) + Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) + Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) ) + Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) + + return Report + end + + return nil + end + + --- Report detailed of a detection result. + -- @param #DETECTION_ZONES self + -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. + -- @return #string + function DETECTION_ZONES:DetectedReportDetailed( AttackGroup ) --R2.1 Fixed missing report + self:F() + + local Report = REPORT:New() + for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do + local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem + local ReportSummary = self:DetectedItemReportSummary( DetectedItem, AttackGroup ) + Report:SetTitle( "Detected areas:" ) + Report:Add( ReportSummary:Text() ) + end + + local ReportText = Report:Text() + + return ReportText + end + + + --- Calculate the optimal intercept point of the DetectedItem. + -- @param #DETECTION_ZONES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + function DETECTION_ZONES:CalculateIntercept( DetectedItem ) + + local DetectedCoord = DetectedItem.Coordinate + local DetectedSpeed = DetectedCoord:GetVelocity() + local DetectedHeading = DetectedCoord:GetHeading() + + if self.Intercept then + local DetectedSet = DetectedItem.Set + -- todo: speed + + local TranslateDistance = DetectedSpeed * self.InterceptDelay + + local InterceptCoord = DetectedCoord:Translate( TranslateDistance, DetectedHeading ) + + DetectedItem.InterceptCoord = InterceptCoord + else + DetectedItem.InterceptCoord = DetectedCoord + end + + end + + + + --- Smoke the detected units + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:SmokeDetectedUnits() + self:F2() + + self._SmokeDetectedUnits = true + return self + end + + --- Flare the detected units + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:FlareDetectedUnits() + self:F2() + + self._FlareDetectedUnits = true + return self + end + + --- Smoke the detected zones + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:SmokeDetectedZones() + self:F2() + + self._SmokeDetectedZones = true + return self + end + + --- Flare the detected zones + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:FlareDetectedZones() + self:F2() + + self._FlareDetectedZones = true + return self + end + + --- Bound the detected zones + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:BoundDetectedZones() + self:F2() + + self._BoundDetectedZones = true + return self + end + + --- Make text documenting the changes of the detected zone. + -- @param #DETECTION_ZONES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @return #string The Changes text + function DETECTION_ZONES:GetChangeText( DetectedItem ) + self:F( DetectedItem ) + + local MT = {} + + for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do + + if ChangeCode == "AA" then + MT[#MT+1] = "Detected new area " .. ChangeData.ID .. ". The center target is a " .. ChangeData.ItemUnitType .. "." + end + + if ChangeCode == "RAU" then + MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". Removed the center target." + end + + if ChangeCode == "AAU" then + MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "." + end + + if ChangeCode == "RA" then + MT[#MT+1] = "Removed old area " .. ChangeData.ID .. ". No more targets in this area." + end + + if ChangeCode == "AU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Detected for area " .. ChangeData.ID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + if ChangeCode == "RU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Removed for area " .. ChangeData.ID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + end + + return table.concat( MT, "\n" ) + + end + + + --- Make a DetectionSet table. This function will be overridden in the derived clsses. + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:CreateDetectionItems() + + + self:F( "Checking Detected Items for new Detected Units ..." ) + + local DetectedUnits = SET_UNIT:New() + + -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. + -- Regroup when needed, split groups when needed. + for ZoneName, DetectionZone in pairs( self.DetectionSetZone:GetSet() ) do + + local DetectedItem = self:GetDetectedItemByKey( ZoneName ) + + if DetectedItem == nil then + DetectedItem = self:AddDetectedItemZone( "ZONE", ZoneName, nil, DetectionZone ) + end + + local DetectedItemSetUnit = self:GetDetectedItemSet( DetectedItem ) + + -- Scan the zone + DetectionZone:Scan( { Object.Category.UNIT }, { Unit.Category.GROUND_UNIT } ) + + local ZoneUnits = DetectionZone:GetScannedUnits() + for DCSUnitID, DCSUnit in pairs( ZoneUnits ) do + local UnitName = DCSUnit:getName() + local ZoneUnit = UNIT:FindByName( UnitName ) + local ZoneUnitCoalition = ZoneUnit:GetCoalition() + if ZoneUnitCoalition == self.DetectionCoalition then + if DetectedItemSetUnit:FindUnit( UnitName ) == nil and DetectedUnits:FindUnit( UnitName ) == nil then + self:F( "Adding " .. UnitName ) + DetectedItemSetUnit:AddUnit( ZoneUnit ) + DetectedUnits:AddUnit( ZoneUnit ) + end + end + end + + end + + + + + -- Now all the tests should have been build, now make some smoke and flares... + -- We also report here the friendlies within the detected areas. + + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do + + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) + local DetectedFirstUnit = DetectedSet:GetFirst() + local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + + -- Set the last known coordinate to the detection item. + local DetectedZoneCoord = DetectedZone:GetCoordinate() + self:SetDetectedItemCoordinate( DetectedItem, DetectedZoneCoord, DetectedFirstUnit ) + + self:CalculateIntercept( DetectedItem ) + + -- We search for friendlies nearby. + -- If there weren't any friendlies nearby, and now there are friendlies nearby, we flag the area as "changed". + -- If there were friendlies nearby, and now there aren't any friendlies nearby, we flag the area as "changed". + -- This is for the A2G dispatcher to detect if there is a change in the tactical situation. + local OldFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table + local NewFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + if OldFriendliesNearbyGround ~= NewFriendliesNearbyGround then + DetectedItem.Changed = true + end + + self:SetDetectedItemThreatLevel( DetectedItem ) -- Calculate A2G threat level + --self:NearestRecce( DetectedItem ) + + + if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedZone:SmokeZone( SMOKECOLOR.Red, 30 ) + end + + --DetectedSet:Flush( self ) + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit ) + if DetectedUnit:IsAlive() then + --self:T( "Detected Set #" .. DetectedItem.ID .. ":" .. DetectedUnit:GetName() ) + if DETECTION_ZONES._FlareDetectedUnits or self._FlareDetectedUnits then + DetectedUnit:FlareGreen() + end + if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedUnit:SmokeGreen() + end + end + end + ) + if DETECTION_ZONES._FlareDetectedZones or self._FlareDetectedZones then + DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) ) + end + if DETECTION_ZONES._SmokeDetectedZones or self._SmokeDetectedZones then + DetectedZone:SmokeZone( SMOKECOLOR.White, 30 ) + end + + if DETECTION_ZONES._BoundDetectedZones or self._BoundDetectedZones then + self.CountryID = DetectedSet:GetFirst():GetCountry() + DetectedZone:BoundZone( 12, self.CountryID ) + end + end + + end + + --- @param #DETECTION_ZONES self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Detection The element on which the detection is based. + -- @param #number DetectionTimeStamp Time stamp of detection event. + function DETECTION_ZONES:onafterDetection( From, Event, To, Detection, DetectionTimeStamp ) + + self.DetectionRun = self.DetectionRun + 1 + if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then + self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method. + + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:UpdateDetectedItemDetection( DetectedItem ) + self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list. + if DetectedItem then + self:__DetectedItem( 0.1, DetectedItem ) + end + end + self:__Detect( self.RefreshTimeInterval ) + end + end + + + --- Set IsDetected flag for the DetectedItem, which can have more units. + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES.DetectedItem DetectedItem + -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet. + function DETECTION_ZONES:UpdateDetectedItemDetection( DetectedItem ) + + local IsDetected = true + + DetectedItem.IsDetected = true + + return IsDetected + end + + + +end \ No newline at end of file diff --git a/Moose Development/Moose/Functional/Escort.lua b/Moose Development/Moose/Functional/Escort.lua index 89d77425c..43686ba06 100644 --- a/Moose Development/Moose/Functional/Escort.lua +++ b/Moose Development/Moose/Functional/Escort.lua @@ -872,7 +872,7 @@ function ESCORT:_AttackTarget( DetectedItem ) EscortGroup:OptionROTPassiveDefense() EscortGroup:SetState( EscortGroup, "Escort", self ) - local DetectedSet = self.Detection:GetDetectedSet( DetectedItem ) + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) local Tasks = {} @@ -895,7 +895,7 @@ function ESCORT:_AttackTarget( DetectedItem ) else - local DetectedSet = self.Detection:GetDetectedSet( DetectedItem ) + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) local Tasks = {} @@ -934,7 +934,7 @@ function ESCORT:_AssistTarget( EscortGroupAttack, DetectedItem ) EscortGroupAttack:OptionROEOpenFire() EscortGroupAttack:OptionROTVertical() - local DetectedSet = self.Detection:GetDetectedSet( DetectedItem ) + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) local Tasks = {} @@ -956,7 +956,7 @@ function ESCORT:_AssistTarget( EscortGroupAttack, DetectedItem ) ) else - local DetectedSet = self.Detection:GetDetectedSet( DetectedItem ) + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) local Tasks = {} diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 5909ab54a..162e605fd 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -48,6 +48,7 @@ __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/DetectionZones.lua' ) __Moose.Include( 'Scripts/Moose/Functional/Designate.lua' ) __Moose.Include( 'Scripts/Moose/Functional/RAT.lua' ) __Moose.Include( 'Scripts/Moose/Functional/Range.lua' ) From 87a44f7f7f430eba90bfb2746e7e4f3bcc2ff2fd Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 15 Mar 2019 13:08:48 +0100 Subject: [PATCH 203/485] Still need to fix that index issue. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 2 ++ .../Moose/Functional/Detection.lua | 4 ++- .../Moose/Functional/DetectionZones.lua | 32 +++++++++---------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 0e64be634..d87424f98 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3983,7 +3983,9 @@ do -- AI_A2G_DISPATCHER self:ClearDefenderTask( DefenderGroup ) end else + -- TODO: prio 1, what is this index stuff again, simplify it. if DefenderTask.Target then + self:F( { Target = DefenderTask.Target } ) local AttackerItem = Detection:GetDetectedItemByIndex( DefenderTask.Target.Index ) if not AttackerItem then self:F( { "Removing obsolete Target:", DefenderTask.Target.Index } ) diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index d44edf334..e07c092be 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1627,7 +1627,9 @@ do -- DETECTION_BASE -- @return #DETECTION_BASE.DetectedItem function DETECTION_BASE:GetDetectedItemByIndex( Index ) - self:F( { DetectedItemsByIndex = self.DetectedItemsByIndex } ) + self:I( { DetectedItemsByIndex = self.DetectedItemsByIndex } ) + + self:I( { self.DetectedItemsByIndex } ) local DetectedItem = self.DetectedItemsByIndex[Index] if DetectedItem then diff --git a/Moose Development/Moose/Functional/DetectionZones.lua b/Moose Development/Moose/Functional/DetectionZones.lua index 614dce0ec..0b10e2b44 100644 --- a/Moose Development/Moose/Functional/DetectionZones.lua +++ b/Moose Development/Moose/Functional/DetectionZones.lua @@ -127,22 +127,22 @@ do -- DETECTION_ZONES function DETECTION_ZONES:CalculateIntercept( DetectedItem ) local DetectedCoord = DetectedItem.Coordinate - local DetectedSpeed = DetectedCoord:GetVelocity() - local DetectedHeading = DetectedCoord:GetHeading() - - if self.Intercept then - local DetectedSet = DetectedItem.Set - -- todo: speed - - local TranslateDistance = DetectedSpeed * self.InterceptDelay - - local InterceptCoord = DetectedCoord:Translate( TranslateDistance, DetectedHeading ) - - DetectedItem.InterceptCoord = InterceptCoord - else - DetectedItem.InterceptCoord = DetectedCoord - end - +-- local DetectedSpeed = DetectedCoord:GetVelocity() +-- local DetectedHeading = DetectedCoord:GetHeading() +-- +-- if self.Intercept then +-- local DetectedSet = DetectedItem.Set +-- -- todo: speed +-- +-- local TranslateDistance = DetectedSpeed * self.InterceptDelay +-- +-- local InterceptCoord = DetectedCoord:Translate( TranslateDistance, DetectedHeading ) +-- +-- DetectedItem.InterceptCoord = InterceptCoord +-- else +-- DetectedItem.InterceptCoord = DetectedCoord +-- end + DetectedItem.InterceptCoord = DetectedCoord end From fb23ac1d55fc32f74b7768a187d6b2c498a53069 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 16 Mar 2019 09:51:09 +0100 Subject: [PATCH 204/485] Updates to make the tasking work correctly. --- Moose Development/Moose/AI/AI_A2G_Dispatcher.lua | 2 +- Moose Development/Moose/Core/Set.lua | 14 ++++++++++++++ Moose Development/Moose/Functional/Detection.lua | 16 ++++++---------- .../Moose/Functional/DetectionZones.lua | 11 +++++------ .../Moose/Tasking/Task_A2G_Dispatcher.lua | 7 ++++--- 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index d87424f98..fad800e7d 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3985,7 +3985,7 @@ do -- AI_A2G_DISPATCHER else -- TODO: prio 1, what is this index stuff again, simplify it. if DefenderTask.Target then - self:F( { Target = DefenderTask.Target } ) + self:F( { TargetIndex = DefenderTask.Target.Index } ) local AttackerItem = Detection:GetDetectedItemByIndex( DefenderTask.Target.Index ) if not AttackerItem then self:F( { "Removing obsolete Target:", DefenderTask.Target.Index } ) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index eb210335a..4399c92f7 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -130,6 +130,20 @@ do -- SET_BASE return self end + + --- Clear the Objects in the Set. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:Clear() + + for Name, Object in pairs( self.Set ) do + self:Remove( Name ) + end + + return self + end + + --- Finds an @{Core.Base#BASE} object based on the object Name. -- @param #SET_BASE self diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index e07c092be..6bebb8c5d 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1524,18 +1524,14 @@ do -- DETECTION_BASE local DetectedItem = {} self.DetectedItemCount = self.DetectedItemCount + 1 self.DetectedItemMax = self.DetectedItemMax + 1 - - if DetectedItemKey then - self.DetectedItems[DetectedItemKey] = DetectedItem - else - self.DetectedItems[self.DetectedItemMax] = DetectedItem - end - - self.DetectedItemsByIndex[self.DetectedItemMax] = DetectedItem - + + self.DetectedItems[DetectedItemKey] = DetectedItem + + local DetectedItemIndex = DetectedItemKey or self.DetectedItemMax + self.DetectedItemsByIndex[DetectedItemIndex] = DetectedItem + DetectedItem.Index = DetectedItemIndex DetectedItem.Set = Set or SET_UNIT:New():FilterDeads():FilterCrashes() - DetectedItem.Index = DetectedItemKey or self.DetectedItemMax DetectedItem.ItemID = ItemPrefix .. "." .. self.DetectedItemMax DetectedItem.ID = self.DetectedItemMax DetectedItem.Removed = false diff --git a/Moose Development/Moose/Functional/DetectionZones.lua b/Moose Development/Moose/Functional/DetectionZones.lua index 0b10e2b44..c44ef9efe 100644 --- a/Moose Development/Moose/Functional/DetectionZones.lua +++ b/Moose Development/Moose/Functional/DetectionZones.lua @@ -261,8 +261,8 @@ do -- DETECTION_ZONES local DetectedUnits = SET_UNIT:New() - -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. - -- Regroup when needed, split groups when needed. + -- First go through all zones, and check if there are new Zones. + -- New Zones become a new DetectedItem. for ZoneName, DetectionZone in pairs( self.DetectionSetZone:GetSet() ) do local DetectedItem = self:GetDetectedItemByKey( ZoneName ) @@ -276,6 +276,8 @@ do -- DETECTION_ZONES -- Scan the zone DetectionZone:Scan( { Object.Category.UNIT }, { Unit.Category.GROUND_UNIT } ) + -- For all the units in the zone, + -- check if they are of the same coalition to be included. local ZoneUnits = DetectionZone:GetScannedUnits() for DCSUnitID, DCSUnit in pairs( ZoneUnits ) do local UnitName = DCSUnit:getName() @@ -289,11 +291,8 @@ do -- DETECTION_ZONES end end end - end - - - + -- Now all the tests should have been build, now make some smoke and flares... -- We also report here the friendlies within the detected areas. diff --git a/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua index 499efcf89..b7ae6ba17 100644 --- a/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua @@ -633,7 +633,7 @@ do -- TASK_A2G_DISPATCHER --DetectedSet:Flush( self ) local DetectedItemID = DetectedItem.ID - local TaskIndex = DetectedItem.ID + local TaskIndex = DetectedItem.Index local DetectedItemChanged = DetectedItem.Changed self:F( { DetectedItemChanged = DetectedItemChanged, DetectedItemID = DetectedItemID, TaskIndex = TaskIndex } ) @@ -649,6 +649,7 @@ do -- TASK_A2G_DISPATCHER if TargetSetUnit then if Task:IsInstanceOf( TASK_A2G_SEAD ) then Task:SetTargetSetUnit( TargetSetUnit ) + Task:SetDetection( Detection, DetectedItem ) Task:UpdateTaskInfo( DetectedItem ) TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) else @@ -659,7 +660,7 @@ do -- TASK_A2G_DISPATCHER if TargetSetUnit then if Task:IsInstanceOf( TASK_A2G_CAS ) then Task:SetTargetSetUnit( TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) + Task:SetDetection( Detection, DetectedItem ) Task:UpdateTaskInfo( DetectedItem ) TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) else @@ -671,7 +672,7 @@ do -- TASK_A2G_DISPATCHER if TargetSetUnit then if Task:IsInstanceOf( TASK_A2G_BAI ) then Task:SetTargetSetUnit( TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) + Task:SetDetection( Detection, DetectedItem ) Task:UpdateTaskInfo( DetectedItem ) TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) else From cff4f60923a692130acbbd66a8032e1ae32634de Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 16 Mar 2019 12:13:39 +0100 Subject: [PATCH 205/485] Improvements on reporting. --- Moose Development/Moose/Tasking/TaskInfo.lua | 57 +++++++------------ .../Moose/Tasking/Task_Capture_Dispatcher.lua | 10 +++- .../Moose/Tasking/Task_Capture_Zone.lua | 11 ++-- 3 files changed, 37 insertions(+), 41 deletions(-) diff --git a/Moose Development/Moose/Tasking/TaskInfo.lua b/Moose Development/Moose/Tasking/TaskInfo.lua index 95c14ee58..0e7c54885 100644 --- a/Moose Development/Moose/Tasking/TaskInfo.lua +++ b/Moose Development/Moose/Tasking/TaskInfo.lua @@ -306,65 +306,51 @@ function TASKINFO:Report( Report, Detail, ReportGroup, Task ) for Key, Data in UTILS.spairs( self.Info.Set, function( t, a, b ) return t[a].Order < t[b].Order end ) do - self:F( { Key = Key, Detail = Detail, Data = Data } ) - if Data.Detail:find( Detail ) then local Text = "" - if Key == "TaskName" then + if Key == "TaskName" then Key = nil Text = Data.Data - end - if Key == "Coordinate" then + elseif Key == "Coordinate" then local Coordinate = Data.Data -- Core.Point#COORDINATE Text = Coordinate:ToString( ReportGroup:GetUnit(1), nil, Task ) - end - if Key == "Threat" then + elseif Key == "Threat" then local DataText = Data.Data -- #string Text = DataText - end - if Key == "Counting" then + elseif Key == "Counting" then local DataText = Data.Data -- #string Text = DataText - end - if Key == "Targets" then + elseif Key == "Targets" then local DataText = Data.Data -- #string Text = DataText - end - if Key == "QFE" then + elseif Key == "QFE" then local Coordinate = Data.Data -- Core.Point#COORDINATE Text = Coordinate:ToStringPressure( ReportGroup:GetUnit(1), nil, Task ) - end - if Key == "Temperature" then + elseif Key == "Temperature" then local Coordinate = Data.Data -- Core.Point#COORDINATE Text = Coordinate:ToStringTemperature( ReportGroup:GetUnit(1), nil, Task ) - end - if Key == "Wind" then + elseif Key == "Wind" then local Coordinate = Data.Data -- Core.Point#COORDINATE Text = Coordinate:ToStringWind( ReportGroup:GetUnit(1), nil, Task ) - end - if Key == "Cargo" then + elseif Key == "Cargo" then + local DataText = Data.Data -- #string + Text = DataText + elseif Key == "Friendlies" then + local DataText = Data.Data -- #string + Text = DataText + elseif Key == "Players" then + local DataText = Data.Data -- #string + Text = DataText + else local DataText = Data.Data -- #string Text = DataText end - if Key == "Friendlies" then - local DataText = Data.Data -- #string - Text = DataText - end - if Key == "Players" then - local DataText = Data.Data -- #string - Text = DataText - end - if Line < math.floor( Data.Order / 10 ) then if Line == 0 then - if Text ~= "" then - Report:AddIndent( LineReport:Text( ", " ), "-" ) - end + Report:AddIndent( LineReport:Text( ", " ), "-" ) else - if Text ~= "" then - Report:AddIndent( LineReport:Text( ", " ) ) - end + Report:AddIndent( LineReport:Text( ", " ) ) end LineReport = REPORT:New() Line = math.floor( Data.Order / 10 ) @@ -373,8 +359,9 @@ function TASKINFO:Report( Report, Detail, ReportGroup, Task ) if Text ~= "" then LineReport:Add( ( Key and ( Key .. ":" ) or "" ) .. Text ) end + end end + Report:AddIndent( LineReport:Text( ", " ) ) - end diff --git a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua index d37ad7d5d..c38ed065f 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua @@ -246,6 +246,7 @@ do -- TASK_CAPTURE_DISPATCHER -- Here we need to check if the pilot is still existing. -- Task = self:RemoveTask( TaskIndex ) end + end -- Now that all obsolete tasks are removed, loop through the Zone tasks. @@ -257,25 +258,32 @@ do -- TASK_CAPTURE_DISPATCHER CaptureZone.Task.TaskPrefix = CaptureZone.TaskPrefix -- We keep the TaskPrefix for further reference! Mission:AddTask( CaptureZone.Task ) TaskReport:Add( TaskName ) + CaptureZone.Task:UpdateTaskInfo() + function CaptureZone.Task.OnEnterSuccess( Task, From, Event, To ) self:Success( Task ) + CaptureZone.Task:UpdateTaskInfo() end function CaptureZone.Task.OnEnterCancelled( Task, From, Event, To ) self:Cancelled( Task ) + CaptureZone.Task:UpdateTaskInfo() end - + function CaptureZone.Task.OnEnterFailed( Task, From, Event, To ) self:Failed( Task ) + CaptureZone.Task:UpdateTaskInfo() end function CaptureZone.Task.OnEnterAborted( Task, From, Event, To ) self:Aborted( Task ) + CaptureZone.Task:UpdateTaskInfo() end -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher. function CaptureZone.Task.OnAfterCaptured( Task, From, Event, To, TaskUnit ) self:Captured( Task, Task.TaskPrefix, TaskUnit ) + CaptureZone.Task:UpdateTaskInfo() end end diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index 8a56784ea..589a35c3c 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -219,12 +219,13 @@ do -- TASK_CAPTURE_ZONE --- Instantiates a new TASK_CAPTURE_ZONE. -- @param #TASK_CAPTURE_ZONE self - function TASK_CAPTURE_ZONE:UpdateTaskInfo() - + function TASK_CAPTURE_ZONE:UpdateTaskInfo( DetectedItem ) + local ZoneCoordinate = self.ZoneGoal:GetZone():GetCoordinate() - self.TaskInfo:AddCoordinate( ZoneCoordinate, 0, "SOD" ) - self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD" ) - self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD" ) + self.TaskInfo:AddTaskName( 0, "MSOD", true ) + self.TaskInfo:AddCoordinate( ZoneCoordinate, 1, "SOD", true ) + self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD", true ) + self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD", true ) end From 837361e899e28628c34b8a470d0db0c028538ea4 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 16 Mar 2019 12:57:14 +0100 Subject: [PATCH 206/485] Updates --- Moose Development/Moose/Tasking/Task_Capture_Zone.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index 589a35c3c..79d4817df 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -226,6 +226,8 @@ do -- TASK_CAPTURE_ZONE self.TaskInfo:AddCoordinate( ZoneCoordinate, 1, "SOD", true ) self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD", true ) self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD", true ) + --local ThreatLevel, ThreatText = self.TargetSetUnit:CalculateThreatLevelA2G() + --self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 10, "MOD", true ) end From 02ee12402a2948c579a73621df4136eb899133d6 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 16 Mar 2019 22:34:03 +0100 Subject: [PATCH 207/485] AIRBOSS v0.9.9.4 **AIRBOSS v0.9.9.4** - Script now supports human RIOs. - Added optional prefix for trap sheet files. - Fixed invalid characters in trap sheet file name cause script to crash (untested). Data will not be saved at all. - Inverted sign of line up error in trap sheet data to make it more intuetive. - Player who leave are deleted from self.players table. --- Moose Development/Moose/Ops/Airboss.lua | 119 +++++++++++++++++++----- 1 file changed, 98 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index a271598ac..937303aaa 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -219,6 +219,7 @@ -- @field #AIRBOSS.LUE lue Lineup error thresholds. -- @field #boolean trapsheet If true, players can save their trap sheets. -- @field #string trappath Path where to save the trap sheets. +-- @field #string trapprefix File prefix for trap sheet files. -- @extends Core.Fsm#FSM --- Be the boss! @@ -634,8 +635,8 @@ -- -- 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. +-- However, good mission planning involves also planning when aircraft are supposed to be launched or recovered. The definition of *case specific* recovery ops within the same mission is described in +-- the next section. -- -- ## Recovery Windows -- @@ -1191,6 +1192,7 @@ AIRBOSS = { lue = {}, trapsheet = nil, trappath = nil, + trapprefix = nil, } --- Aircraft types capable of landing on carrier (human+AI). @@ -1580,7 +1582,8 @@ AIRBOSS.Difficulty={ --- Player data table holding all important parameters of each player. -- @type AIRBOSS.PlayerData --- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string unitname Name of the unit. -- @field Wrapper.Client#CLIENT client Client object of player. -- @field #string callsign Callsign of player. -- @field #string difficulty Difficulty level. @@ -1621,7 +1624,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.3" +AIRBOSS.version="0.9.9.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -2621,11 +2624,13 @@ end --- Enable saving of player's trap sheets and specify an optional directory path. -- @param #AIRBOSS self -- @param #string path (Optional) Path where to save the trap sheets. +-- @param #string prefix (Optional) Prefix for trap sheet files. File name will be saved as *prefix_aircrafttype-0001.csv*, *prefix_aircrafttype-0002.csv*, etc. -- @return #AIRBOSS self -function AIRBOSS:SetTrapSheet(path) +function AIRBOSS:SetTrapSheet(path, prefix) if io then self.trapsheet=true self.trappath=path + self.trapprefix=prefix else self:E(self.lid.."ERROR: io is not desanitized. Cannot save trap sheet.") end @@ -3092,8 +3097,8 @@ function AIRBOSS:onafterStart(From, Event, To) -- Init patrol route of carrier. self:_PatrolRoute(1) - -- Check if no recovery window is set. - if #self.recoverytimes==0 then + -- Check if no recovery window is set. DISABLED! + if #self.recoverytimes==0 and false then -- Open window in 15 minutes for 3 hours. local Topen=timer.getAbsTime()+15*60 @@ -3155,6 +3160,19 @@ function AIRBOSS:onafterStatus(From, Event, To) clock, self:GetState(), self.case, self.carrier:GetVelocityKNOTS(), hdg, self.currentwp, eta, tostring(self.turning), tostring(collision)) self:T(self.lid..text) + -- Players online: + text="Players:" + local i=0 + for _name,_player in pairs(self.players) do + i=i+1 + local player=_player --#AIRBOSS.FlightGroup + text=text..string.format("\n%d.) %s step=%s, unit=%s", i, tostring(player.name), tostring(player.step), tostring(player.unitname)) + end + if i==0 then + text=text.." none" + end + self:I(self.lid..text) + -- Check for collision. if collision then @@ -6586,6 +6604,7 @@ function AIRBOSS:_NewPlayer(unitname) -- Player unit, client and callsign. playerData.unit = playerunit + playerData.unitname = unitname playerData.name = playername playerData.callsign = playerData.unit:GetCallsign() playerData.client = CLIENT:FindByName(unitname, nil, true) @@ -7173,7 +7192,18 @@ function AIRBOSS:_RemoveFlight(flight, completely) while #grades>0 and grades[#grades].finalscore==nil do table.remove(grades, #grades) end - end + end + + + -- Remove player from players table. + local playerdata=self.players[flight.name] + if playerdata then + self:I(self.lid..string.format("Removing player %s completely.", flight.name)) + self.players[flight.name]=nil + end + + -- Remove flight. + flight=nil else @@ -7511,9 +7541,9 @@ function AIRBOSS:OnEventBirth(EventData) local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - 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)) + self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T(self.lid.."BIRTH: player = "..tostring(_playername)) if _unit and _playername then @@ -7624,15 +7654,15 @@ function AIRBOSS:OnEventLand(EventData) 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) self:_RemoveFlightFromQueue(self.Qwaiting, playerData) self:_RemoveFlightFromQueue(self.Qspinning, playerData) + -- Reinitialize player data. + self:_InitPlayer(playerData) + return end @@ -7741,7 +7771,6 @@ end --- Airboss event handler for event that a unit shuts down its engines. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData ---function AIRBOSS:OnEventPlayerLeaveUnit(EventData) function AIRBOSS:OnEventEngineShutdown(EventData) self:F3({eventengineshutdown=EventData}) @@ -13189,6 +13218,27 @@ function AIRBOSS:_GetPlayerDataGroup(group) return nil end +--- Returns the unit of a player and the player name from the self.players table if it exists. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of player or nil. +function AIRBOSS:_GetPlayerUnit(_unitName) + + for _,_player in pairs(self.players) do + + local player=_player --#AIRBOSS.PlayerData + + if player.unit and player.unit:GetName()==_unitName then + self:T(self.lid..string.format("Found player=%s unit=%s in players table.", tostring(player.name), tostring(_unitName))) + return player.unit, player.name + end + + end + + return nil,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. @@ -13199,16 +13249,31 @@ function AIRBOSS:_GetPlayerUnitAndName(_unitName) if _unitName ~= nil then + -- First, let's look up all current players. + local u,pn=self:_GetPlayerUnit(_unitName) + + -- Return + if u and pn then + return u, pn + end + -- Get DCS unit from its name. local DCSunit=Unit.getByName(_unitName) if DCSunit then + -- Get player name if any. local playername=DCSunit:getPlayerName() + + -- Unit object. local unit=UNIT:Find(DCSunit) + -- Debug. self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + + -- Check if enverything is there. if DCSunit and unit and playername then + self:T(self.lid..string.format("Found DCS unit %s with player %s.", tostring(_unitName), tostring(playername))) return unit, playername end @@ -15925,7 +15990,11 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) -- Send message. self:MessageToPlayer(playerData, text, nil, "", 30, true) - end + else + self:E(self.lid..string.format("ERROR: playerData=nil. Unit name=%s, player name=%s", _unitName, _playername)) + end + else + self:E(self.lid..string.format("ERROR: could not find player for unit %s", _unitName)) end end @@ -16191,9 +16260,13 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) --- Function that saves data to file local function _savefile(filename, data) - local f = assert(io.open(filename, "wb")) - f:write(data) - f:close() + local f = io.open(filename, "wb") + if f then + f:write(data) + f:close() + else + self:E(self.lid..string.format("ERROR: could not save trap sheet to file %s.\nFile may contain invalid characters.", tostring(filename))) + end end -- Set path or default. @@ -16208,7 +16281,11 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) for i=1,9999 do -- Create file name - filename=string.format("AIRBOSS-%s_Trapsheet-%s_%s-%04d.csv", self.alias, playerData.name, playerData.actype, i) + if self.trapprefix then + filename=string.format("%s_%s-%04d.csv", self.trapprefix, playerData.actype, i) + else + filename=string.format("AIRBOSS-%s_Trapsheet-%s_%s-%04d.csv", self.alias, playerData.name, playerData.actype, i) + end -- Set path. if path~=nil then @@ -16242,7 +16319,7 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) local d=UTILS.MetersToFeet(groove.Alt) local e=groove.AoA local f=groove.GSE - local g=groove.LUE + local g=-groove.LUE local h=UTILS.MpsToKnots(groove.Vel) local i=groove.Vy*196.85 local j=groove.Gamma From 0ea5c7fa48bc0e9ca17274d8bec45c30f5a0cfcd Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 17 Mar 2019 07:33:28 +0100 Subject: [PATCH 208/485] Updated bug in Detection and other classes. Now only alive units are considered when broadcasting a message to a SET_GROUP. Made DESIGNATE crash in certain cases. --- Moose Development/Moose/Functional/Designate.lua | 2 +- Moose Development/Moose/Functional/Detection.lua | 10 +++++----- Moose Development/Moose/Wrapper/Positionable.lua | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/Functional/Designate.lua b/Moose Development/Moose/Functional/Designate.lua index 7d6f09d26..a11aa21b8 100644 --- a/Moose Development/Moose/Functional/Designate.lua +++ b/Moose Development/Moose/Functional/Designate.lua @@ -1393,7 +1393,7 @@ do -- DESIGNATE self:F( "Smoking ..." ) local RecceGroup = self.RecceSet:FindNearestGroupFromPointVec2(SmokeUnit:GetPointVec2()) - local RecceUnit = RecceGroup:GetUnit( 1 ) + local RecceUnit = RecceGroup:GetUnit( 1 ) -- Wrapper.Unit#UNIT if RecceUnit then diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 6bebb8c5d..76b3d0274 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1524,12 +1524,12 @@ do -- DETECTION_BASE local DetectedItem = {} self.DetectedItemCount = self.DetectedItemCount + 1 self.DetectedItemMax = self.DetectedItemMax + 1 - + + + DetectedItemKey = DetectedItemKey or self.DetectedItemMax self.DetectedItems[DetectedItemKey] = DetectedItem - - local DetectedItemIndex = DetectedItemKey or self.DetectedItemMax - self.DetectedItemsByIndex[DetectedItemIndex] = DetectedItem - DetectedItem.Index = DetectedItemIndex + self.DetectedItemsByIndex[DetectedItemKey] = DetectedItem + DetectedItem.Index = DetectedItemKey DetectedItem.Set = Set or SET_UNIT:New():FilterDeads():FilterCrashes() DetectedItem.ItemID = ItemPrefix .. "." .. self.DetectedItemMax diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 70d7a3c15..09d17a92c 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -1101,7 +1101,7 @@ function POSITIONABLE:MessageToSetGroup( Message, Duration, MessageSetGroup, Nam local DCSObject = self:GetDCSObject() if DCSObject then if DCSObject:isExist() then - MessageSetGroup:ForEachGroup( + MessageSetGroup:ForEachGroupAlive( function( MessageGroup ) self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) end From ece7fc3ea9dfe5a10e634e308975a4604ba1e8bd Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 17 Mar 2019 08:20:42 +0100 Subject: [PATCH 209/485] Threat level report for TASK_CAPTURE_ZONE --- .../Moose/AI/AI_A2G_Dispatcher.lua | 20 ++++++++++++++++--- Moose Development/Moose/Core/Zone.lua | 14 +++++++++++++ .../Moose/Tasking/DetectionManager.lua | 2 ++ Moose Development/Moose/Tasking/TaskInfo.lua | 2 +- .../Moose/Tasking/Task_Capture_Zone.lua | 7 ++++--- 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index fad800e7d..2a568b220 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3467,6 +3467,20 @@ do -- AI_A2G_DISPATCHER end end + function Fsm:onafterPatrolRoute( Defender, From, Event, To, AttackSetUnit ) + self:F({"Defender PatrolRoute", Defender:GetName()}) + self:GetParent(self).onafterPatrolRoute( self, Defender, From, Event, To, AttackSetUnit ) + + local DefenderName = Defender:GetName() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + if Squadron then + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) + end + + Dispatcher:ClearDefenderTaskTarget( Defender ) + end + function Fsm:onafterRTB( Defender, From, Event, To ) self:F({"Defender RTB", Defender:GetName()}) self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) @@ -3564,15 +3578,15 @@ do -- AI_A2G_DISPATCHER end end - function Fsm:OnAfterEngageRoute( Defender, From, Event, To, AttackSetUnit ) + function Fsm:onafterEngageRoute( Defender, From, Event, To, AttackSetUnit ) self:F({"Engage Route", Defender:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + self:GetParent(self).onafterEngageRoute( self, Defender, From, Event, To, AttackSetUnit ) local DefenderName = Defender:GetName() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - if FirstUnit then + if Squadron then local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 70fb83ca3..8c6d420e6 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -704,6 +704,20 @@ function ZONE_RADIUS:GetScannedUnits() end +function ZONE_RADIUS:GetScannedSetUnit() + + local SetUnit = SET_UNIT:New() + + if self.ScanData then + for ObjectID, UnitObject in pairs( self.ScanData.Units ) do + SetUnit:AddUnit( UNIT:FindByName(UnitObject:getName() ) ) + end + end + + return SetUnit +end + + function ZONE_RADIUS:CountScannedCoalitions() local Count = 0 diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index 04eab84a8..b641ac1c3 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -239,6 +239,8 @@ do -- DETECTION MANAGER -- @param #string Message The message to be sent. -- @return #DETECTION_MANGER self function DETECTION_MANAGER:MessageToPlayers( Message ) + + self:F( { Message = Message } ) if self.CC then self.CC:MessageToCoalition( Message ) diff --git a/Moose Development/Moose/Tasking/TaskInfo.lua b/Moose Development/Moose/Tasking/TaskInfo.lua index 0e7c54885..8f2d6231a 100644 --- a/Moose Development/Moose/Tasking/TaskInfo.lua +++ b/Moose Development/Moose/Tasking/TaskInfo.lua @@ -162,7 +162,7 @@ end -- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. -- @return #TASKINFO self function TASKINFO:AddThreat( ThreatText, ThreatLevel, Order, Detail, Keep ) - self:AddInfo( "Threat", ThreatText .. " [" .. string.rep( "■", ThreatLevel ) .. string.rep( "□", 10 - ThreatLevel ) .. "]", Order, Detail, Keep ) + self:AddInfo( "Threat", " [" .. string.rep( "■", ThreatLevel ) .. string.rep( "□", 10 - ThreatLevel ) .. "]:" .. ThreatText, Order, Detail, Keep ) return self end diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index 79d4817df..aa41cd7de 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -47,7 +47,7 @@ do -- TASK_ZONE_GOAL -- @param Tasking.Mission#MISSION Mission -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. -- @param #string TaskName The name of the Task. - -- @param Core.ZoneGoal#ZONE_GOAL ZoneGoal + -- @param Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal -- @return #TASK_ZONE_GOAL self function TASK_ZONE_GOAL:New( Mission, SetGroup, TaskName, ZoneGoal, TaskType, TaskBriefing ) local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType, TaskBriefing ) ) -- #TASK_ZONE_GOAL @@ -226,8 +226,9 @@ do -- TASK_CAPTURE_ZONE self.TaskInfo:AddCoordinate( ZoneCoordinate, 1, "SOD", true ) self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD", true ) self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD", true ) - --local ThreatLevel, ThreatText = self.TargetSetUnit:CalculateThreatLevelA2G() - --self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 10, "MOD", true ) + local SetUnit = self.ZoneGoal.Zone:GetScannedSetUnit() + local ThreatLevel, ThreatText = SetUnit:CalculateThreatLevelA2G() + self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 20, "MOD", true ) end From 02a486e457e2eb5b0c09c2c6e2e817e93bac1eb5 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 18 Mar 2019 18:04:08 +0100 Subject: [PATCH 210/485] In progress. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 6 ++++++ .../Moose/Functional/ZoneCaptureCoalition.lua | 6 +++--- .../Moose/Tasking/Task_Capture_Dispatcher.lua | 21 +++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 2a568b220..034b2ba06 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -1148,6 +1148,12 @@ do -- AI_A2G_DISPATCHER end + --- Locks the DefenseItem from being defended. + -- @param #AI_A2G_DISPATCHER self + -- @param #string DefenseItemKey The key of the defense item. + + + --- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:ResourcePark( DefenderSquadron ) diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 6d36beae0..32a860aa8 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -356,11 +356,11 @@ do -- ZONE_CAPTURE_COALITION -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( AttackZone, coalition.side.RED ) -- Create a new ZONE_CAPTURE_COALITION object of zone AttackZone with ownership RED coalition. -- ZoneCaptureCoalition:__Guard( 1 ) -- Start the Guarding of the AttackZone. -- - function ZONE_CAPTURE_COALITION:New( Zone, Coalition ) + function ZONE_CAPTURE_COALITION:New( Zone, Coalition, UnitCategories ) - local self = BASE:Inherit( self, ZONE_GOAL_COALITION:New( Zone, Coalition ) ) -- #ZONE_CAPTURE_COALITION + local self = BASE:Inherit( self, ZONE_GOAL_COALITION:New( Zone, Coalition, UnitCategories ) ) -- #ZONE_CAPTURE_COALITION - self:F( { Zone = Zone, Coalition = Coalition } ) + self:F( { Zone = Zone, Coalition = Coalition, UnitCategories = UnitCategories } ) do diff --git a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua index c38ed065f..514c355d0 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua @@ -168,7 +168,10 @@ do -- TASK_CAPTURE_DISPATCHER Zones = {}, ZoneCount = 0, } + + + TASK_CAPTURE_DISPATCHER.AI_A2G_Dispatcher = nil -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER --- TASK_CAPTURE_DISPATCHER constructor. -- @param #TASK_CAPTURE_DISPATCHER self @@ -223,6 +226,18 @@ do -- TASK_CAPTURE_DISPATCHER end + --- Link an AI_A2G_DISPATCHER to the TASK_CAPTURE_DISPATCHER. + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER AI_A2G_Dispatcher The AI Dispatcher to be linked to the tasking. + -- @return Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE + function TASK_CAPTURE_DISPATCHER:Link_AI_A2G_Dispatcher( AI_A2G_Dispatcher ) + + self.AI_A2G_Dispatcher = AI_A2G_Dispatcher -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER + + return self + end + + --- Assigns tasks to the @{Core.Set#SET_GROUP}. -- @param #TASK_CAPTURE_DISPATCHER self -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. @@ -259,6 +274,11 @@ do -- TASK_CAPTURE_DISPATCHER Mission:AddTask( CaptureZone.Task ) TaskReport:Add( TaskName ) CaptureZone.Task:UpdateTaskInfo() + + function CaptureZone.Task.OnEnterAssigned( Task, From, Event, To ) + self.AI_A2G_Dispatcher:Unlock( Task.TaskZoneName ) -- This will unlock the zone to be defended by AI. + CaptureZone.Task:UpdateTaskInfo() + end function CaptureZone.Task.OnEnterSuccess( Task, From, Event, To ) self:Success( Task ) @@ -267,6 +287,7 @@ do -- TASK_CAPTURE_DISPATCHER function CaptureZone.Task.OnEnterCancelled( Task, From, Event, To ) self:Cancelled( Task ) + self.AI_A2G_Dispatcher:Unlock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. CaptureZone.Task:UpdateTaskInfo() end From 70e7857b627a7a7055ca9c1bf0bcc484c0f38f08 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 18 Mar 2019 19:53:12 +0100 Subject: [PATCH 211/485] Implemented the linking of TASK_CAPTURE_DISPATCHER and AI_A2G_DISPATCHER. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 193 ++++++++++-------- .../Moose/Functional/Detection.lua | 66 ++++++ .../Moose/Tasking/Task_Capture_Dispatcher.lua | 21 +- 3 files changed, 193 insertions(+), 87 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 034b2ba06..825d2e124 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -1126,7 +1126,7 @@ do -- AI_A2G_DISPATCHER self.TakeoffScheduleID = self:ScheduleRepeat( 10, 10, 0, nil, self.ResourceTakeoff, self ) - self:__Start( 5 ) + self:__Start( 1 ) return self end @@ -1150,9 +1150,30 @@ do -- AI_A2G_DISPATCHER --- Locks the DefenseItem from being defended. -- @param #AI_A2G_DISPATCHER self - -- @param #string DefenseItemKey The key of the defense item. + -- @param #string DetectedItemIndex The index of the detected item. + function AI_A2G_DISPATCHER:Lock( DetectedItemIndex ) + self:F( { DetectedItemIndex = DetectedItemIndex } ) + local DetectedItem = self.Detection:GetDetectedItemByIndex( DetectedItemIndex ) + if DetectedItem then + self:F( { Locked = DetectedItem } ) + self.Detection:LockDetectedItem( DetectedItem ) + end + end + --- Unlocks the DefenseItem from being defended. + -- @param #AI_A2G_DISPATCHER self + -- @param #string DetectedItemIndex The index of the detected item. + function AI_A2G_DISPATCHER:Unlock( DetectedItemIndex ) + self:F( { DetectedItemIndex = DetectedItemIndex } ) + self:F( { Index = self.Detection.DetectedItemsByIndex } ) + local DetectedItem = self.Detection:GetDetectedItemByIndex( DetectedItemIndex ) + if DetectedItem then + self:F( { Unlocked = DetectedItem } ) + self.Detection:UnlockDetectedItem( DetectedItem ) + end + end + --- @param #AI_A2G_DISPATCHER self @@ -4032,100 +4053,102 @@ do -- AI_A2G_DISPATCHER -- 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 AttackCoordinate = 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 EngageCoordinate = nil - - for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do - local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE - - local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) + if not self.Detection:IsDetectedItemLocked( DetectedItem ) == true then + 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 - if EvaluateDistance <= self.DefenseRadius then + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) - local DistanceProbability = ( self.DefenseRadius / EvaluateDistance * self.DefenseReactivity ) - local DefenseProbability = math.random() + -- 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 EngageCoordinate = nil + + for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do + local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE + + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) - self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) + if EvaluateDistance <= self.DefenseRadius then - if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then - EngageCoordinate = DefenseCoordinate - break + local DistanceProbability = ( self.DefenseRadius / EvaluateDistance * self.DefenseReactivity ) + local DefenseProbability = math.random() + + self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) + + if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then + EngageCoordinate = DefenseCoordinate + break + end end end - end - - if EngageCoordinate 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", EngageCoordinate ) + + if EngageCoordinate 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", EngageCoordinate ) + 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", EngageCoordinate ) + 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", EngageCoordinate ) + end 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", EngageCoordinate ) - 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 - 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", EngageCoordinate ) - 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 - local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() - Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "■", ThreatLevel ) ) ) - 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" ) ) + if self.TacticalDisplay then + -- Show tactical situation + local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() + Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "■", ThreatLevel ) ) ) + 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 end end diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 76b3d0274..99f98848b 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1536,6 +1536,10 @@ do -- DETECTION_BASE DetectedItem.ID = self.DetectedItemMax DetectedItem.Removed = false + if self.Locking then + self:LockDetectedItem( DetectedItem ) + end + return DetectedItem end @@ -1725,6 +1729,68 @@ do -- DETECTION_BASE end + --- Lock the detected items when created and lock all existing detected items. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE + function DETECTION_BASE:LockDetectedItems() + + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:LockDetectedItem( DetectedItem ) + end + self.Locking = true + + return self + end + + + --- Unlock the detected items when created and unlock all existing detected items. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE + function DETECTION_BASE:UnlockDetectedItems() + + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:UnlockDetectedItem( DetectedItem ) + end + self.Locking = nil + + return self + end + + --- Validate if the detected item is locked. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @return #boolean + function DETECTION_BASE:IsDetectedItemLocked( DetectedItem ) + + return self.Locking and DetectedItem.Locked == true + + end + + + --- Lock a detected item. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @return #DETECTION_BASE + function DETECTION_BASE:LockDetectedItem( DetectedItem ) + + DetectedItem.Locked = true + + return self + end + + --- Unlock a detected item. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @return #DETECTION_BASE + function DETECTION_BASE:UnlockDetectedItem( DetectedItem ) + + DetectedItem.Locked = nil + + return self + end + + + --- Set the detected item coordinate. -- @param #DETECTION_BASE self diff --git a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua index 514c355d0..3fc4a86c8 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua @@ -233,6 +233,7 @@ do -- TASK_CAPTURE_DISPATCHER function TASK_CAPTURE_DISPATCHER:Link_AI_A2G_Dispatcher( AI_A2G_Dispatcher ) self.AI_A2G_Dispatcher = AI_A2G_Dispatcher -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER + AI_A2G_Dispatcher.Detection:LockDetectedItems() return self end @@ -276,34 +277,50 @@ do -- TASK_CAPTURE_DISPATCHER CaptureZone.Task:UpdateTaskInfo() function CaptureZone.Task.OnEnterAssigned( Task, From, Event, To ) - self.AI_A2G_Dispatcher:Unlock( Task.TaskZoneName ) -- This will unlock the zone to be defended by AI. + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Unlock( Task.TaskZoneName ) -- This will unlock the zone to be defended by AI. + end CaptureZone.Task:UpdateTaskInfo() end function CaptureZone.Task.OnEnterSuccess( Task, From, Event, To ) self:Success( Task ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end CaptureZone.Task:UpdateTaskInfo() end function CaptureZone.Task.OnEnterCancelled( Task, From, Event, To ) self:Cancelled( Task ) - self.AI_A2G_Dispatcher:Unlock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end CaptureZone.Task:UpdateTaskInfo() end function CaptureZone.Task.OnEnterFailed( Task, From, Event, To ) self:Failed( Task ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end CaptureZone.Task:UpdateTaskInfo() end function CaptureZone.Task.OnEnterAborted( Task, From, Event, To ) self:Aborted( Task ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end CaptureZone.Task:UpdateTaskInfo() end -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher. function CaptureZone.Task.OnAfterCaptured( Task, From, Event, To, TaskUnit ) self:Captured( Task, Task.TaskPrefix, TaskUnit ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end CaptureZone.Task:UpdateTaskInfo() end From 40ebc4e9905da1898039f85ca60773be8a195c7e Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 19 Mar 2019 01:18:34 +0100 Subject: [PATCH 212/485] AB v0.9.9.5 --- Moose Development/Moose/Ops/Airboss.lua | 491 +++++++++++++++++----- Moose Development/Moose/Wrapper/Group.lua | 10 +- 2 files changed, 393 insertions(+), 108 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 937303aaa..951d7bbb3 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -73,6 +73,8 @@ -- * [[MOOSE] Airboss - Groove Test A-4E Community Mod](https://www.youtube.com/watch?v=ZbjD7FHiaHo) -- * [[MOOSE] Airboss - Groove Test: On-the-fly LSO Grading](https://www.youtube.com/watch?v=Xgs1hwDcPyM) -- * [[MOOSE] Airboss - Carrier Auto Steam Into Wind](https://www.youtube.com/watch?v=IsU8dYgsp90) +-- * [[MOOSE] Airboss - CASE I Walkthrough by TG](https://www.youtube.com/watch?v=o1UrP4Q6PMM) +-- * [[MOOSE] Airboss - New LSO/Marshal Voice Overs by Raynor](https://www.youtube.com/watch?v=_Suo68bRu8k) -- -- ### Lex explaining Boat Ops: -- @@ -211,6 +213,7 @@ -- @field #number dTbeacon Time interval to refresh the beacons. Default 5 minutes. -- @field #AIRBOSS.LSOCalls LSOCall Radio voice overs of the LSO. -- @field #AIRBOSS.MarshalCalls MarshalCall Radio voice over of the Marshal/Airboss. +-- @field #AIRBOSS.PilotCalls PilotCall Radio voice over from AI pilots. -- @field #number lowfuelAI Low fuel threshold for AI groups in percent. -- @field #boolean emergency If true (default), allow emergency landings, i.e. bypass any pattern and go for final approach. -- @field #boolean respawnAI If true, respawn AI flights as they enter the CCA to detach and airfields from the mission plan. Default false. @@ -1379,6 +1382,30 @@ AIRBOSS.GroovePos={ -- @field #string modexreceiver Onboard number of the receiver (optional). -- @field #string sender Sender of the message (optional). Default radio alias. +--- Pilot radio calls. +-- type AIRBOSS.PilotCalls +-- @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. +-- @field #AIRBOSS.RadioCall POINT "Point" call. +-- @field #AIRBOSS.RadioCall BALL "Ball" call. +-- @field #AIRBOSS.RadioCall HARRIER "Harrier" call. +-- @field #AIRBOSS.RadioCall HAWKEYE "Hawkeye" call. +-- @field #AIRBOSS.RadioCall HORNET "Hornet" call. +-- @field #AIRBOSS.RadioCall SKYHAWK "Skyhawk" call. +-- @field #AIRBOSS.RadioCall TOMCAT "Tomcat" call. +-- @field #AIRBOSS.RadioCall VIKING "Viking" call. +-- @field #AIRBOSS.RadioCall BINGOFUEL "Bingo Fuel" call. +-- @field #AIRBOSS.RadioCall GASATDIVERT "Going for gas at the divert field" call. +-- @field #AIRBOSS.RadioCall GASATTANKER "Going for gas at the recovery tanker" call. + --- LSO radio calls. -- @type AIRBOSS.LSOCalls -- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call. @@ -1416,13 +1443,6 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard" call. -- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. -- @field #AIRBOSS.RadioCall NOISE Static noise sound. --- @field #AIRBOSS.RadioCall BALL "Ball" call. --- @field #AIRBOSS.RadioCall HARRIER "Harrier" call. --- @field #AIRBOSS.RadioCall HAWKEYE "Hawkeye" call. --- @field #AIRBOSS.RadioCall HORNET "Hornet" call. --- @field #AIRBOSS.RadioCall SKYHAWK "Skyhawk" call. --- @field #AIRBOSS.RadioCall TOMCAT "Tomcat" call. --- @field #AIRBOSS.RadioCall VIKING "Viking" call. -- @field #AIRBOSS.RadioCall SPINIT "Spin it" call. --- Marshal radio calls. @@ -1468,9 +1488,7 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall STARTINGRECOVERY "Starting aircraft recovery" call. -- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. -- @field #AIRBOSS.RadioCall NOISE Static noise sound. --- @field #AIRBOSS.RadioCall BINGOFUEL "Bingo Fuel" call. --- @field #AIRBOSS.RadioCall GASATDIVERT "Going for gas at the divert field" call. --- @field #AIRBOSS.RadioCall GASATTANKER "Going for gas at the recovery tanker" call. + --- Difficulty level. -- @type AIRBOSS.Difficulty @@ -1624,7 +1642,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.4" +AIRBOSS.version="0.9.9.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -4028,6 +4046,91 @@ function AIRBOSS:_InitTarawa() end +--- Set parameters for Marshal Voice overs by *Raynor*. +-- @param #AIRBOSS self +function AIRBOSS:SetVoiceOversMarshalRaynor() + + self.MarshalCall.AFFIRMATIVE.duration=0.70 + self.MarshalCall.ALTIMETER.duration=0.60 + self.MarshalCall.BRC.duration=0.60 + self.MarshalCall.CARRIERTURNTOHEADING.duration=1.87 + self.MarshalCall.CASE.duration=0.60 + self.MarshalCall.CHARLIETIME.duration=0.81 + self.MarshalCall.CLEAREDFORRECOVERY.duration=1.21 + self.MarshalCall.DECKCLOSED.duration=0.86 + self.MarshalCall.DEGREES.duration=0.55 + self.MarshalCall.EXPECTED.duration=0.61 + self.MarshalCall.FLYNEEDLES.duration=0.90 + self.MarshalCall.HOLDATANGELS.duration=0.91 + self.MarshalCall.HOURS.duration=0.54 + self.MarshalCall.MARSHALRADIAL.duration=0.80 + self.MarshalCall.N0.duration=0.38 + self.MarshalCall.N1.duration=0.30 + self.MarshalCall.N2.duration=0.30 + self.MarshalCall.N3.duration=0.30 + self.MarshalCall.N4.duration=0.32 + self.MarshalCall.N5.duration=0.41 + self.MarshalCall.N6.duration=0.48 + self.MarshalCall.N7.duration=0.51 + self.MarshalCall.N8.duration=0.38 + self.MarshalCall.N9.duration=0.34 + self.MarshalCall.NEGATIVE.duration=0.60 + self.MarshalCall.NEWFB.duration=1.10 + self.MarshalCall.OPS.duration=0.46 + self.MarshalCall.POINT.duration=0.21 + self.MarshalCall.RADIOCHECK.duration=0.95 + self.MarshalCall.RECOVERY.duration=0.63 + self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.36 + self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.8 -- Strangely the file is actually a shorter ~2.4 sec. + self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=2.75 + self.MarshalCall.REPORTSEEME.duration=0.96 + self.MarshalCall.RESUMERECOVERY.duration=1.41 + self.MarshalCall.ROGER.duration=0.41 + self.MarshalCall.SAYNEEDLES.duration=0.79 + self.MarshalCall.STACKFULL.duration=4.70 + self.MarshalCall.STARTINGRECOVERY.duration=2.06 + +end + +--- Set parameters for LSO Voice overs by *Raynor*. +-- @param #AIRBOSS self +function AIRBOSS:SetVoiceOversLSORaynor() + + self.LSOCall.BOLTER.duration=0.75 + self.LSOCall.CALLTHEBALL.duration=0.625 + self.LSOCall.CHECK.duration=0.40 + self.LSOCall.CLEAREDTOLAND.duration=0.85 + self.LSOCall.COMELEFT.duration=0.60 + self.LSOCall.DEPARTANDREENTER.duration=1.10 + self.LSOCall.EXPECTHEAVYWAVEOFF.duration=1.30 + self.LSOCall.EXPECTSPOT75.duration=1.85 + self.LSOCall.FAST.duration=0.75 + self.LSOCall.FOULDECK.duration=0.75 + self.LSOCall.HIGH.duration=0.65 + self.LSOCall.IDLE.duration=0.40 + self.LSOCall.LONGINGROOVE.duration=1.25 + self.LSOCall.LOW.duration=0.60 + self.LSOCall.N0.duration=0.38 + self.LSOCall.N1.duration=0.30 + self.LSOCall.N2.duration=0.30 + self.LSOCall.N3.duration=0.30 + self.LSOCall.N4.duration=0.32 + self.LSOCall.N5.duration=0.41 + self.LSOCall.N6.duration=0.48 + self.LSOCall.N7.duration=0.51 + self.LSOCall.N8.duration=0.38 + self.LSOCall.N9.duration=0.34 + self.LSOCall.PADDLESCONTACT.duration=0.91 + self.LSOCall.POWER.duration=0.45 + self.LSOCall.RADIOCHECK.duration=0.90 + self.LSOCall.RIGHTFORLINEUP.duration=0.70 + self.LSOCall.ROGERBALL.duration=0.72 + self.LSOCall.SLOW.duration=0.63 + self.LSOCall.SLOW.duration=0.59 + self.LSOCall.STABILIZED.duration=0.75 + self.LSOCall.WAVEOFF.duration=0.55 + self.LSOCall.WELCOMEABOARD.duration=0.80 +end --- Init voice over radio transmission call. -- @param #AIRBOSS self @@ -4307,62 +4410,6 @@ function AIRBOSS:_InitVoiceOvers() subtitle="", duration=3.6, }, - SKYHAWK={ - file="AIRBOSS-Skyhawk", - suffix="ogg", - loud=false, - subtitle="", - duration=0.95, - subduration=5, - }, - HARRIER={ - file="AIRBOSS-Harrier", - suffix="ogg", - loud=false, - subtitle="", - duration=0.58, - subduration=5, - }, - HAWKEYE={ - file="AIRBOSS-Hawkeye", - suffix="ogg", - loud=false, - subtitle="", - duration=0.63, - subduration=5, - }, - TOMCAT={ - file="AIRBOSS-Tomcat", - suffix="ogg", - loud=false, - subtitle="", - duration=0.66, - subduration=5, - }, - HORNET={ - file="AIRBOSS-Hornet", - suffix="ogg", - loud=false, - subtitle="", - duration=0.56, - subduration=5, - }, - VIKING={ - file="AIRBOSS-Viking", - suffix="ogg", - loud=false, - subtitle="", - duration=0.61, - subduration=5, - }, - BALL={ - file="AIRBOSS-Ball", - suffix="ogg", - loud=false, - subtitle="", - duration=0.50, - subduration=5, - }, SPINIT={ file="AIRBOSS-SpinIt", suffix="ogg", @@ -4373,6 +4420,168 @@ function AIRBOSS:_InitVoiceOvers() }, } + ----------------- + -- Pilot Calls -- + ----------------- + + -- Pilot Radio Calls. + self.PilotCall={ + N0={ + file="PILOT-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="PILOT-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="PILOT-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="PILOT-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="PILOT-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="PILOT-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N6={ + file="PILOT-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="PILOT-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="PILOT-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="PILOT-N9", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + POINT={ + file="PILOT-Point", + suffix="ogg", + loud=false, + subtitle="", + duration=0.33, + }, + SKYHAWK={ + file="PILOT-Skyhawk", + suffix="ogg", + loud=false, + subtitle="", + duration=0.95, + subduration=5, + }, + HARRIER={ + file="PILOT-Harrier", + suffix="ogg", + loud=false, + subtitle="", + duration=0.58, + subduration=5, + }, + HAWKEYE={ + file="PILOT-Hawkeye", + suffix="ogg", + loud=false, + subtitle="", + duration=0.63, + subduration=5, + }, + TOMCAT={ + file="PILOT-Tomcat", + suffix="ogg", + loud=false, + subtitle="", + duration=0.66, + subduration=5, + }, + HORNET={ + file="PILOT-Hornet", + suffix="ogg", + loud=false, + subtitle="", + duration=0.56, + subduration=5, + }, + VIKING={ + file="PILOT-Viking", + suffix="ogg", + loud=false, + subtitle="", + duration=0.61, + subduration=5, + }, + BALL={ + file="PILOT-Ball", + suffix="ogg", + loud=false, + subtitle="", + duration=0.50, + subduration=5, + }, + BINGOFUEL={ + file="PILOT-BingoFuel", + suffix="ogg", + loud=false, + subtitle="", + duration=0.80, + }, + GASATDIVERT={ + file="PILOT-GasAtDivert", + suffix="ogg", + loud=false, + subtitle="", + duration=1.80, + }, + GASATTANKER={ + file="PILOT-GasAtTanker", + suffix="ogg", + loud=false, + subtitle="", + duration=1.95, + }, + } + ------------------- -- MARSHAL Radio -- ------------------- @@ -4681,27 +4890,6 @@ function AIRBOSS:_InitVoiceOvers() subtitle="", duration=3.6, }, - BINGOFUEL={ - file="AIRBOSS-BingoFuel", - suffix="ogg", - loud=false, - subtitle="", - duration=0.80, - }, - GASATDIVERT={ - file="AIRBOSS-GasAtDivert", - suffix="ogg", - loud=false, - subtitle="", - duration=1.80, - }, - GASATTANKER={ - file="AIRBOSS-GasAtTanker", - suffix="ogg", - loud=false, - subtitle="", - duration=1.95, - }, } end @@ -7538,6 +7726,18 @@ end function AIRBOSS:OnEventBirth(EventData) self:F3({eventbirth = EventData}) + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") + self:E(EventData) + return + end + local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) @@ -7588,6 +7788,18 @@ end function AIRBOSS:OnEventLand(EventData) self:F3({eventland = EventData}) + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event LAND!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event LAND!") + self:E(EventData) + return + end + -- Get unit name that landed. local _unitName=EventData.IniUnitName @@ -7773,6 +7985,19 @@ end -- @param Core.Event#EVENTDATA EventData function AIRBOSS:OnEventEngineShutdown(EventData) self:F3({eventengineshutdown=EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event ENGINESHUTDOWN!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event ENGINESHUTDOWN!") + self:E(EventData) + return + end + local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) @@ -7822,6 +8047,19 @@ end function AIRBOSS:OnEventTakeoff(EventData) self:F3({eventtakeoff=EventData}) + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event TAKEOFF!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event TAKEOFF!") + self:E(EventData) + return + end + + local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) @@ -7873,6 +8111,19 @@ end -- @param Core.Event#EVENTDATA EventData function AIRBOSS:OnEventCrash(EventData) self:F3({eventcrash = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event CRASH!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event CRASH!") + self:E(EventData) + return + end + local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) @@ -7909,6 +8160,19 @@ end -- @param Core.Event#EVENTDATA EventData function AIRBOSS:OnEventEjection(EventData) self:F3({eventland = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event EJECTION!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event EJECTION!") + self:E(EventData) + return + end + local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) @@ -7949,6 +8213,19 @@ end --function AIRBOSS:OnEventPlayerLeaveUnit(EventData) function AIRBOSS:_PlayerLeft(EventData) self:F3({eventleave=EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event PLAYERLEFTUNIT!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event PLAYERLEFTUNIT!") + self:E(EventData) + return + end + local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) @@ -13477,7 +13754,8 @@ end -- @param #number delay Delay in seconds, before the message is broadcasted. -- @param #number interval Interval in seconds after the last sound has been played. -- @param #boolean click If true, play radio click at the end. -function AIRBOSS:RadioTransmission(radio, call, loud, delay, interval, click) +-- @param #booelan pilotcall If true, it's a pilot call. +function AIRBOSS:RadioTransmission(radio, call, loud, delay, interval, click, pilotcall) self:F2({radio=radio, call=call, loud=loud, delay=delay, interval=interval, click=click}) -- Nil check. @@ -13498,12 +13776,12 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay, interval, click) -- Player onboard number if sender has one. if self:_IsOnboard(call.modexsender) then - self:_Number2Radio(radio, call.modexsender, delay, 0.3) + self:_Number2Radio(radio, call.modexsender, delay, 0.3, pilotcall) end -- Play onboard number if receiver has one. if self:_IsOnboard(call.modexreceiver) then - self:_Number2Radio(radio, call.modexreceiver, delay, 0.3) + self:_Number2Radio(radio, call.modexreceiver, delay, 0.3, pilotcall) end -- Add transmission to the right queue. @@ -14058,8 +14336,9 @@ end -- @param #string number Number string, e.g. "032" or "183". -- @param #number delay Delay before transmission in seconds. -- @param #number interval Interval between the next call. +-- @param #boolean pilotcall If true, use pilot sound files. -- @return #number Duration of the call in seconds. -function AIRBOSS:_Number2Radio(radio, number, delay, interval) +function AIRBOSS:_Number2Radio(radio, number, delay, interval, pilotcall) --- Split string into characters. local function _split(str) @@ -14081,6 +14360,10 @@ function AIRBOSS:_Number2Radio(radio, number, delay, interval) self:E(self.lid..string.format("ERROR: Unknown radio alias %s!", tostring(radio.alias))) end + if pilotcall then + Sender="PilotCall" + end + -- Split string into characters. local numbers=_split(number) @@ -14132,18 +14415,18 @@ function AIRBOSS:_LSOCallAircraftBall(modex, nickname, fuelstate) local FS=UTILS.Split(string.format("%.1f", fuelstate), ".") -- Create new call to display complete subtitle. - local call=self:_NewRadioCall(self.LSOCall[NICKNAME], modex, text, self.Tmessage, nil, modex) + local call=self:_NewRadioCall(self.PilotCall[NICKNAME], modex, text, self.Tmessage, nil, modex) -- Hornet .. - self:RadioTransmission(self.LSORadio, call) + self:RadioTransmission(self.LSORadio, call, nil, nil, nil, nil, true) -- Ball, - self:RadioTransmission(self.LSORadio, self.LSOCall.BALL) + self:RadioTransmission(self.LSORadio, self.PilotCall.BALL, nil, nil, nil, nil, true) -- X.. - self:_Number2Radio(self.LSORadio, FS[1]) + self:_Number2Radio(self.LSORadio, FS[1], nil, nil, true) -- Point.. - self:RadioTransmission(self.LSORadio, self.MarshalCall.POINT) + self:RadioTransmission(self.LSORadio, self.PilotCall.POINT, nil, nil, nil, nil, true) -- Y. - self:_Number2Radio(self.LSORadio, FS[2]) + self:_Number2Radio(self.LSORadio, FS[2], nil, nil, true) -- CLICK! self:RadioTransmission(self.LSORadio, self.LSOCall.CLICK) @@ -14162,13 +14445,13 @@ function AIRBOSS:_MarshalCallGasAtTanker(modex) self:I(self.lid..text) -- Create new call to display complete subtitle. - local call=self:_NewRadioCall(self.MarshalCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex) + local call=self:_NewRadioCall(self.PilotCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex) -- MODEX, bingo fuel! - self:RadioTransmission(self.MarshalRadio, call) + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, nil, true) -- Going for fuel at the recovery tanker. Click! - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.GASATTANKER, nil, nil, nil, true) + self:RadioTransmission(self.MarshalRadio, self.PilotCall.GASATTANKER, nil, nil, nil, true, true) end @@ -14185,13 +14468,13 @@ function AIRBOSS:_MarshalCallGasAtDivert(modex, divertname) self:I(self.lid..text) -- Create new call to display complete subtitle. - local call=self:_NewRadioCall(self.MarshalCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex) + local call=self:_NewRadioCall(self.PilotCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex) -- MODEX, bingo fuel! - self:RadioTransmission(self.MarshalRadio, call) + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, nil, true) -- Going for fuel at the divert field. Click! - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.GASATDIVERT, nil, nil, nil, true) + self:RadioTransmission(self.MarshalRadio, self.PilotCall.GASATDIVERT, nil, nil, nil, true, true) end diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index a790deddd..25b6a9f91 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -952,10 +952,12 @@ function GROUP:GetFuelMin() local tmp = nil for UnitID, UnitData in pairs( self:GetUnits() ) do - tmp = UnitData:GetFuel() - if tmp < min then - min = tmp - unit = UnitData + if UnitData and UnitData:IsAlive() then + tmp = UnitData:GetFuel() + if tmp < min then + min = tmp + unit = UnitData + end end end From 5e20b244c50395f80ab2934f22dd40d0a092f703 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 19 Mar 2019 16:32:43 +0100 Subject: [PATCH 213/485] AB 0995w --- Moose Development/Moose/Ops/Airboss.lua | 103 ++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 951d7bbb3..01837d568 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1642,7 +1642,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.5" +AIRBOSS.version="0.9.9.5w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -4046,9 +4046,10 @@ function AIRBOSS:_InitTarawa() end ---- Set parameters for Marshal Voice overs by *Raynor*. + +--- Init parameters for Marshal Voice overs by *Raynor*. -- @param #AIRBOSS self -function AIRBOSS:SetVoiceOversMarshalRaynor() +function AIRBOSS:SetVoiceOversMarshalByRaynor() self.MarshalCall.AFFIRMATIVE.duration=0.70 self.MarshalCall.ALTIMETER.duration=0.60 @@ -4094,7 +4095,7 @@ end --- Set parameters for LSO Voice overs by *Raynor*. -- @param #AIRBOSS self -function AIRBOSS:SetVoiceOversLSORaynor() +function AIRBOSS:SetVoiceOversLSOByRaynor() self.LSOCall.BOLTER.duration=0.75 self.LSOCall.CALLTHEBALL.duration=0.625 @@ -4126,12 +4127,100 @@ function AIRBOSS:SetVoiceOversLSORaynor() self.LSOCall.RIGHTFORLINEUP.duration=0.70 self.LSOCall.ROGERBALL.duration=0.72 self.LSOCall.SLOW.duration=0.63 - self.LSOCall.SLOW.duration=0.59 + --self.LSOCall.SLOW.duration=0.59 --TODO self.LSOCall.STABILIZED.duration=0.75 self.LSOCall.WAVEOFF.duration=0.55 self.LSOCall.WELCOMEABOARD.duration=0.80 end + + +--- Set parameters for LSO Voice overs by *funkyfranky*. +-- @param #AIRBOSS self +function AIRBOSS:SetVoiceOversLSOByFF() + + self.LSOCall.BOLTER.duration=0.75 + self.LSOCall.CALLTHEBALL.duration=0.60 + self.LSOCall.CHECK.duration=0.45 + self.LSOCall.CLEAREDTOLAND.duration=1.00 + self.LSOCall.COMELEFT.duration=0.60 + self.LSOCall.DEPARTANDREENTER.duration=1.10 + self.LSOCall.EXPECTHEAVYWAVEOFF.duration=1.20 + self.LSOCall.EXPECTSPOT75.duration=2.00 + self.LSOCall.FAST.duration=0.70 + self.LSOCall.FOULDECK.duration=0.62 + self.LSOCall.HIGH.duration=0.65 + self.LSOCall.IDLE.duration=0.45 + self.LSOCall.LONGINGROOVE.duration=1.20 + self.LSOCall.LOW.duration=0.50 + self.LSOCall.N0.duration=0.40 + self.LSOCall.N1.duration=0.25 + self.LSOCall.N2.duration=0.37 + self.LSOCall.N3.duration=0.37 + self.LSOCall.N4.duration=0.39 + self.LSOCall.N5.duration=0.39 + self.LSOCall.N6.duration=0.40 + self.LSOCall.N7.duration=0.40 + self.LSOCall.N8.duration=0.37 + self.LSOCall.N9.duration=0.40 + self.LSOCall.PADDLESCONTACT.duration=1.00 + self.LSOCall.POWER.duration=0.50 + self.LSOCall.RADIOCHECK.duration=1.10 + self.LSOCall.RIGHTFORLINEUP.duration=0.80 + self.LSOCall.ROGERBALL.duration=1.00 + self.LSOCall.SLOW.duration=0.65 + self.LSOCall.SLOW.duration=0.59 + self.LSOCall.STABILIZED.duration=0.90 + self.LSOCall.WAVEOFF.duration=0.60 + self.LSOCall.WELCOMEABOARD.duration=1.00 +end + +--- Intit parameters for Marshal Voice overs by *funkyfranky*. +-- @param #AIRBOSS self +function AIRBOSS:SetVoiceOversMarshalByFF() + + self.MarshalCall.AFFIRMATIVE.duration=0.90 + self.MarshalCall.ALTIMETER.duration=0.85 + self.MarshalCall.BRC.duration=0.80 + self.MarshalCall.CARRIERTURNTOHEADING.duration=2.48 + self.MarshalCall.CASE.duration=0.40 + self.MarshalCall.CHARLIETIME.duration=0.90 + self.MarshalCall.CLEAREDFORRECOVERY.duration=1.25 + self.MarshalCall.DECKCLOSED.duration=1.10 + self.MarshalCall.DEGREES.duration=0.60 + self.MarshalCall.EXPECTED.duration=0.55 + self.MarshalCall.FLYNEEDLES.duration=0.90 + self.MarshalCall.HOLDATANGELS.duration=1.10 + self.MarshalCall.HOURS.duration=0.60 + self.MarshalCall.MARSHALRADIAL.duration=1.10 + self.MarshalCall.N0.duration=0.40 + self.MarshalCall.N1.duration=0.25 + self.MarshalCall.N2.duration=0.37 + self.MarshalCall.N3.duration=0.37 + self.MarshalCall.N4.duration=0.39 + self.MarshalCall.N5.duration=0.39 + self.MarshalCall.N6.duration=0.40 + self.MarshalCall.N7.duration=0.40 + self.MarshalCall.N8.duration=0.37 + self.MarshalCall.N9.duration=0.40 + self.MarshalCall.NEGATIVE.duration=0.80 + self.MarshalCall.NEWFB.duration=1.35 + self.MarshalCall.OPS.duration=0.48 + self.MarshalCall.POINT.duration=0.33 + self.MarshalCall.RADIOCHECK.duration=1.20 + self.MarshalCall.RECOVERY.duration=0.70 + self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.65 + self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.9 -- Strangely the file is actually a shorter ~2.4 sec. + self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=3.40 + self.MarshalCall.REPORTSEEME.duration=0.95 + self.MarshalCall.RESUMERECOVERY.duration=1.75 + self.MarshalCall.ROGER.duration=0.53 + self.MarshalCall.SAYNEEDLES.duration=0.90 + self.MarshalCall.STACKFULL.duration=6.35 + self.MarshalCall.STARTINGRECOVERY.duration=2.65 + +end + --- Init voice over radio transmission call. -- @param #AIRBOSS self function AIRBOSS:_InitVoiceOvers() @@ -4891,6 +4980,10 @@ function AIRBOSS:_InitVoiceOvers() duration=3.6, }, } + + -- Default timings by Raynor + self:SetVoiceOversLSOByRaynor() + self:SetVoiceOversMarshalByRaynor() end From d9d9235fba2dfdb86929faf248733a4799b6a577 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 19 Mar 2019 19:34:28 +0100 Subject: [PATCH 214/485] Great new additions: - Defense tactics (SetDefenseApproach) now can be: AI_A2G_DISPATCHER.DefenseApproach.Random -- random detected item AI_A2G_DISPATCHER.DefenseApproach.Distance -- shortest distance between defense point and detected item. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 100 ++++++++++++++---- 1 file changed, 81 insertions(+), 19 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 825d2e124..557c9bc13 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -919,7 +919,7 @@ do -- AI_A2G_DISPATCHER AI_A2G_DISPATCHER.Takeoff = GROUP.Takeoff --- Defnes Landing location. - -- @field Landing + -- @field #AI_A2G_DISPATCHER.Landing AI_A2G_DISPATCHER.Landing = { NearAirbase = 1, AtRunway = 2, @@ -950,7 +950,12 @@ do -- AI_A2G_DISPATCHER AI_A2G_DISPATCHER.DefenseQueue = {} - + --- Defense approach types + -- @type #AI_A2G_DISPATCHER.DefenseApproach + AI_A2G_DISPATCHER.DefenseApproach = { + Random = 1, + Distance = 2, + } --- AI_A2G_DISPATCHER constructor. -- This is defining the A2G DISPATCHER for one coaliton. @@ -996,6 +1001,8 @@ do -- AI_A2G_DISPATCHER -- self.Detection:SetRefreshTimeInterval( 30 ) self:SetDefenseRadius() + self:SetDefenseLimit( nil ) + self:SetDefenseApproach( AI_A2G_DISPATCHER.DefenseApproach.Random ) self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. @@ -1175,6 +1182,26 @@ do -- AI_A2G_DISPATCHER end + --- Sets maximum zones to be engaged at one time by defenders. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DefenseLimit The maximum amount of detected items to be engaged at the same time. + function AI_A2G_DISPATCHER:SetDefenseLimit( DefenseLimit ) + self:F( { DefenseLimit = DefenseLimit } ) + + self.DefenseLimit = DefenseLimit + end + + + --- Sets the method of the tactical approach of the defenses. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DefenseApproach Use the structure AI_A2G_DISPATCHER.DefenseApproach to set the defense approach. + -- The default defense approach is AI_A2G_DISPATCHER.DefenseApproach.Random. + function AI_A2G_DISPATCHER:SetDefenseApproach( DefenseApproach ) + self:F( { DefenseApproach = DefenseApproach } ) + + self._DefenseApproach = DefenseApproach + end + --- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:ResourcePark( DefenderSquadron ) @@ -3927,7 +3954,7 @@ do -- AI_A2G_DISPATCHER end end - return nil, nil, nil + return 0, 0, 0 end @@ -3960,7 +3987,7 @@ do -- AI_A2G_DISPATCHER end end - return nil, nil, nil + return 0, 0, 0 end @@ -3993,11 +4020,29 @@ do -- AI_A2G_DISPATCHER end end - return nil, nil, nil + return 0, 0, 0 end + --- Assigns A2G AI Tasks in relation to the detected items. + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:Order( DetectedItem ) + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ShortestDistance = 999999999 + + for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do + local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) + + if EvaluateDistance <= ShortestDistance then + ShortestDistance = EvaluateDistance + end + end + + return ShortestDistance + end --- Assigns A2G AI Tasks in relation to the detected items. -- @param #AI_A2G_DISPATCHER self @@ -4049,9 +4094,11 @@ do -- AI_A2G_DISPATCHER local DefenderGroupCount = 0 local DefendersTotal = 0 + local DefenseTotal = 0 -- Now that all obsolete tasks are removed, loop through the detected targets. - for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do if not self.Detection:IsDetectedItemLocked( DetectedItem ) == true then local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem @@ -4060,6 +4107,8 @@ do -- AI_A2G_DISPATCHER local DetectedZone = DetectedItem.Zone self:F( { "Target ID", DetectedItem.ItemID } ) + + self:F( { DefenseLimit = self.DefenseLimitmit, DefenseTotal = DefenseTotal } ) DetectedSet:Flush( self ) local DetectedID = DetectedItem.ID @@ -4086,17 +4135,24 @@ do -- AI_A2G_DISPATCHER self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) - if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then + if self._DefenseApproach == AI_A2G_DISPATCHER.DefenseApproach.Random then + + if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then + EngageCoordinate = DefenseCoordinate + break + end + end + if self._DefenseApproach == AI_A2G_DISPATCHER.DefenseApproach.Distance then EngageCoordinate = DefenseCoordinate break end end end - if EngageCoordinate then + if EngageCoordinate and DefenseTotal < self.DefenseLimit 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 + if DefendersMissing > 0 then self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD", EngageCoordinate ) end @@ -4104,7 +4160,7 @@ do -- AI_A2G_DISPATCHER 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 + if DefendersMissing > 0 then self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS", EngageCoordinate ) end @@ -4112,20 +4168,26 @@ do -- AI_A2G_DISPATCHER 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 + if DefendersMissing > 0 then self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI", EngageCoordinate ) 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 + + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + local Defender = Defender -- Wrapper.Group#GROUP + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + DefenseTotal = DefenseTotal + 1 + end + end + + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + if DefenseQueueItem.AttackerDetection.Index == DetectedItem.Index then + DefenseTotal = DefenseTotal + 1 + end + end if self.TacticalDisplay then -- Show tactical situation From 3f8468dcc7354caeea50715c6c97e0946ebc6aeb Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 19 Mar 2019 21:50:34 +0100 Subject: [PATCH 215/485] Fixed shortest distance to airbase. --- Moose Development/Moose/Tasking/Task_A2A.lua | 2 +- Moose Development/Moose/Tasking/Task_A2G.lua | 3 ++- .../Moose/Tasking/Task_Capture_Zone.lua | 14 ++++++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Tasking/Task_A2A.lua b/Moose Development/Moose/Tasking/Task_A2A.lua index bb91c02bb..0f11ba4c9 100644 --- a/Moose Development/Moose/Tasking/Task_A2A.lua +++ b/Moose Development/Moose/Tasking/Task_A2A.lua @@ -362,7 +362,7 @@ do -- TASK_A2A return math.random( 1, 9 ) elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then local Coordinate = self.TaskInfo:GetData( "Coordinate" ) - local Distance = TaskGroup:GetCoordinate():Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) return math.floor( Distance ) elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then return 1 diff --git a/Moose Development/Moose/Tasking/Task_A2G.lua b/Moose Development/Moose/Tasking/Task_A2G.lua index af0d25160..bb2e2f816 100644 --- a/Moose Development/Moose/Tasking/Task_A2G.lua +++ b/Moose Development/Moose/Tasking/Task_A2G.lua @@ -367,7 +367,8 @@ do -- TASK_A2G return math.random( 1, 9 ) elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then local Coordinate = self.TaskInfo:GetData( "Coordinate" ) - local Distance = TaskGroup:GetCoordinate():Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + self:F({Distance=Distance}) return math.floor( Distance ) elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then return 1 diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index aa41cd7de..dae324456 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -221,14 +221,16 @@ do -- TASK_CAPTURE_ZONE -- @param #TASK_CAPTURE_ZONE self function TASK_CAPTURE_ZONE:UpdateTaskInfo( DetectedItem ) + self:F({"Update"}) + local ZoneCoordinate = self.ZoneGoal:GetZone():GetCoordinate() - self.TaskInfo:AddTaskName( 0, "MSOD", true ) - self.TaskInfo:AddCoordinate( ZoneCoordinate, 1, "SOD", true ) - self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD", true ) - self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD", true ) + self.TaskInfo:AddTaskName( 0, "MSOD" ) + self.TaskInfo:AddCoordinate( ZoneCoordinate, 1, "SOD" ) + self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD" ) + self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD" ) local SetUnit = self.ZoneGoal.Zone:GetScannedSetUnit() local ThreatLevel, ThreatText = SetUnit:CalculateThreatLevelA2G() - self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 20, "MOD", true ) + self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 20, "MOD" ) end @@ -276,7 +278,7 @@ do -- TASK_CAPTURE_ZONE return math.random( 1, 9 ) elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then local Coordinate = self.TaskInfo:GetCoordinate() - local Distance = TaskGroup:GetCoordinate():Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) return math.floor( Distance ) elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then return 1 From a70fa066fe6a946717d4db286cbb13076f4d4f02 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 20 Mar 2019 00:12:04 +0100 Subject: [PATCH 216/485] AB 0995 --- Moose Development/Moose/Ops/Airboss.lua | 42 ++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 01837d568..9119791f0 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -4084,7 +4084,7 @@ function AIRBOSS:SetVoiceOversMarshalByRaynor() self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.36 self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.8 -- Strangely the file is actually a shorter ~2.4 sec. self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=2.75 - self.MarshalCall.REPORTSEEME.duration=0.96 + self.MarshalCall.REPORTSEEME.duration=1.06 --0.96 self.MarshalCall.RESUMERECOVERY.duration=1.41 self.MarshalCall.ROGER.duration=0.41 self.MarshalCall.SAYNEEDLES.duration=0.79 @@ -7900,9 +7900,9 @@ function AIRBOSS:OnEventLand(EventData) 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)) + self:T(self.lid.."LAND: unit = "..tostring(EventData.IniUnitName)) + self:T(self.lid.."LAND: group = "..tostring(EventData.IniGroupName)) + self:T(self.lid.."LAND: player = "..tostring(_playername)) -- This would be the closest airbase. local airbase=EventData.Place @@ -16689,23 +16689,23 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) for _,_groove in ipairs(playerData.trapsheet) do local groove=_groove --#AIRBOSS.GrooveData local t=groove.Time-T0 - local a=UTILS.MetersToNM(groove.Rho) - local b=-groove.X - local c=groove.Z - local d=UTILS.MetersToFeet(groove.Alt) - local e=groove.AoA - local f=groove.GSE - local g=-groove.LUE - local h=UTILS.MpsToKnots(groove.Vel) - local i=groove.Vy*196.85 - local j=groove.Gamma - local k=groove.Pitch - local l=groove.Roll - local m=groove.Yaw - local n=self:_GS(groove.Step, -1) - local o=groove.Grade - local p=groove.GradePoints - local q=groove.GradeDetail + local a=UTILS.MetersToNM(groove.Rho or 0) + local b=-groove.X or 0 + local c=groove.Z or 0 + local d=UTILS.MetersToFeet(groove.Alt or 0) + local e=groove.AoA or 0 + local f=groove.GSE or 0 + local g=-groove.LUE or 0 + local h=UTILS.MpsToKnots(groove.Vel or 0) + local i=(groove.Vy or 0)*196.85 + local j=groove.Gamma or 0 + local k=groove.Pitch or 0 + local l=groove.Roll or 0 + local m=groove.Yaw or 0 + local n=self:_GS(groove.Step, -1) or "n/a" + local o=groove.Grade or "n/a" + local p=groove.GradePoints or 0 + local q=groove.GradeDetail or "n/a" -- t a b c d e f g h i j k l m n o p q data=data..string.format("%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s,%s,%.1f,%s\n",t,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q) end From 8534afe0e394acc6f32fb55b513d29001d585e2e Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 20 Mar 2019 16:22:00 +0100 Subject: [PATCH 217/485] AB --- Moose Development/Moose/Ops/Airboss.lua | 2 ++ Moose Development/Moose/Wrapper/Unit.lua | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 9119791f0..b0ec07bf3 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -10457,6 +10457,7 @@ function AIRBOSS:_GetZoneHolding(case, stack) if self.carriertype==AIRBOSS.CarrierType.TARAWA then zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", self.carrier:GetVec2(), UTILS.NMToMeters(5)) end + else -- CASE II/II @@ -13369,6 +13370,7 @@ function AIRBOSS:_GetOnboardNumbers(group, playeronly) return numbers end + --- Get Tower frequency of carrier. -- @param #AIRBOSS self function AIRBOSS:_GetTowerFrequency() diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 91d157515..7fc35a772 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -318,6 +318,30 @@ function UNIT:GetCallsign() return nil end +--- Check if an (air) unit is a client or player slot. Information is retrieved from the group template. +-- @param #UNIT self +-- @return #boolean If true, unit is associated with a client or player slot. +function UNIT:IsPlayer() + + -- Get group. + local group=self:GetGroup() + + -- Units of template group. + local units=group:GetTemplate().units + + -- Get numbers. + for _,unit in pairs(units) do + + -- Check if unit name matach and skill is Client or Player. + if unit.name==self:GetName() and (unit.skill=="Client" or unit.skill=="Player") then + return true + end + + end + + return false +end + --- Returns name of the player that control the unit or nil if the unit is controlled by A.I. -- @param #UNIT self From 985282ba555a24d53f4d4f0992c29daf715f9dd6 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 20 Mar 2019 18:03:24 +0100 Subject: [PATCH 218/485] Debug --- Moose Development/Moose/Wrapper/Group.lua | 3 +++ Moose Development/Moose/Wrapper/Unit.lua | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index a790deddd..afe7667f2 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -2107,8 +2107,11 @@ do -- Players local PlayerNames = {} + self:F({Group = self:GetName()}) + local Units = self:GetUnits() for UnitID, UnitData in pairs( Units ) do + self:F({UnitData:GetName()}) local Unit = UnitData -- Wrapper.Unit#UNIT local PlayerName = Unit:GetPlayerName() if PlayerName and PlayerName ~= "" then diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 91d157515..51b5f2ecb 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -324,13 +324,16 @@ end -- @return #string Player Name -- @return #nil The DCS Unit is not existing or alive. function UNIT:GetPlayerName() - self:F2( self.UnitName ) + self:F( self.UnitName ) local DCSUnit = self:GetDCSObject() -- DCS#Unit if DCSUnit then + self:F({self:GetName()}) + local PlayerName = DCSUnit:getPlayerName() + self:F({PlayerName = PlayerName}) -- TODO Workaround DCS-BUG-3 - https://github.com/FlightControl-Master/MOOSE/issues/696 -- if PlayerName == nil or PlayerName == "" then -- local PlayerCategory = DCSUnit:getDesc().category From 964a6d07088b3000da95239b0df5d3643a29c8c6 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 20 Mar 2019 18:15:37 +0100 Subject: [PATCH 219/485] Debug --- Moose Development/Moose/Tasking/Task.lua | 2 +- Moose Development/Moose/Wrapper/Group.lua | 3 --- Moose Development/Moose/Wrapper/Unit.lua | 3 --- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index a99012590..797de7d61 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -1808,7 +1808,7 @@ function TASK:GetPlayerNames() --R2.1 Get a map of the players. if PlayerGroup:IsAlive() == true then if self:IsGroupAssigned( PlayerGroup ) then local PlayerNames = PlayerGroup:GetPlayerNames() - for PlayerNameID, PlayerName in pairs( PlayerNames ) do + for PlayerNameID, PlayerName in pairs( PlayerNames or {} ) do PlayerNameMap[PlayerName] = PlayerGroup end end diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index afe7667f2..a790deddd 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -2107,11 +2107,8 @@ do -- Players local PlayerNames = {} - self:F({Group = self:GetName()}) - local Units = self:GetUnits() for UnitID, UnitData in pairs( Units ) do - self:F({UnitData:GetName()}) local Unit = UnitData -- Wrapper.Unit#UNIT local PlayerName = Unit:GetPlayerName() if PlayerName and PlayerName ~= "" then diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 51b5f2ecb..363e42e08 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -330,10 +330,7 @@ function UNIT:GetPlayerName() if DCSUnit then - self:F({self:GetName()}) - local PlayerName = DCSUnit:getPlayerName() - self:F({PlayerName = PlayerName}) -- TODO Workaround DCS-BUG-3 - https://github.com/FlightControl-Master/MOOSE/issues/696 -- if PlayerName == nil or PlayerName == "" then -- local PlayerCategory = DCSUnit:getDesc().category From d9fb9c49282a9754e3edffe4dd26c55a2871322c Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 20 Mar 2019 18:36:55 +0100 Subject: [PATCH 220/485] Report Mission fix --- Moose Development/Moose/Tasking/CommandCenter.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 5c98fc1e0..ef35a5a24 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -212,7 +212,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) if EventGroup and self:HasGroup( EventGroup ) then local CommandCenterMenu = MENU_GROUP:New( EventGroup, self:GetText() ) local MenuReporting = MENU_GROUP:New( EventGroup, "Missions Reports", CommandCenterMenu ) - local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Status Report", MenuReporting, self.ReportMissionsStatus, self, EventGroup ) + local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Status Report", MenuReporting, self.ReportSummary, self, EventGroup ) local MenuMissionsDetails = MENU_GROUP_COMMAND:New( EventGroup, "Missions Players Report", MenuReporting, self.ReportMissionsPlayers, self, EventGroup ) self:ReportSummary( EventGroup ) local PlayerUnit = EventData.IniUnit From dab0aef1e37991e4097671404f6185c27a177b39 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 20 Mar 2019 21:57:18 +0100 Subject: [PATCH 221/485] AIRBOSS v0.9.9.5 --- Moose Development/Moose/Ops/Airboss.lua | 35 ++++++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index b0ec07bf3..85bb5d41b 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -879,6 +879,29 @@ -- -- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles/") -- +-- ## Carrier Specific Voice Overs +-- +-- It is possible to use different sound files for different carriers. If you have set up two (or more) AIRBOSS objects at different carriers - say Stennis and Tarawa - each +-- carrier would use the files in the specified directory, e.g. +-- +-- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles Stennis/") +-- airbossTarawa:SetSoundfilesFolder("Airboss Soundfiles Tarawa/") +-- +-- ## Sound Packs +-- +-- The AIRBOSS currently has two different "sound packs" for both LSO and Marshal radios. These contain voice overs by different actors. +-- These can be set by @{#AIRBOSS.SetVoiceOversLSOByRaynor}() and @{#AIRBOSS.SetVoiceOversMarshalByRaynor}(). These are the default settings. +-- The other sound files can be set by @{#AIRBOSS.SetVoiceOversLSOByFF}() and @{#AIRBOSS.SetVoiceOversMarshalByFF}(). +-- Also combinations can be used, e.g. +-- +-- airbossStennis:SetVoiceOversLSOByFF() +-- airbossStennis:SetVoiceOversMarshalByRaynor() +-- +-- In this example LSO voice overs by FF and Marshal voice overs by Raynor are used. +-- +-- **Note** that this only initializes the correct parameters parameters of sound files, i.e. the duration. The correct files have to be in the directory set by the +-- @{#AIRBOSS.SetSoundfilesFolder}(*folder*) function. +-- -- ## How To Use Your Own Voice Overs -- -- If you have a set of AIRBOSS sound files recorded or got it from elsewhere it is possible to use those instead of the default ones. @@ -898,13 +921,7 @@ -- -- Again, changing the file name, subtitle, subtitle duration is not required if you name the file exactly like the original one, which is this case would be "LSO-RogerBall.ogg". -- --- ## Carrier Specific Voice Overs -- --- It is possible to use different sound files for different carriers. If you have set up two (or more) AIRBOSS objects at different carriers - say Stennis and Tarawa - each --- carrier would use the files in the specified directory, e.g. --- --- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles Stennis/") --- airbossTarawa:SetSoundfilesFolder("Airboss Soundfiles Tarawa/") -- -- ## The Radio Dilemma -- @@ -3184,7 +3201,7 @@ function AIRBOSS:onafterStatus(From, Event, To) for _name,_player in pairs(self.players) do i=i+1 local player=_player --#AIRBOSS.FlightGroup - text=text..string.format("\n%d.) %s step=%s, unit=%s", i, tostring(player.name), tostring(player.step), tostring(player.unitname)) + text=text..string.format("\n%d.) %s: Step=%s, Unit=%s, Airframe=%s", i, tostring(player.name), tostring(player.step), tostring(player.unitname), tostring(player.actype)) end if i==0 then text=text.." none" @@ -4050,6 +4067,7 @@ end --- Init parameters for Marshal Voice overs by *Raynor*. -- @param #AIRBOSS self function AIRBOSS:SetVoiceOversMarshalByRaynor() + self:I(self.lid..string.format("Marshal Raynor reporting for duty!")) self.MarshalCall.AFFIRMATIVE.duration=0.70 self.MarshalCall.ALTIMETER.duration=0.60 @@ -4096,6 +4114,7 @@ end --- Set parameters for LSO Voice overs by *Raynor*. -- @param #AIRBOSS self function AIRBOSS:SetVoiceOversLSOByRaynor() + self:I(self.lid..string.format("LSO Raynor reporting for duty!")) self.LSOCall.BOLTER.duration=0.75 self.LSOCall.CALLTHEBALL.duration=0.625 @@ -4138,6 +4157,7 @@ end --- Set parameters for LSO Voice overs by *funkyfranky*. -- @param #AIRBOSS self function AIRBOSS:SetVoiceOversLSOByFF() + self:I(self.lid..string.format("Marshal FF reporting for duty!")) self.LSOCall.BOLTER.duration=0.75 self.LSOCall.CALLTHEBALL.duration=0.60 @@ -4178,6 +4198,7 @@ end --- Intit parameters for Marshal Voice overs by *funkyfranky*. -- @param #AIRBOSS self function AIRBOSS:SetVoiceOversMarshalByFF() + self:I(self.lid..string.format("Marshal FF reporting for duty!")) self.MarshalCall.AFFIRMATIVE.duration=0.90 self.MarshalCall.ALTIMETER.duration=0.85 From 3f2b3e0e1a1f17a520982908e9e8338a37e9179f Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 20 Mar 2019 21:59:32 +0100 Subject: [PATCH 222/485] AIRBOSS v0.9.9.5 - Added Raynor LSO/Marshal voice over parameters. - Added nil checks for events. --- Moose Development/Moose/Ops/Airboss.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 85bb5d41b..c03a52ace 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1659,7 +1659,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.5w" +AIRBOSS.version="0.9.9.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list From e996a30333eb392d2f2c364bda27c733f84c9166 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 21 Mar 2019 05:21:16 +0100 Subject: [PATCH 223/485] Updates --- .../Moose/Wrapper/Positionable.lua | 30 +++++++++++++++++ Moose Development/Moose/Wrapper/Static.lua | 32 ------------------- Moose Development/Moose/Wrapper/Unit.lua | 31 ------------------ 3 files changed, 30 insertions(+), 63 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 09d17a92c..400c2d3be 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -1479,3 +1479,33 @@ function POSITIONABLE:SmokeBlue() end +--- Returns true if the unit is within a @{Zone}. +-- @param #STPOSITIONABLEATIC 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 POSITIONABLE:IsInZone( Zone ) + self:F2( { self.PositionableName, 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 #POSITIONABLE 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 POSITIONABLE:IsNotInZone( Zone ) + self:F2( { self.PositionableName, Zone } ) + + if self:IsAlive() then + local IsNotInZone = not Zone:IsVec3InZone( self:GetVec3() ) + + return IsNotInZone + else + return false + end +end \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Static.lua b/Moose Development/Moose/Wrapper/Static.lua index fb3d73296..68c224922 100644 --- a/Moose Development/Moose/Wrapper/Static.lua +++ b/Moose Development/Moose/Wrapper/Static.lua @@ -214,35 +214,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 363e42e08..9dc6d4d6c 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -807,37 +807,6 @@ end -- Is functions ---- Returns true if the unit is within a @{Zone}. --- @param #UNIT 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 UNIT:IsInZone( Zone ) - self:F2( { self.UnitName, 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 #UNIT 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 UNIT:IsNotInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone - else - return false - end -end --- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. From ca65154ecddd6dc8e3d61435b37c9886e4ec6d3b Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 21 Mar 2019 05:49:48 +0100 Subject: [PATCH 224/485] Fixes --- Moose Development/Moose/Core/Zone.lua | 5 ++++- .../Moose/Tasking/Task_Capture_Zone.lua | 16 ++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 8c6d420e6..7e137253d 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -710,7 +710,10 @@ function ZONE_RADIUS:GetScannedSetUnit() if self.ScanData then for ObjectID, UnitObject in pairs( self.ScanData.Units ) do - SetUnit:AddUnit( UNIT:FindByName(UnitObject:getName() ) ) + local UnitObject = UnitObject -- DCS#Unit + if UnitObject:isExist() then + SetUnit:AddUnit( UNIT:FindByName( UnitObject:getName() ) ) + end end end diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index dae324456..71b96a8d7 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -211,7 +211,7 @@ do -- TASK_CAPTURE_ZONE "Capture Zone " .. self.TaskZoneName ) - self:UpdateTaskInfo() + self:UpdateTaskInfo( true ) return self end @@ -219,18 +219,18 @@ do -- TASK_CAPTURE_ZONE --- Instantiates a new TASK_CAPTURE_ZONE. -- @param #TASK_CAPTURE_ZONE self - function TASK_CAPTURE_ZONE:UpdateTaskInfo( DetectedItem ) + function TASK_CAPTURE_ZONE:UpdateTaskInfo( Persist ) - self:F({"Update"}) + Persist = Persist or false local ZoneCoordinate = self.ZoneGoal:GetZone():GetCoordinate() - self.TaskInfo:AddTaskName( 0, "MSOD" ) - self.TaskInfo:AddCoordinate( ZoneCoordinate, 1, "SOD" ) - self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD" ) - self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD" ) + self.TaskInfo:AddTaskName( 0, "MSOD", Persist ) + self.TaskInfo:AddCoordinate( ZoneCoordinate, 1, "SOD", Persist ) + self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD", Persist ) + self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD", Persist ) local SetUnit = self.ZoneGoal.Zone:GetScannedSetUnit() local ThreatLevel, ThreatText = SetUnit:CalculateThreatLevelA2G() - self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 20, "MOD" ) + self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 20, "MSOD", Persist ) end From e8b74f0fc7a56f4cffca9a217433090f3db0e7b7 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 21 Mar 2019 06:03:13 +0100 Subject: [PATCH 225/485] handling static in scan --- Moose Development/Moose/Core/Zone.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 7e137253d..871404e96 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -712,7 +712,15 @@ function ZONE_RADIUS:GetScannedSetUnit() for ObjectID, UnitObject in pairs( self.ScanData.Units ) do local UnitObject = UnitObject -- DCS#Unit if UnitObject:isExist() then - SetUnit:AddUnit( UNIT:FindByName( UnitObject:getName() ) ) + local FoundUnit = UNIT:FindByName( UnitObject:getName() ) + if FoundUnit then + SetUnit:AddUnit( FoundUnit ) + else + local FoundStatic = STATIC:FindByName( UnitObject:getName() ) + if FoundStatic then + SetUnit:AddUnit( FoundStatic ) + end + end end end end From 565a3062cb8cebd8799830a53062654e62cc80bf Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 21 Mar 2019 06:11:59 +0100 Subject: [PATCH 226/485] Fixing goal state machine rule --- Moose Development/Moose/Tasking/Task.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index 797de7d61..6a26a3d6f 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -432,7 +432,7 @@ function TASK:New( Mission, SetGroupAssign, TaskName, TaskType, TaskBriefing ) self:AddTransition( "Assigned", "Fail", "Failed" ) self:AddTransition( { "Planned", "Assigned" }, "Abort", "Aborted" ) self:AddTransition( "Assigned", "Cancel", "Cancelled" ) - self:AddTransition( "Assigned", "Goal", "*" ) + self:AddTransition( "*", "Goal", "*" ) self.Fsm = {} From 310ba9bb8a51853ef149013d4082775965ef3bb9 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 21 Mar 2019 06:18:48 +0100 Subject: [PATCH 227/485] Reverse --- Moose Development/Moose/Tasking/Task.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index 6a26a3d6f..797de7d61 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -432,7 +432,7 @@ function TASK:New( Mission, SetGroupAssign, TaskName, TaskType, TaskBriefing ) self:AddTransition( "Assigned", "Fail", "Failed" ) self:AddTransition( { "Planned", "Assigned" }, "Abort", "Aborted" ) self:AddTransition( "Assigned", "Cancel", "Cancelled" ) - self:AddTransition( "*", "Goal", "*" ) + self:AddTransition( "Assigned", "Goal", "*" ) self.Fsm = {} From b1e99dc31782ae9b054defbe7fefe46b2844b2f2 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 21 Mar 2019 06:31:13 +0100 Subject: [PATCH 228/485] Goal --- Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua | 2 +- Moose Development/Moose/Tasking/Task_Capture_Zone.lua | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua index 3fc4a86c8..74dff588e 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua @@ -284,7 +284,7 @@ do -- TASK_CAPTURE_DISPATCHER end function CaptureZone.Task.OnEnterSuccess( Task, From, Event, To ) - self:Success( Task ) + --self:Success( Task ) if self.AI_A2G_Dispatcher then self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. end diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index 71b96a8d7..10645599a 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -212,6 +212,8 @@ do -- TASK_CAPTURE_ZONE ) self:UpdateTaskInfo( true ) + + self:SetGoalTotal( 1 ) return self end @@ -261,6 +263,7 @@ do -- TASK_CAPTURE_ZONE Scoring:_AddMissionGoalScore( self.Mission, PlayerName, "Zone " .. self.ZoneGoal:GetZoneName() .." captured", PlayerContribution * 200 / TotalContributions ) end end + self:AddProgress( PlayerName, "Zone " .. self.ZoneGoal:GetZoneName() .." captured", timer.getTime() , 1 ) end end From 9856a8625dfa5ff60719a8af691a1b7a8cbdca47 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 21 Mar 2019 06:57:04 +0100 Subject: [PATCH 229/485] Goal --- Moose Development/Moose/Core/Goal.lua | 1 + .../Moose/Tasking/CommandCenter.lua | 16 ++++++++++------ .../Moose/Tasking/Task_Capture_Zone.lua | 3 +-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Core/Goal.lua b/Moose Development/Moose/Core/Goal.lua index 4165e7f3f..ac1f616b9 100644 --- a/Moose Development/Moose/Core/Goal.lua +++ b/Moose Development/Moose/Core/Goal.lua @@ -142,6 +142,7 @@ do -- Goal -- @param #GOAL self -- @param #string PlayerName The name of the player. function GOAL:AddPlayerContribution( PlayerName ) + self:F({PlayerName}) self.Players[PlayerName] = self.Players[PlayerName] or 0 self.Players[PlayerName] = self.Players[PlayerName] + 1 self.TotalContributions = self.TotalContributions + 1 diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index ef35a5a24..dcef1bb72 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -507,14 +507,18 @@ function COMMANDCENTER:AssignTask( TaskGroup ) end local Task = Tasks[ math.random( 1, #Tasks ) ] -- Tasking.Task#TASK + + if Task then - self:I( "Assigning task " .. Task:GetName() .. " using auto assign method " .. self.AutoAssignMethod .. " to " .. TaskGroup:GetName() .. " with task priority " .. AssignPriority ) - - if not self.AutoAcceptTasks == true then - Task:SetAutoAssignMethod( ACT_ASSIGN_MENU_ACCEPT:New( Task.TaskBriefing ) ) + self:I( "Assigning task " .. Task:GetName() .. " using auto assign method " .. self.AutoAssignMethod .. " to " .. TaskGroup:GetName() .. " with task priority " .. AssignPriority ) + + if not self.AutoAcceptTasks == true then + Task:SetAutoAssignMethod( ACT_ASSIGN_MENU_ACCEPT:New( Task.TaskBriefing ) ) + end + + Task:AssignToGroup( TaskGroup ) + end - - Task:AssignToGroup( TaskGroup ) end diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index 10645599a..e39e8b2c9 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -253,7 +253,6 @@ do -- TASK_CAPTURE_ZONE if self.ZoneGoal then if self.ZoneGoal.Goal:IsAchieved() then - self:Success() local TotalContributions = self.ZoneGoal.Goal:GetTotalContributions() local PlayerContributions = self.ZoneGoal.Goal:GetPlayerContributions() self:F( { TotalContributions = TotalContributions, PlayerContributions = PlayerContributions } ) @@ -263,7 +262,7 @@ do -- TASK_CAPTURE_ZONE Scoring:_AddMissionGoalScore( self.Mission, PlayerName, "Zone " .. self.ZoneGoal:GetZoneName() .." captured", PlayerContribution * 200 / TotalContributions ) end end - self:AddProgress( PlayerName, "Zone " .. self.ZoneGoal:GetZoneName() .." captured", timer.getTime() , 1 ) + self:Success() end end From ec155ed80c760759cb72eca4a3cf42b41fc0a87c Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 22 Mar 2019 16:23:22 +0100 Subject: [PATCH 230/485] AB v0.9.9.6w --- Moose Development/Moose/Ops/Airboss.lua | 135 ++++++++++++++++++------ 1 file changed, 102 insertions(+), 33 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index c03a52ace..8d3623ddf 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -208,6 +208,8 @@ -- @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. +-- @field #string soundfolderLSO Folder withing the mission (miz) file where LSO sound files are stored. +-- @field #string soundfolderMSH Folder withing the mission (miz) file where Marshal sound files are stored. -- @field #boolean despawnshutdown Despawn group after engine shutdown. -- @field #number Tbeacon Last time the beacons were refeshed. -- @field #number dTbeacon Time interval to refresh the beacons. Default 5 minutes. @@ -1200,6 +1202,8 @@ AIRBOSS = { collisiondist = nil, Tmessage = nil, soundfolder = nil, + soundfolderLSO = nil, + soundfolderMSH = nil, despawnshutdown= nil, dTbeacon = nil, Tbeacon = nil, @@ -1659,7 +1663,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.5" +AIRBOSS.version="0.9.9.6w" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -4066,8 +4070,23 @@ end --- Init parameters for Marshal Voice overs by *Raynor*. -- @param #AIRBOSS self -function AIRBOSS:SetVoiceOversMarshalByRaynor() - self:I(self.lid..string.format("Marshal Raynor reporting for duty!")) +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversMarshalByRaynor(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderMSH=mizfolder + else + -- Default is the general folder. + self.soundfolderMSH=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("Marshal Raynor reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) self.MarshalCall.AFFIRMATIVE.duration=0.70 self.MarshalCall.ALTIMETER.duration=0.60 @@ -4113,8 +4132,23 @@ end --- Set parameters for LSO Voice overs by *Raynor*. -- @param #AIRBOSS self -function AIRBOSS:SetVoiceOversLSOByRaynor() - self:I(self.lid..string.format("LSO Raynor reporting for duty!")) +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversLSOByRaynor(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderLSO=mizfolder + else + -- Default is the general folder. + self.soundfolderLSO=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("LSO Raynor reporting for duty! Soundfolder=%s", tostring(self.soundfolderLSO))) self.LSOCall.BOLTER.duration=0.75 self.LSOCall.CALLTHEBALL.duration=0.625 @@ -4156,8 +4190,20 @@ end --- Set parameters for LSO Voice overs by *funkyfranky*. -- @param #AIRBOSS self -function AIRBOSS:SetVoiceOversLSOByFF() - self:I(self.lid..string.format("Marshal FF reporting for duty!")) +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversLSOByFF(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderLSO=mizfolder + else + -- Default is the general folder. + self.soundfolderLSO=self.soundfolder + end self.LSOCall.BOLTER.duration=0.75 self.LSOCall.CALLTHEBALL.duration=0.60 @@ -4197,8 +4243,23 @@ end --- Intit parameters for Marshal Voice overs by *funkyfranky*. -- @param #AIRBOSS self -function AIRBOSS:SetVoiceOversMarshalByFF() - self:I(self.lid..string.format("Marshal FF reporting for duty!")) +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversMarshalByFF(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderMSH=mizfolder + else + -- Default is the general folder. + self.soundfolderMSH=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("Marshal FF reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) self.MarshalCall.AFFIRMATIVE.duration=0.90 self.MarshalCall.ALTIMETER.duration=0.85 @@ -5020,7 +5081,7 @@ function AIRBOSS:SetVoiceOver(radiocall, duration, subtitle, subduration, filena radiocall.duration=duration radiocall.subtitle=subtitle or radiocall.subtitle radiocall.file=filename - radiocall.suffix=".ogg" + radiocall.suffix=suffix or ".ogg" end --- Get optimal aircraft AoA parameters.. @@ -13955,7 +14016,7 @@ function AIRBOSS:Broadcast(radio, call, loud) local sender=self:_GetRadioSender(radio) -- Construct file name and subtitle. - local filename=self:_RadioFilename(call, loud) + local filename=self:_RadioFilename(call, loud, radio.alias) -- Create subtitle for transmission. local subtitle=self:_RadioSubtitle(radio, call, loud) @@ -14064,7 +14125,7 @@ function AIRBOSS:Sound2Player(playerData, radio, call, loud, delay) if playerData.unit:IsInZone(self.zoneCCA) and call then -- Construct file name. - local filename=self:_RadioFilename(call, loud) + local filename=self:_RadioFilename(call, loud, radio.alias) -- Get Subtitle local subtitle=self:_RadioSubtitle(radio, call, loud) @@ -14134,25 +14195,34 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. -- @param #boolean loud Use loud version of file if available. +-- @param #string channel Radio channel alias "LSO" or "LSOCall", "MARSHAL" or "MarshalCall". -- @return #string The file name of the radio sound. -function AIRBOSS:_RadioFilename(call, loud) +function AIRBOSS:_RadioFilename(call, loud, channel) - -- Construct file name and subtitle. - local prefix=call.file or "" - local suffix=call.suffix or "ogg" - - -- Path to sound files. Default is in the ME - local path=self.soundfolder or "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) + -- Construct file name and subtitle. + local prefix=call.file or "" + local suffix=call.suffix or "ogg" + + -- Path to sound files. Default is in the ME + local path=self.soundfolder or "l10n/DEFAULT/" + + -- Check for special LSO and Marshal sound folders. + if string.find(call.file, "LSO-") and channel and (channel=="LSO" or channel=="LSOCall") then + path=self.soundfolderLSO or path + end + if string.find(call.file, "MARSHAL-") and channel and (channel=="MARSHAL" or channel=="MarshalCall") then + path=self.soundfolderMSH or path + end + + -- 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 + return filename end --- Send text message to player client. @@ -14206,21 +14276,21 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration -- Negative. if string.find(text:lower(), "negative") then - local filename=self:_RadioFilename(self.MarshalCall.NEGATIVE) + local filename=self:_RadioFilename(self.MarshalCall.NEGATIVE, false, "MARSHAL") USERSOUND:New(filename):ToGroup(playerData.group, wait) wait=wait+self.MarshalCall.NEGATIVE.duration end -- Affirm. if string.find(text:lower(), "affirm") then - local filename=self:_RadioFilename(self.MarshalCall.AFFIRMATIVE) + local filename=self:_RadioFilename(self.MarshalCall.AFFIRMATIVE, false, "MARSHAL") USERSOUND:New(filename):ToGroup(playerData.group, wait) wait=wait+self.MarshalCall.AFFIRMATIVE.duration end -- Roger. if string.find(text:lower(), "roger") then - local filename=self:_RadioFilename(self.MarshalCall.ROGER) + local filename=self:_RadioFilename(self.MarshalCall.ROGER, false, "MARSHAL") USERSOUND:New(filename):ToGroup(playerData.group, wait) wait=wait+self.MarshalCall.ROGER.duration end @@ -14432,8 +14502,7 @@ function AIRBOSS:_Number2Sound(playerData, sender, number, delay) local call=self[Sender][N] --#AIRBOSS.RadioCall -- Create file name. - --local filename=string.format("%s.%s", call.file, call.suffix) - local filename=self:_RadioFilename(call, false) + local filename=self:_RadioFilename(call, false, Sender) -- Play sound. USERSOUND:New(filename):ToGroup(playerData.group, delay+wait) From aa8498ce0b867b8401622b0448be2a225f2a6e14 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 22 Mar 2019 22:05:25 +0100 Subject: [PATCH 231/485] AIRBOSS v0.9.9.6 --- Moose Development/Moose/Ops/Airboss.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 8d3623ddf..8d18d4e53 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -73,8 +73,9 @@ -- * [[MOOSE] Airboss - Groove Test A-4E Community Mod](https://www.youtube.com/watch?v=ZbjD7FHiaHo) -- * [[MOOSE] Airboss - Groove Test: On-the-fly LSO Grading](https://www.youtube.com/watch?v=Xgs1hwDcPyM) -- * [[MOOSE] Airboss - Carrier Auto Steam Into Wind](https://www.youtube.com/watch?v=IsU8dYgsp90) --- * [[MOOSE] Airboss - CASE I Walkthrough by TG](https://www.youtube.com/watch?v=o1UrP4Q6PMM) +-- * [[MOOSE] Airboss - CASE I Walkthrough in the F/A-18C by TG](https://www.youtube.com/watch?v=o1UrP4Q6PMM) -- * [[MOOSE] Airboss - New LSO/Marshal Voice Overs by Raynor](https://www.youtube.com/watch?v=_Suo68bRu8k) +-- * [[MOOSE] Airboss - CASE I, "Until We Go Down" featuring the F-14B by Pikes](https://www.youtube.com/watch?v=ojgHDSw3Doc) -- -- ### Lex explaining Boat Ops: -- @@ -1663,7 +1664,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.6w" +AIRBOSS.version="0.9.9.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -4204,6 +4205,9 @@ function AIRBOSS:SetVoiceOversLSOByFF(mizfolder) -- Default is the general folder. self.soundfolderLSO=self.soundfolder end + + -- Report for duty. + self:I(self.lid..string.format("LSO FF reporting for duty! Soundfolder=%s", tostring(self.soundfolderLSO))) self.LSOCall.BOLTER.duration=0.75 self.LSOCall.CALLTHEBALL.duration=0.60 From bfb11b6f79026607543f811c20dae7c6644c26ec Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 23 Mar 2019 10:45:14 +0100 Subject: [PATCH 232/485] Updates on goal logic --- Moose Development/Moose/Tasking/Mission.lua | 15 ++++++++------- Moose Development/Moose/Tasking/Task.lua | 17 +++++++++++++++++ .../Moose/Tasking/Task_Capture_Zone.lua | 4 ++-- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/Moose Development/Moose/Tasking/Mission.lua b/Moose Development/Moose/Tasking/Mission.lua index 2d6ac4660..f057c9648 100644 --- a/Moose Development/Moose/Tasking/Mission.lua +++ b/Moose Development/Moose/Tasking/Mission.lua @@ -968,18 +968,19 @@ function MISSION:ReportPlayersProgress( ReportGroup ) -- Determine how many tasks are remaining. for TaskID, Task in pairs( self:GetTasks() ) do local Task = Task -- Tasking.Task#TASK - local TaskGoalTotal = Task:GetGoalTotal() or 0 local TaskName = Task:GetName() + local Goal = Task:GetGoal() PlayerList[TaskName] = PlayerList[TaskName] or {} - if TaskGoalTotal ~= 0 then - local PlayerNames = self:GetPlayerNames() - for PlayerName, PlayerData in pairs( PlayerNames ) do - PlayerList[TaskName][PlayerName] = string.format( 'Player (%s): Task "%s": %d%%', PlayerName, TaskName, Task:GetPlayerProgress( PlayerName ) * 100 / TaskGoalTotal ) + if Goal then + local TotalContributions = Goal:GetTotalContributions() + local PlayerContributions = Goal:GetPlayerContributions() + self:F( { TotalContributions = TotalContributions, PlayerContributions = PlayerContributions } ) + for PlayerName, PlayerContribution in pairs( PlayerContributions ) do + PlayerList[TaskName][PlayerName] = string.format( 'Player (%s): Task "%s": %d%%', PlayerName, TaskName, PlayerContributions[PlayerName] * 100 / TotalContributions ) end else PlayerList[TaskName]["_"] = string.format( 'Player (---): Task "%s": %d%%', TaskName, 0 ) - end - + end end for TaskName, TaskData in pairs( PlayerList ) do diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index 797de7d61..68c8a4803 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -1709,6 +1709,23 @@ end do -- Links + --- Set goal of a task + -- @param #TASK self + -- @param Core.Goal#GOAL Goal + -- @return #TASK + function TASK:SetGoal( Goal ) + self.Goal = Goal + end + + + --- Get goal of a task + -- @param #TASK self + -- @return Core.Goal#GOAL The Goal + function TASK:GetGoal() + return self.Goal + end + + --- Set dispatcher of a task -- @param #TASK self -- @param Tasking.DetectionManager#DETECTION_MANAGER Dispatcher diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index e39e8b2c9..9ab0f35c7 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -213,7 +213,7 @@ do -- TASK_CAPTURE_ZONE self:UpdateTaskInfo( true ) - self:SetGoalTotal( 1 ) + self:SetGoal( self.ZoneGoal.Goal ) return self end @@ -232,7 +232,7 @@ do -- TASK_CAPTURE_ZONE self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD", Persist ) local SetUnit = self.ZoneGoal.Zone:GetScannedSetUnit() local ThreatLevel, ThreatText = SetUnit:CalculateThreatLevelA2G() - self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 20, "MSOD", Persist ) + self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 20, "MOD", Persist ) end From bd290ffdab937b857087d02cd737843f12240af9 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 23 Mar 2019 11:24:04 +0100 Subject: [PATCH 233/485] Fixing hit problem in multi player for database when hitting static objects. --- Moose Development/Moose/Core/Database.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 1a3d7c7e5..5a07d5f5a 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -1330,7 +1330,7 @@ end -- What is he hitting? if Event.TgtCategory then - if Event.IniCoalition then -- A coalition object was hit, probably a static. + if Event.WeaponCoalition then -- A coalition object was hit, probably a static. -- A target got hit self.HITS[Event.TgtUnitName] = self.HITS[Event.TgtUnitName] or {} local Hit = self.HITS[Event.TgtUnitName] From de0c8e37893876b4c9997e1ef69eb8526e374b16 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 27 Mar 2019 16:23:39 +0100 Subject: [PATCH 234/485] AB 0.996w --- Moose Development/Moose/Core/Spawn.lua | 23 +++++++++++-------- Moose Development/Moose/Ops/Airboss.lua | 10 +++++++- .../Moose/Ops/RecoveryTanker.lua | 2 +- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index d65aae7a2..2655bfcdc 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1470,12 +1470,20 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) -- Template group, unit and its attributes. - local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) - local TemplateUnit=TemplateGroup:GetUnit(1) + --local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) + --local TemplateUnit=TemplateGroup:GetUnit(1) + --local ishelo=TemplateUnit:HasAttribute("Helicopters") + --local isbomber=TemplateUnit:HasAttribute("Bombers") + --local istransport=TemplateUnit:HasAttribute("Transports") + --local isfighter=TemplateUnit:HasAttribute("Battleplanes") + + local group=TemplateGroup + local istransport=group:HasAttribute("Transports") and group:HasAttribute("Planes") + local isawacs=group:HasAttribute("AWACS") + local isfighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers")) + local isbomber=group:HasAttribute("Strategic bombers") + local istanker=group:HasAttribute("Tankers") local ishelo=TemplateUnit:HasAttribute("Helicopters") - local isbomber=TemplateUnit:HasAttribute("Bombers") - local istransport=TemplateUnit:HasAttribute("Transports") - local isfighter=TemplateUnit:HasAttribute("Battleplanes") -- Number of units in the group. With grouping this can actually differ from the template group size! local nunits=#SpawnTemplate.units @@ -1585,10 +1593,7 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT else -- Fixed wing aircraft is spawned. if termtype==nil then - --TODO: Add some default cases for transport, bombers etc. if no explicit terminal type is provided. - --TODO: We don't want Bombers to spawn in shelters. But I don't know a good attribute for just fighers. - --TODO: Some attributes are "Helicopters", "Bombers", "Transports", "Battleplanes". Need to check it out. - if isbomber or istransport then + if isbomber or istransport or istanker or isawacs then -- First we fill the potentially bigger spots. self:T(string.format("Transport/bomber group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.OpenBig)) spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.OpenBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 8d3623ddf..76a63905d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -9055,9 +9055,17 @@ function AIRBOSS:_Break(playerData, part) self:_AbortPattern(playerData, X, Z, breakpoint, true) return end + + -- Player made a very tight turn and did not trigger the latebreak threshold at 0.8 NM. + local tooclose=false + if part==AIRBOSS.PatternStep.LATEBREAK then + if X<0 and Z Date: Wed, 27 Mar 2019 19:18:12 +0100 Subject: [PATCH 235/485] New Zone Detection module. --- .../Moose/Core/Zone_Detection.lua | 203 ++++++++++++++++++ .../Moose/Functional/Detection.lua | 15 ++ Moose Development/Moose/Modules.lua | 1 + 3 files changed, 219 insertions(+) create mode 100644 Moose Development/Moose/Core/Zone_Detection.lua diff --git a/Moose Development/Moose/Core/Zone_Detection.lua b/Moose Development/Moose/Core/Zone_Detection.lua new file mode 100644 index 000000000..96a508c0e --- /dev/null +++ b/Moose Development/Moose/Core/Zone_Detection.lua @@ -0,0 +1,203 @@ + +--- The ZONE_DETECTION class, defined by a zone name, a detection object and a radius. +-- @type ZONE_DETECTION +-- @field DCS#Vec2 Vec2 The current location of the zone. +-- @field DCS#Distance Radius The radius of the zone. +-- @extends #ZONE_BASE + +--- The ZONE_DETECTION class defined by a zone name, a location and a radius. +-- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. +-- +-- ## ZONE_DETECTION constructor +-- +-- * @{#ZONE_DETECTION.New}(): Constructor. +-- +-- @field #ZONE_DETECTION +ZONE_DETECTION = { + ClassName="ZONE_DETECTION", + } + +--- Constructor of @{#ZONE_DETECTION}, taking the zone name, the zone location and a radius. +-- @param #ZONE_DETECTION self +-- @param #string ZoneName Name of the zone. +-- @param Functional.Detection#DETECTION_BASE Detection The detection object defining the locations of the central detections. +-- @param DCS#Distance Radius The radius around the detections defining the combined zone. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:New( ZoneName, Detection, Radius ) + local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_DETECTION + self:F( { ZoneName, Detection, Radius } ) + + self.Detection = Detection + self.Radius = Radius + + return self +end + +--- Bounds the zone with tires. +-- @param #ZONE_DETECTION self +-- @param #number Points (optional) The amount of points in the circle. Default 360. +-- @param DCS#country.id CountryID The country id of the tire objects, e.g. country.id.USA for blue or country.id.RUSSIA for red. +-- @param #boolean UnBound (Optional) If true the tyres will be destroyed. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:BoundZone( Points, CountryID, UnBound ) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + -- + for Angle = 0, 360, (360 / Points ) do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + + local CountryName = _DATABASE.COUNTRY_NAME[CountryID] + + local Tire = { + ["country"] = CountryName, + ["category"] = "Fortifications", + ["canCargo"] = false, + ["shape_name"] = "H-tyre_B_WF", + ["type"] = "Black_Tyre_WF", + --["unitId"] = Angle + 10000, + ["y"] = Point.y, + ["x"] = Point.x, + ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ), + ["heading"] = 0, + } -- end of ["group"] + + local Group = coalition.addStaticObject( CountryID, Tire ) + if UnBound and UnBound == true then + Group:destroy() + end + end + + return self +end + + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_DETECTION self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Points (optional) The amount of points in the circle. +-- @param #number AddHeight (optional) The height to be added for the smoke. +-- @param #number AddOffSet (optional) The angle to be added for the smoking start position. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:SmokeZone( SmokeColor, Points, AddHeight, AngleOffset ) + self:F2( SmokeColor ) + + local Point = {} + local Vec2 = self:GetVec2() + + AddHeight = AddHeight or 0 + AngleOffset = AngleOffset or 0 + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, 360 / Points do + local Radial = ( Angle + AngleOffset ) * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + POINT_VEC2:New( Point.x, Point.y, AddHeight ):Smoke( SmokeColor ) + end + + return self +end + + +--- Flares the zone boundaries in a color. +-- @param #ZONE_DETECTION self +-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Points (optional) The amount of points in the circle. +-- @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_DETECTION self +function ZONE_DETECTION:FlareZone( FlareColor, Points, Azimuth, AddHeight ) + self:F2( { FlareColor, Azimuth } ) + + local Point = {} + local Vec2 = self:GetVec2() + + AddHeight = AddHeight or 0 + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, 360 / Points do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + POINT_VEC2:New( Point.x, Point.y, AddHeight ):Flare( FlareColor, Azimuth ) + end + + return self +end + +--- Returns the radius around the detected locations defining the combine zone. +-- @param #ZONE_DETECTION self +-- @return DCS#Distance The radius. +function ZONE_DETECTION:GetRadius() + self:F2( self.ZoneName ) + + self:T2( { self.Radius } ) + + return self.Radius +end + +--- Sets the radius around the detected locations defining the combine zone. +-- @param #ZONE_DETECTION self +-- @param DCS#Distance Radius The radius. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:SetRadius( Radius ) + self:F2( self.ZoneName ) + + self.Radius = Radius + self:T2( { self.Radius } ) + + return self.Radius +end + + + +--- Returns if a location is within the zone. +-- @param #ZONE_DETECTION self +-- @param DCS#Vec2 Vec2 The location to test. +-- @return #boolean true if the location is within the zone. +function ZONE_DETECTION:IsVec2InZone( Vec2 ) + self:F2( Vec2 ) + + local Coordinates = self.Detection:GetDetectedItemCoordinates() -- This returns a list of coordinates that define the (central) locations of the detections. + + for CoordinateID, Coordinate in pairs( Coordinates ) do + local ZoneVec2 = Coordinate:GetVec2() + if ZoneVec2 then + if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then + return true + end + end + end + + return false +end + +--- Returns if a point is within the zone. +-- @param #ZONE_DETECTION self +-- @param DCS#Vec3 Vec3 The point to test. +-- @return #boolean true if the point is within the zone. +function ZONE_DETECTION:IsVec3InZone( Vec3 ) + self:F2( Vec3 ) + + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) + + return InZone +end + diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 99f98848b..c3d0c21a3 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1826,6 +1826,21 @@ do -- DETECTION_BASE return nil end + --- Get a list of the detected item coordinates. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem to set the coordinate at. + -- @return Core.Point#COORDINATE + function DETECTION_BASE:GetDetectedItemCoordinates() + + local Coordinates = {} + + for DetectedItemID, DetectedItem in pairs( self:GetDetectedItems() ) do + Coordinates[DetectedItem] = self:GetDetectedItemCoordinate( DetectedItem ) + end + + return Coordinates + end + --- Set the detected item threatlevel. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem The DetectedItem to calculate the threatlevel for. diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 162e605fd..8a86a02f9 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -11,6 +11,7 @@ __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/Zone_Detection.lua' ) __Moose.Include( 'Scripts/Moose/Core/Database.lua' ) __Moose.Include( 'Scripts/Moose/Core/Set.lua' ) __Moose.Include( 'Scripts/Moose/Core/Point.lua' ) From b7b6034d76b6dbf6df1ba51206c876073c0d360f Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 27 Mar 2019 23:29:41 +0100 Subject: [PATCH 236/485] AIRBOSS v0.9.9.6w --- Moose Development/Moose/Core/Spawn.lua | 5 +- Moose Development/Moose/Ops/Airboss.lua | 63 +++++++++++++++++++ .../Moose/Ops/RecoveryTanker.lua | 2 +- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 2655bfcdc..7b25c5367 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1470,8 +1470,9 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) -- Template group, unit and its attributes. - --local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) - --local TemplateUnit=TemplateGroup:GetUnit(1) + local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) + local TemplateUnit=TemplateGroup:GetUnit(1) + --local ishelo=TemplateUnit:HasAttribute("Helicopters") --local isbomber=TemplateUnit:HasAttribute("Bombers") --local istransport=TemplateUnit:HasAttribute("Transports") diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index aa5166440..6ce88e199 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -4068,6 +4068,69 @@ function AIRBOSS:_InitTarawa() end +--- Init parameters for Marshal Voice overs *Gabriella* by HighwaymanEd. +-- @param #AIRBOSS self +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversMarshalByGabriella(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderMSH=mizfolder + else + -- Default is the general folder. + self.soundfolderMSH=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("Marshal Gabriella reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) + + self.MarshalCall.AFFIRMATIVE.duration=0.65 + self.MarshalCall.ALTIMETER.duration=0.60 + self.MarshalCall.BRC.duration=0.67 + self.MarshalCall.CARRIERTURNTOHEADING.duration=1.62 + self.MarshalCall.CASE.duration=0.30 + self.MarshalCall.CHARLIETIME.duration=0.77 + self.MarshalCall.CLEAREDFORRECOVERY.duration=0.93 + self.MarshalCall.DECKCLOSED.duration=0.73 + self.MarshalCall.DEGREES.duration=0.48 + self.MarshalCall.EXPECTED.duration=0.50 + self.MarshalCall.FLYNEEDLES.duration=0.89 + self.MarshalCall.HOLDATANGELS.duration=0.81 + self.MarshalCall.HOURS.duration=0.41 + self.MarshalCall.MARSHALRADIAL.duration=0.95 + self.MarshalCall.N0.duration=0.41 + self.MarshalCall.N1.duration=0.30 + self.MarshalCall.N2.duration=0.34 + self.MarshalCall.N3.duration=0.31 + self.MarshalCall.N4.duration=0.34 + self.MarshalCall.N5.duration=0.30 + self.MarshalCall.N6.duration=0.33 + self.MarshalCall.N7.duration=0.38 + self.MarshalCall.N8.duration=0.35 + self.MarshalCall.N9.duration=0.35 + self.MarshalCall.NEGATIVE.duration=0.60 + self.MarshalCall.NEWFB.duration=0.95 + self.MarshalCall.OPS.duration=0.23 + self.MarshalCall.POINT.duration=0.38 + self.MarshalCall.RADIOCHECK.duration=1.27 + self.MarshalCall.RECOVERY.duration=0.60 + self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.25 + self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.55 + self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=2.55 + self.MarshalCall.REPORTSEEME.duration=0.87 + self.MarshalCall.RESUMERECOVERY.duration=1.55 + self.MarshalCall.ROGER.duration=0.50 + self.MarshalCall.SAYNEEDLES.duration=0.82 + self.MarshalCall.STACKFULL.duration=5.70 + self.MarshalCall.STARTINGRECOVERY.duration=1.61 + +end + + --- Init parameters for Marshal Voice overs by *Raynor*. -- @param #AIRBOSS self diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 68a026276..1ad33047f 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -884,7 +884,7 @@ function RECOVERYTANKER:onafterStart(From, Event, To) else -- Spawn tanker at airbase. - self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, AIRBASE.TerminalType.OpenBig) + self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) end From 5f58fd3550ca2b0b2760ec65dc2611750b1df384 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 28 Mar 2019 16:35:38 +0100 Subject: [PATCH 237/485] AB 0996w --- Moose Development/Moose/Ops/Airboss.lua | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 6ce88e199..ac1a451f4 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -5655,16 +5655,21 @@ function AIRBOSS:_ClearForLanding(flight) self:_RemoveFlightFromMarshalQueue(flight, false) self:_LandAI(flight) + -- Cleared for Case X recovery. + self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) + else + + -- Cleared for Case X recovery. + if flight.step~=AIRBOSS.PatternStep.COMMENCING then + self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) + end -- 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 - - -- Cleared for Case X recovery. - self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) - + end --- Set player step. Any warning is erased and next step hint shown. @@ -9126,7 +9131,11 @@ function AIRBOSS:_Break(playerData, part) -- Player made a very tight turn and did not trigger the latebreak threshold at 0.8 NM. local tooclose=false if part==AIRBOSS.PatternStep.LATEBREAK then - if X<0 and Z Date: Sun, 31 Mar 2019 21:55:30 +0200 Subject: [PATCH 238/485] AIRBOSS v0.9.9.7 --- Moose Development/Moose/Ops/Airboss.lua | 98 +++++++++++++++++++++---- 1 file changed, 84 insertions(+), 14 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index ac1a451f4..54576db9b 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1664,7 +1664,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.6" +AIRBOSS.version="0.9.9.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -5486,7 +5486,15 @@ function AIRBOSS:_GetNextMarshalFight() -- Check if conditions are right. if stack==1 and flight.holding~=nil and Tmarshal>=TmarshalMin then - return flight + if flight.ai then + -- Return AI flight. + return flight + else + -- Check for human player if they are already commencing. + if flight.step~=AIRBOSS.PatternStep.COMMENCING then + return flight + end + end end end @@ -5663,6 +5671,7 @@ function AIRBOSS:_ClearForLanding(flight) -- Cleared for Case X recovery. if flight.step~=AIRBOSS.PatternStep.COMMENCING then self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) + flight.time=timer.getAbsTime() end -- Set step to commencing. This will trigger the zone check until the player is in the right place. @@ -8771,6 +8780,14 @@ function AIRBOSS:_Commencing(playerData, zonecheck) -- Skip the rest if not in the zone yet. if not inzone then + + -- Friendly reminder. + if timer.getAbsTime()-playerData.time>180 then + self:_MarshalCallClearedForRecovery(playerData.onboard, playerData.case) + playerData.time=timer.getAbsTime() + end + + -- Skip the rest. return end @@ -9136,6 +9153,9 @@ function AIRBOSS:_Break(playerData, part) close=0.5 end if X<0 and Z Date: Mon, 1 Apr 2019 18:17:52 +0200 Subject: [PATCH 239/485] AIBOSS v0.9.9.7 --- Moose Development/Moose/Ops/Airboss.lua | 45 ++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 54576db9b..4123afb15 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -15375,10 +15375,46 @@ function AIRBOSS:_RequestEmergency(_unitName) -- Mission designer did not allow emergency landing. text="negative, no emergency landings on my carrier. We are currently busy. See how you get along!" - elseif not _unit:InAir() then + elseif not _unit:InAir() then + + -- Carrier zone. + local zone=self:_GetZoneCarrierBox() + + -- Check if player is on the carrier. + if playerData.unit:IsInZone(zone) then + + -- Bolter pattern. + text="roger, you are now technically in the bolter pattern. Your next step after takeoff is abeam!" + + -- Get flight lead. + local lead=self:_GetFlightLead(playerData) + + -- Set set for lead. + self:_SetPlayerStep(lead, AIRBOSS.PatternStep.BOLTER) + + -- Also set bolter pattern for all members. + for _,sec in pairs(lead.section) do + local sectionmember=sec --#AIRBOSS.PlayerData + self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.BOLTER) + end + + -- Remove flight from waiting queue just in case. + self:_RemoveFlightFromQueue(self.Qwaiting, lead) + + if self:_InQueue(self.Qmarshal, lead.group) then + -- Remove flight from Marshal queue and add to pattern. + self:_RemoveFlightFromMarshalQueue(lead) + else + -- Add flight to pattern if he was not. + if not self:_InQueue(self.Qpattern, lead.group) then + self:_AddFlightToPatternQueue(lead) + end + end - -- Flight group is not in air - text=string.format("negative, you are not airborne. Request denied!") + else + -- Flight group is not in air. + text=string.format("negative, you are not airborne. Request denied!") + end else @@ -15399,8 +15435,7 @@ function AIRBOSS:_RequestEmergency(_unitName) -- Remove flight from spinning queue just in case (everone can spin on his own). self:_RemoveFlightFromQueue(self.Qspinning, sectionmember) end - - + -- Remove flight from waiting queue just in case. self:_RemoveFlightFromQueue(self.Qwaiting, lead) From 70f30d2c53c3648d3c4698ba90f84ed113fd67b8 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 1 Apr 2019 18:28:17 +0200 Subject: [PATCH 240/485] AIBOSS v0.9.9.7 --- 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 4123afb15..b9332e85d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -9016,11 +9016,11 @@ function AIRBOSS:_DirtyUp(playerData) self:_PlayerHint(playerData) -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. - if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then + if playerData.actype==AIRBOSS.AircraftCarrier.HORNET or playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then local callsay=self:_NewRadioCall(self.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard) local callfly=self:_NewRadioCall(self.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard) - self:RadioTransmission(self.MarshalRadio, callsay, false, 50, nil, true) - self:RadioTransmission(self.MarshalRadio, callfly, false, 55, nil, true) + self:RadioTransmission(self.MarshalRadio, callsay, false, 55, nil, true) + self:RadioTransmission(self.MarshalRadio, callfly, false, 60, nil, true) end -- TODO: Make Fly Bullseye call if no automatic ICLS is active. From 4bcf43122f3906efde94cf0609246716671e7ca6 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 2 Apr 2019 16:27:41 +0200 Subject: [PATCH 241/485] AB --- Moose Development/Moose/Ops/Airboss.lua | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index b9332e85d..5fe985d33 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -376,6 +376,9 @@ -- -- All section members are supposed to follow. Player (or section lead) is removed from all other queues and automatically added to the landing pattern queue. -- +-- If this command is called while the player is currently on the carrier, he will be put in the bolter pattern. So the next expected step after take of +-- is the abeam position. This allows for quick landing training exercises without having to go through the whole pattern. +-- -- The mission designer can forbid this option my setting @{#AIRBOSS.SetEmergencyLandings}(false) in the script. -- -- ### [Reset My Status] @@ -13187,11 +13190,17 @@ function AIRBOSS:_GetETAatNextWP() -- Current velocity [m/s]. local v=self.carrier:GetVelocityMPS() + -- Next waypoint. + local nextWP=self:_GetNextWaypoint() + -- Distance to next waypoint. - local s=0 - if #self.waypoints>cwp then - s=p:Get2DDistance(self.waypoints[cwp+1]) - end + local s=p:Get2DDistance(nextWP) + + -- 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 From 6593a262bb55ac4ecb2451c4b920d6e647e0805a Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 3 Apr 2019 23:36:22 +0200 Subject: [PATCH 242/485] Update Airboss.lua --- Moose Development/Moose/Ops/Airboss.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 5fe985d33..d49aff897 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -6047,7 +6047,13 @@ function AIRBOSS:_MarshalAI(flight, nstack, respawn) -- Nil check. if flight==nil or flight.group==nil then - self:E(self.lid.."ERROR: flight or flight.group is nil") + self:E(self.lid.."ERROR: flight or flight.group is nil.") + return + end + + -- Nil check. + if flight.group:GetCoordinate()==nil then + self:E(self.lid.."ERROR: cannot get coordinate of flight group.") return end From 4abdad5f35cc6683f06fc2a9ce24fae5158e41d3 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 6 Apr 2019 13:04:28 +0200 Subject: [PATCH 243/485] First version --- Moose Development/Moose/AI/AI_Escort.lua | 1089 +++++++++++++++++++ Moose Development/Moose/AI/AI_Formation.lua | 235 ++-- Moose Development/Moose/Modules.lua | 1 + 3 files changed, 1225 insertions(+), 100 deletions(-) create mode 100644 Moose Development/Moose/AI/AI_Escort.lua diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua new file mode 100644 index 000000000..0c4c6f8af --- /dev/null +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -0,0 +1,1089 @@ +--- **Functional** -- Taking the lead of AI escorting your flight or of other AI. +-- +-- === +-- +-- ## Features: +-- +-- * Escort navigation commands. +-- * Escort hold at position commands. +-- * Escorts reporting detected targets. +-- * Escorts scanning targets in advance. +-- * Escorts attacking specific targets. +-- * Request assistance from other groups for attack. +-- * Manage rule of engagement of escorts. +-- * Manage the allowed evasion techniques of escorts. +-- * Make escort to execute a defined mission or path. +-- * Escort tactical situation reporting. +-- +-- === +-- +-- ## Missions: +-- +-- [ESC - Escorting](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ESC%20-%20Escorting) +-- +-- === +-- +-- Allows you to interact with escorting AI on your flight and take the lead. +-- +-- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10). +-- +-- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes. +-- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. +-- +-- # RADIO MENUs that can be created: +-- +-- Find a summary below of the current available commands: +-- +-- ## Navigation ...: +-- +-- Escort group navigation functions: +-- +-- * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you. +-- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. +-- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. +-- +-- ## Hold position ...: +-- +-- Escort group navigation functions: +-- +-- * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. +-- * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. +-- +-- ## Report targets ...: +-- +-- Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). +-- +-- * **"Report now":** Will report the current detected targets. +-- * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list. +-- * **"Report targets off":** Will stop detecting targets. +-- +-- ## Scan targets ...: +-- +-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. +-- +-- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. +-- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. +-- +-- ## Attack targets ...: +-- +-- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. +-- +-- ## Request assistance from ...: +-- +-- This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. +-- This menu item allows to request attack support from other escorts supporting the current client group. +-- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. +-- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. +-- +-- ## ROE ...: +-- +-- Sets the Rules of Engagement (ROE) of the escort group when in flight. +-- +-- * **"Hold Fire":** The escort group will hold fire. +-- * **"Return Fire":** The escort group will return fire. +-- * **"Open Fire":** The escort group will open fire on designated targets. +-- * **"Weapon Free":** The escort group will engage with any target. +-- +-- ## Evasion ...: +-- +-- Will define the evasion techniques that the escort group will perform during flight or combat. +-- +-- * **"Fight until death":** The escort group will have no reaction to threats. +-- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. +-- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. +-- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. +-- +-- ## Resume Mission ...: +-- +-- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. +-- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. +-- +-- === +-- +-- ### Authors: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Escort +-- @image Escorting.JPG + + + +--- @type AI_ESCORT +-- @extends AI.AI_Formation#AI_FORMATION +-- @field Wrapper.Client#CLIENT EscortUnit +-- @field Wrapper.Group#GROUP EscortGroup +-- @field #string EscortName +-- @field #AI_ESCORT.MODE EscortMode The mode the escort is in. +-- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. +-- @field #number FollowDistance The current follow distance. +-- @field #boolean ReportTargets If true, nearby targets are reported. +-- @Field DCS#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. +-- @field DCS#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. +-- @field FunctionalMENU_GROUPDETECTION_BASE Detection + +--- AI_ESCORT class +-- +-- # AI_ESCORT construction methods. +-- +-- Create a new SPAWN object with the @{#AI_ESCORT.New} method: +-- +-- * @{#AI_ESCORT.New}: Creates a new AI_ESCORT object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text. +-- +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = AI_ESCORT:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +-- +-- @field #AI_ESCORT +AI_ESCORT = { + ClassName = "AI_ESCORT", + EscortName = nil, -- The Escort Name + EscortUnit = nil, + EscortGroup = nil, + EscortMode = 1, + MODE = { + FOLLOW = 1, + MISSION = 2, + }, + Targets = {}, -- The identified targets + FollowScheduler = nil, + ReportTargets = true, + OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, + OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, + SmokeDirectionVector = false, + TaskPoints = {} +} + +--- AI_ESCORT.Mode class +-- @type AI_ESCORT.MODE +-- @field #number FOLLOW +-- @field #number MISSION + +--- MENUPARAM type +-- @type MENUPARAM +-- @field #AI_ESCORT ParamSelf +-- @field #Distance ParamDistance +-- @field #function ParamFunction +-- @field #string ParamMessage + +--- AI_ESCORT class constructor for an AI group +-- @param #AI_ESCORT self +-- @param Wrapper.Client#CLIENT EscortUnit The client escorted by the EscortGroup. +-- @param Core.Set#SET_GROUP EscortGroupSet The set of group AI escorting the EscortUnit. +-- @param #string EscortName Name of the escort. +-- @param #string EscortBriefing A text showing the AI_ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #AI_ESCORT self +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = AI_ESCORT:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) + + local self = BASE:Inherit( self, AI_FORMATION:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT + self:F( { EscortUnit, EscortGroupSet } ) + + self.EscortUnit = self.FollowUnit -- Wrapper.Unit#UNIT + self.EscortGroupSet = EscortGroupSet + self.EscortBriefing = EscortBriefing + + + + + +-- if not EscortBriefing then +-- EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. +-- "We're escorting your flight. " .. +-- "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", +-- 60, EscortUnit +-- ) +-- else +-- EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") " .. EscortBriefing, +-- 60, EscortUnit +-- ) +-- end + + self.FollowDistance = 100 + self.CT1 = 0 + self.GT1 = 0 + + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName() ) + + -- Set EscortGroup known at EscortUnit. + if not self.EscortUnit._EscortGroups then + self.EscortUnit._EscortGroups = {} + end + + if not self.EscortUnit._EscortGroups[EscortGroup:GetName()] then + self.EscortUnit._EscortGroups[EscortGroup:GetName()] = {} + self.EscortUnit._EscortGroups[EscortGroup:GetName()].EscortGroup = EscortGroup + self.EscortUnit._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName + self.EscortUnit._EscortGroups[EscortGroup:GetName()].Detection = self.Detection + end + + EscortGroup.EscortMode = AI_ESCORT.MODE.FOLLOW + end + ) + + self.Detection = DETECTION_AREAS:New( EscortGroupSet, 5000 ) + + self.Detection:Start() + + return self +end + +function AI_ESCORT:onafterStart( EscortGroupSet ) + + self:E("Start") + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName() ) + EscortGroup:WayPointInitialize( 1 ) + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEOpenFire() + end + ) + +end + +--- Set a Detection method for the EscortUnit to be reported upon. +-- Detection methods are based on the derived classes from DETECTION_BASE. +-- @param #AI_ESCORT self +-- @param Function.Detection#DETECTION_BASE Detection +function AI_ESCORT:SetDetection( Detection ) + + self.Detection = Detection + self.EscortGroup.Detection = self.Detection + self.EscortUnit._EscortGroups[self.EscortGroup:GetName()].Detection = self.EscortGroup.Detection + + Detection:__Start( 1 ) + +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_ESCORT self +-- @param #boolean SmokeDirection If true, then the direction vector will be smoked. +function AI_ESCORT:TestSmokeDirectionVector( SmokeDirection ) + self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false +end + + +--- Defines the default menus +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:Menus() + self:F() + +-- self:MenuScanForTargets( 100, 60 ) + + self:MenuHoldAtEscortPosition( 1000, 500 ) + self:MenuHoldAtLeaderPosition( 1000, 500 ) + + self:MenuFlare() + self:MenuSmoke() + + self:MenuReportTargets( 60 ) + self:MenuAssistedAttack() + self:MenuROE() + self:MenuEvasion() + +-- self:MenuResumeMission() + + + return self +end + + + + +--- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Hold position**. +-- @param #AI_ESCORT self +-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Speed Optional parameter that lets the escort orbit with a specified speed. The default value is a speed that is average for the type of airplane or helicopter. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) + self:F( { Height, Speed, MenuTextFormat } ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + + if not EscortGroup.EscortMenuHold then + EscortGroup.EscortMenuHold = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold position", EscortGroup.EscortMenu ) + end + + if not Height then + Height = 30 + end + + if not Speed then + Speed = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Speed == 0 then + MenuText = string.format( "Hold at %d meter", Height ) + else + MenuText = string.format( "Hold at %d meter at %d", Height, Speed ) + end + else + if Speed == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Speed ) + end + end + + if not EscortGroup.EscortMenuHoldPosition then + EscortGroup.EscortMenuHoldPosition = {} + end + + EscortGroup.EscortMenuHoldPosition[#EscortGroup.EscortMenuHoldPosition+1] = MENU_GROUP_COMMAND + :New( + self.EscortUnit:GetGroup(), + MenuText, + EscortGroup.EscortMenuHold, + AI_ESCORT._HoldPosition, + self, + EscortGroup, + EscortGroup, + Height, + Speed + ) + end + end + ) + + return self +end + + +--- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Navigation**. +-- @param #AI_ESCORT self +-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Speed Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) + self:F( { Height, Speed, MenuTextFormat } ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + + if not self.EscortMenuHold then + self.EscortMenuHold = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold position", EscortGroup.EscortMenu ) + end + + if not Height then + Height = 30 + end + + if not Speed then + Speed = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Speed == 0 then + MenuText = string.format( "Rejoin and hold at %d meter", Height ) + else + MenuText = string.format( "Rejoin and hold at %d meter at %d", Height, Speed ) + end + else + if Speed == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Speed ) + end + end + + if not self.EscortMenuHoldAtLeaderPosition then + self.EscortMenuHoldAtLeaderPosition = {} + end + + self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1] = MENU_GROUP_COMMAND + :New( + self.EscortUnit:GetGroup(), + MenuText, + self.EscortMenuHold, + AI_ESCORT._HoldPosition, + self, + self.EscortUnit:GetGroup(), + EscortGroup, + Height, + Speed + ) + end + end + ) + + return self +end + +--- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. +-- This menu will appear under **Scan targets**. +-- @param #AI_ESCORT self +-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuScan then + self.EscortMenuScan = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Scan for targets", self.EscortMenu ) + end + + if not Height then + Height = 100 + end + + if not Seconds then + Seconds = 30 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "At %d meter", Height ) + else + MenuText = string.format( "At %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuScanForTargets then + self.EscortMenuScanForTargets = {} + end + + self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_GROUP_COMMAND + :New( + self.EscortUnit:GetGroup(), + MenuText, + self.EscortMenuScan, + AI_ESCORT._ScanTargets, + self, + 30 + ) + end + + return self +end + + + +--- Defines a menu slot to let the escort disperse a flare in a certain color. +-- This menu will appear under **Navigation**. +-- The flare will be fired from the first unit in the group. +-- @param #AI_ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFlare( MenuTextFormat ) + self:F() + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if not EscortGroup.EscortMenuReportNavigation then + EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Flare" + else + MenuText = MenuTextFormat + end + + if not EscortGroup.EscortMenuFlare then + EscortGroup.EscortMenuFlare = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, EscortGroup.EscortMenuReportNavigation ) + EscortGroup.EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release green flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green, "Released a green flare!" ) + EscortGroup.EscortMenuFlareRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release red flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Red, "Released a red flare!" ) + EscortGroup.EscortMenuFlareWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release white flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.White, "Released a white flare!" ) + EscortGroup.EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release yellow flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Yellow, "Released a yellow flare!" ) + end + end + ) + + return self +end + +--- Defines a menu slot to let the escort disperse a smoke in a certain color. +-- This menu will appear under **Navigation**. +-- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. +-- The smoke will be fired from the first unit in the group. +-- @param #AI_ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuSmoke( MenuTextFormat ) + self:F() + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if not EscortGroup:IsAir() then + if not EscortGroup.EscortMenuReportNavigation then + EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Smoke" + else + MenuText = MenuTextFormat + end + + if not EscortGroup.EscortMenuSmoke then + EscortGroup.EscortMenuSmoke = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Smoke", EscortGroup.EscortMenuReportNavigation ) + EscortGroup.EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release green smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green, "Releasing green smoke!" ) + EscortGroup.EscortMenuSmokeRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release red smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Red, "Releasing red smoke!" ) + EscortGroup.EscortMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release white smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.White, "Releasing white smoke!" ) + EscortGroup.EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release orange smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + EscortGroup.EscortMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release blue smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Blue, "Releasing blue smoke!" ) + end + end + end + ) + + return self +end + +--- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. +-- This menu will appear under **Report targets**. +-- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. +-- @param #AI_ESCORT self +-- @param DCS#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. +-- @return #AI_ESCORT +function AI_ESCORT:MenuReportTargets( Seconds ) + self:F( { Seconds } ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if not EscortGroup.EscortMenuReportNearbyTargets then + EscortGroup.EscortMenuReportNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Report targets", EscortGroup.EscortMenu ) + end + + if not Seconds then + Seconds = 30 + end + + -- Report Targets + EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets now!", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup ) + EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) + EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) + + -- Attack Targets + EscortGroup.EscortMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Attack targets", EscortGroup.EscortMenu ) + + + EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, 1, Seconds ) + end + ) + + return self +end + +--- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. +-- This menu will appear under **Request assistance from**. +-- Note that this method needs to be preceded with the method MenuReportTargets. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuAssistedAttack() + self:F() + + -- Request assistance from other escorts. + -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... + self.EscortMenuTargetAssistance = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Request assistance from", self.EscortMenu ) + + return self +end + +--- Defines a menu to let the escort set its rules of engagement. +-- All rules of engagement will appear under the menu **ROE**. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuROE( MenuTextFormat ) + self:F( MenuTextFormat ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if not EscortGroup.EscortMenuROE then + -- Rules of Engagement + EscortGroup.EscortMenuROE = MENU_GROUP:New( self.EscortUnit:GetGroup(), "ROE", EscortGroup.EscortMenu ) + if EscortGroup:OptionROEHoldFirePossible() then + EscortGroup.EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Hold Fire", EscortGroup.EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup:OptionROEHoldFire(), "Holding weapons!" ) + end + if EscortGroup:OptionROEReturnFirePossible() then + EscortGroup.EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Return Fire", EscortGroup.EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup:OptionROEReturnFire(), "Returning fire!" ) + end + if EscortGroup:OptionROEOpenFirePossible() then + EscortGroup.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Open Fire", EscortGroup.EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup:OptionROEOpenFire(), "Opening fire on designated targets!!" ) + end + if EscortGroup:OptionROEWeaponFreePossible() then + EscortGroup.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Weapon Free", EscortGroup.EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup:OptionROEWeaponFree(), "Opening fire on targets of opportunity!" ) + end + end + end + ) + + return self +end + + +--- Defines a menu to let the escort set its evasion when under threat. +-- All rules of engagement will appear under the menu **Evasion**. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuEvasion( MenuTextFormat ) + self:F( MenuTextFormat ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + if not EscortGroup.EscortMenuEvasion then + -- Reaction to Threats + EscortGroup.EscortMenuEvasion = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Evasion", EscortGroup.EscortMenu ) + if EscortGroup:OptionROTNoReactionPossible() then + EscortGroup.EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Fight until death", EscortGroup.EscortMenuEvasion, AI_ESCORT._ROT, self, EscortGroup, EscortGroup:OptionROTNoReaction(), "Fighting until death!" ) + end + if EscortGroup:OptionROTPassiveDefensePossible() then + EscortGroup.EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Use flares, chaff and jammers", EscortGroup.EscortMenuEvasion, AI_ESCORT._ROT, self, EscortGroup, EscortGroup:OptionROTPassiveDefense(), "Defending using jammers, chaff and flares!" ) + end + if EscortGroup:OptionROTEvadeFirePossible() then + EscortGroup.EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Evade enemy fire", EscortGroup.EscortMenuEvasion, AI_ESCORT._ROT, self, EscortGroup, EscortGroup:OptionROTEvadeFire(), "Evading on enemy fire!" ) + end + if EscortGroup:OptionROTVerticalPossible() then + EscortGroup.EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Go below radar and evade fire", EscortGroup.EscortMenuEvasion, AI_ESCORT._ROT, self, EscortGroup, EscortGroup:OptionROTVertical(), "Evading on enemy fire with vertical manoeuvres!" ) + end + end + end + end + ) + + return self +end + +--- Defines a menu to let the escort resume its mission from a waypoint on its route. +-- All rules of engagement will appear under the menu **Resume mission from**. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuResumeMission() + self:F() + + if not self.EscortMenuResumeMission then + -- Mission Resume Menu Root + self.EscortMenuResumeMission = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Resume mission from", self.EscortMenu ) + end + + return self +end + + +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP OrbitGroup +-- @param Wrapper.Group#GROUP EscortGroup +-- @param #number OrbitHeight +-- @param #number OrbitSeconds +function AI_ESCORT:_HoldPosition( OrbitGroup, EscortGroup, OrbitHeight, OrbitSeconds ) + + local EscortUnit = self.EscortUnit + + local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT + + self:ReleaseFormation( EscortGroup ) + + local PointFrom = {} + local GroupVec3 = EscortGroup:GetUnit(1):GetVec3() + PointFrom = {} + PointFrom.x = GroupVec3.x + PointFrom.y = GroupVec3.z + PointFrom.speed = 250 + PointFrom.type = AI.Task.WaypointType.TURNING_POINT + PointFrom.alt = GroupVec3.y + PointFrom.alt_type = AI.Task.AltitudeType.BARO + + local OrbitPoint = OrbitUnit:GetVec2() + local PointTo = {} + PointTo.x = OrbitPoint.x + PointTo.y = OrbitPoint.y + PointTo.speed = 250 + PointTo.type = AI.Task.WaypointType.TURNING_POINT + PointTo.alt = OrbitHeight + PointTo.alt_type = AI.Task.AltitudeType.BARO + PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) + + local Points = { PointFrom, PointTo } + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTPassiveDefense() + + EscortGroup:SetTask( EscortGroup:TaskRoute( Points ), 1 ) + EscortGroup:MessageTypeToGroup( "Orbiting at current location.", MESSAGE.Type.Information, EscortUnit:GetGroup() ) + +end + +--- @param #MENUPARAM MenuParam +function AI_ESCORT:_JoinUpAndFollow( Distance ) + + local EscortGroup = self.EscortGroup + local EscortUnit = self.EscortUnit + + self.Distance = Distance + + self:JoinUpAndFollow( EscortGroup, EscortUnit, self.Distance ) +end + + +--- @param #MENUPARAM MenuParam +function AI_ESCORT:_Flare( EscortGroup, Color, Message ) + + local EscortUnit = self.EscortUnit + + EscortGroup:GetUnit(1):Flare( Color ) + EscortGroup:MessageTypeToGroup( Message, MESSAGE.Type.Information, EscortUnit:GetGroup() ) +end + +--- @param #MENUPARAM MenuParam +function AI_ESCORT:_Smoke( EscortGroup, Color, Message ) + + local EscortUnit = self.EscortUnit + + EscortGroup:GetUnit(1):Smoke( Color ) + EscortGroup:MessageTypeToGroup( Message, MESSAGE.Type.Information, EscortUnit:GetGroup() ) +end + + +--- @param #MENUPARAM MenuParam +function AI_ESCORT:_ReportNearbyTargetsNow( EscortGroup ) + + local EscortUnit = self.EscortUnit + + self:_ReportTargetsScheduler( EscortGroup ) + +end + +function AI_ESCORT:_SwitchReportNearbyTargets( EscortGroup, ReportTargets ) + + local EscortUnit = self.EscortUnit + + self.ReportTargets = ReportTargets + + if self.ReportTargets then + if not EscortGroup.ReportTargetsScheduler then + EscortGroup.ReportTargetsScheduler:Schedule( self, self._ReportTargetsScheduler, {}, 1, 30 ) + end + else + routines.removeFunction( EscortGroup.ReportTargetsScheduler ) + EscortGroup.ReportTargetsScheduler = nil + end +end + +--- @param #MENUPARAM MenuParam +function AI_ESCORT:_ScanTargets( ScanDuration ) + + local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP + local EscortUnit = self.EscortUnit + + self.FollowScheduler:Stop( self.FollowSchedule ) + + if EscortGroup:IsHelicopter() then + EscortGroup:PushTask( + EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 200, 20 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ), 1 ) + elseif EscortGroup:IsAirPlane() then + EscortGroup:PushTask( + EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 1000, 500 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ), 1 ) + end + + EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortUnit ) + + if self.EscortMode == AI_ESCORT.MODE.FOLLOW then + self.FollowScheduler:Start( self.FollowSchedule ) + end + +end + +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +function AI_ESCORT.___Resume( EscortGroup, self ) + + if EscortGroup.EscortMode == AI_ESCORT.MODE.FOLLOW then + self:JoinFormation( EscortGroup ) + end + +end + +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup The escort group that will attack the detected item. +-- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem +function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) + + self:F( EscortGroup ) + + local EscortUnit = self.EscortUnit + + self:ReleaseFormation( EscortGroup ) + + if EscortGroup:IsAir() then + EscortGroup:OptionROEOpenFire() + EscortGroup:OptionROTPassiveDefense() + EscortGroup:SetState( EscortGroup, "Escort", self ) + + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + local AttackUnitTasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + AttackUnitTasks[#AttackUnitTasks+1] = EscortGroup:TaskAttackUnit( DetectedUnit ) + end + end, Tasks + ) + + Tasks[#Tasks+1] = EscortGroup:TaskCombo( AttackUnitTasks ) + Tasks[#Tasks+1] = EscortGroup:TaskFunction( "AI_ESCORT.___Resume", self, EscortGroup ) + + EscortGroup:SetTask( + EscortGroup:TaskCombo( + Tasks + ), 1 + ) + + else + + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) + end + end, Tasks + ) + + EscortGroup:SetTask( + EscortGroup:TaskCombo( + Tasks + ), 1 + ) + + end + + EscortGroup:MessageTypeToGroup( "Engaging Designated Unit!", MESSAGE.Type.Information, EscortUnit ) + +end + +--- +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup The escort group that will attack the detected item. +-- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem +function AI_ESCORT:_AssistTarget( EscortGroup, DetectedItem ) + + local EscortUnit = self.EscortUnit + + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) + end + end, Tasks + ) + + EscortGroup:SetTask( + EscortGroup:TaskCombo( + Tasks + ), 1 + ) + + + EscortGroup:MessageTypeToGroup( "Assisting with the destroying the enemy unit!", MESSAGE.Type.Information, EscortUnit:GetGroup() ) + +end + +--- @param #MENUPARAM MenuParam +function AI_ESCORT:_ROE( EscortGroup, EscortROEFunction, EscortROEMessage ) + + local EscortUnit = self.EscortUnit + + pcall( function() EscortROEFunction() end ) + EscortGroup:MessageTypeToGroup( EscortROEMessage, MESSAGE.Type.Information, EscortUnit:GetGroup() ) +end + +--- @param #MENUPARAM MenuParam +function AI_ESCORT:_ROT( EscortGroup, EscortROTFunction, EscortROTMessage ) + + local EscortUnit = self.EscortUnit + + pcall( function() EscortROTFunction() end ) + EscortGroup:MessageTypeToGroup( EscortROTMessage, MESSAGE.Type.Information, EscortUnit:GetGroup() ) +end + +--- @param #MENUPARAM MenuParam +function AI_ESCORT:_ResumeMission( WayPoint ) + + local EscortGroup = self.EscortGroup + local EscortUnit = self.EscortUnit + + self.FollowScheduler:Stop( self.FollowSchedule ) + + local WayPoints = EscortGroup:GetTaskRoute() + self:T( WayPoint, WayPoints ) + + for WayPointIgnore = 1, WayPoint do + table.remove( WayPoints, 1 ) + end + + SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) + + EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortUnit ) +end + +--- Registers the waypoints +-- @param #AI_ESCORT self +-- @return #table +function AI_ESCORT:RegisterRoute() + self:F() + + local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP + + local TaskPoints = EscortGroup:GetTaskRoute() + + self:T( TaskPoints ) + + return TaskPoints +end + + + +--- Report Targets Scheduler. +-- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) + self:F( EscortGroup:GetName() ) + + if EscortGroup:IsAlive() and self.EscortUnit:IsAlive() then + + if true then + + local EscortGroupName = EscortGroup:GetName() + + EscortGroup.EscortMenuAttackNearbyTargets:RemoveSubMenus() + + if EscortGroup.EscortMenuTargetAssistance then + EscortGroup.EscortMenuTargetAssistance:RemoveSubMenus() + end + + local DetectedItems = self.Detection:GetDetectedItems() + self:F( DetectedItems ) + + local DetectedTargets = false + + local DetectedMsgs = {} + + local ClientEscortTargets = self.Detection + --local EscortUnit = EscortGroupData:GetUnit( 1 ) + + for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do + self:F( { DetectedItemIndex, DetectedItem } ) + + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) + + if EscortGroup:IsAir() then + + local DetectedMsg = DetectedItemReportSummary:Text("\n") + DetectedMsgs[#DetectedMsgs+1] = DetectedMsg + + self:T( DetectedMsg ) + + MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), + DetectedMsg, + EscortGroup.EscortMenuAttackNearbyTargets, + AI_ESCORT._AttackTarget, + self, + EscortGroup, + DetectedItem + ) + else + if self.EscortMenuTargetAssistance then + + local DetectedMsg = DetectedItemReportSummary:Text("\n") + self:T( DetectedMsg ) + + local MenuTargetAssistance = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroupName, EscortGroup.EscortMenuTargetAssistance ) + MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), + DetectedMsg, + MenuTargetAssistance, + AI_ESCORT._AssistTarget, + self, + EscortGroup, + DetectedItem + ) + end + + end + DetectedTargets = true + end + self:F( DetectedMsgs ) + if DetectedTargets then + EscortGroup:MessageTypeToGroup( "Reporting detected targets:\n" .. table.concat( DetectedMsgs, "\n" ), MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) + else + EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) + end + + return true + else + end + end + + return false +end diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index c02096609..338d29738 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -136,6 +136,13 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin self.FollowUnit = FollowUnit -- Wrapper.Unit#UNIT self.FollowGroupSet = FollowGroupSet -- Core.Set#SET_GROUP + self.FollowGroupSet:ForEachGroup( + function( FollowGroup ) + self:E("Following") + FollowGroup.Following = true + end + ) + self:SetFlightRandomization( 2 ) self:SetStartState( "None" ) @@ -906,6 +913,31 @@ function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 end +--- This releases the air unit in your flight from the formation flight. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup FollowGroup. +-- @return #AI_FORMATION +function AI_FORMATION:ReleaseFormation( FollowGroup ) + + FollowGroup.Following = false + + return self +end + + +--- This joins up the air unit in your formation flight. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup FollowGroup. +-- @return #AI_FORMATION +function AI_FORMATION:JoinFormation( FollowGroup ) + + FollowGroup.Following = true + + return self +end + + + --- Stop function. Formation will not be updated any more. -- @param #AI_FORMATION self -- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. @@ -960,109 +992,112 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 --- @param Wrapper.Group#GROUP FollowGroup -- @param Wrapper.Unit#UNIT ClientUnit function( FollowGroup, Formation, ClientUnit, CT1, CV1, CT2, CV2 ) + + if FollowGroup.Following == true then - FollowGroup:OptionROTEvadeFire() - FollowGroup:OptionROEReturnFire() - - local GroupUnit = FollowGroup:GetUnit( 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() ) - 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 ) - + FollowGroup:OptionROTEvadeFire() + FollowGroup:OptionROEReturnFire() + + local GroupUnit = FollowGroup:GetUnit( 1 ) + local FollowFormation = FollowGroup:GetState( self, "FormationVec3" ) + if FollowFormation then + local FollowDistance = FollowFormation.x + local GT1 = GroupUnit:GetState( self, "GT1" ) - local GT2 = timer.getTime() - local GV1 = GroupUnit:GetState( self, "GV1" ) - local GV2 = GroupUnit:GetPointVec3() - GV2:AddX( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) - GV2:AddY( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) - 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_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 ) - - - -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. - -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + CS * 10 * math.sin(Ca), - 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, - z = GDV.z - GVz - } - - if self.SmokeDirectionVector == true then - trigger.action.smoke( GDV, trigger.smokeColor.Green ) - trigger.action.smoke( GDV_Formation, trigger.smokeColor.White ) + + 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() ) + 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" ) + local GV2 = GroupUnit:GetPointVec3() + GV2:AddX( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) + GV2:AddY( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) + 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_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 ) + + + -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. + -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) + local CVI = { x = CV2.x + CS * 10 * math.sin(Ca), + 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, + 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 + Speed = 0 + end + + -- Now route the escort to the desired point with the desired speed. + FollowGroup:RouteToVec3( GDV_Formation, GS ) -- DCS models speed in Mps (Miles per second) end - - - - local Time = 60 - - local Speed = - ( Distance + FollowFormation.x ) / Time - local GS = Speed + CS - if Speed < 0 then - Speed = 0 - end - - -- Now route the escort to the desired point with the desired speed. - FollowGroup:RouteToVec3( GDV_Formation, GS ) -- DCS models speed in Mps (Miles per second) end end end, diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 8a86a02f9..68e0125d9 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -84,6 +84,7 @@ __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_Escort.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' ) From 7a89960d212d0edb8504f7d4a4c3540454c11c5e Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 6 Apr 2019 16:33:22 +0200 Subject: [PATCH 244/485] Updates --- Moose Development/Moose/AI/AI_Escort.lua | 367 +++++++++++++++--- .../Moose/Functional/Detection.lua | 35 +- 2 files changed, 351 insertions(+), 51 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 0c4c6f8af..ec1576c4c 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -218,6 +218,8 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) self.GT1 = 0 + self.FlightMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Flight" ) + EscortGroupSet:ForEachGroup( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) @@ -324,6 +326,49 @@ end function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) self:F( { Height, Speed, MenuTextFormat } ) + if not Height then + Height = 30 + end + + if not Speed then + Speed = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Speed == 0 then + MenuText = string.format( "Hold at %d meter", Height ) + else + MenuText = string.format( "Hold at %d meter at %d", Height, Speed ) + end + else + if Speed == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Speed ) + end + end + + if not self.FlightMenuHold then + self.FlightMenuHold = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold position", self.FlightMenu ) + end + + if not self.FlightMenuHoldPosition then + self.FlightMenuHoldPosition = {} + end + + self.FlightMenuHoldPosition[#self.FlightMenuHoldPosition+1] = MENU_GROUP_COMMAND + :New( + self.EscortUnit:GetGroup(), + MenuText, + self.FlightMenuHold, + AI_ESCORT._FlightHoldPosition, + self, + nil, + Height, + Speed + ) + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) @@ -333,28 +378,6 @@ function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) EscortGroup.EscortMenuHold = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold position", EscortGroup.EscortMenu ) end - if not Height then - Height = 30 - end - - if not Speed then - Speed = 0 - end - - local MenuText = "" - if not MenuTextFormat then - if Speed == 0 then - MenuText = string.format( "Hold at %d meter", Height ) - else - MenuText = string.format( "Hold at %d meter at %d", Height, Speed ) - end - else - if Speed == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Speed ) - end - end if not EscortGroup.EscortMenuHoldPosition then EscortGroup.EscortMenuHoldPosition = {} @@ -390,13 +413,56 @@ end function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) self:F( { Height, Speed, MenuTextFormat } ) + if not self.FlightMenuHold then + self.FlightMenuHold = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold position", self.FlightMenu ) + end + + if not Height then + Height = 30 + end + + if not Speed then + Speed = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Speed == 0 then + MenuText = string.format( "Rejoin and hold at %d meter", Height ) + else + MenuText = string.format( "Rejoin and hold at %d meter at %d", Height, Speed ) + end + else + if Speed == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Speed ) + end + end + + if not self.FlightMenuHoldAtLeaderPosition then + self.FlightMenuHoldAtLeaderPosition = {} + end + + self.FlightMenuHoldAtLeaderPosition[#self.FlightMenuHoldAtLeaderPosition+1] = MENU_GROUP_COMMAND + :New( + self.EscortUnit:GetGroup(), + MenuText, + self.FlightMenuHold, + AI_ESCORT._FlightHoldPosition, + self, + self.EscortUnit:GetGroup(), + Height, + Speed + ) + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then - if not self.EscortMenuHold then - self.EscortMenuHold = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold position", EscortGroup.EscortMenu ) + if not EscortGroup.EscortMenuHold then + EscortGroup.EscortMenuHold = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold position", EscortGroup.EscortMenu ) end if not Height then @@ -430,7 +496,7 @@ function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) :New( self.EscortUnit:GetGroup(), MenuText, - self.EscortMenuHold, + EscortGroup.EscortMenuHold, AI_ESCORT._HoldPosition, self, self.EscortUnit:GetGroup(), @@ -512,6 +578,25 @@ end function AI_ESCORT:MenuFlare( MenuTextFormat ) self:F() + if not self.FlightMenuReportNavigation then + self.FlightMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", self.FlightMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Flare" + else + MenuText = MenuTextFormat + end + + if not self.FlightMenuFlare then + self.FlightMenuFlare = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, self.FlightMenuReportNavigation ) + self.FlightMenuFlareGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release green flare", self.FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Green, "Released a green flare!" ) + self.FlightMenuFlareRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release red flare", self.FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Red, "Released a red flare!" ) + self.FlightMenuFlareWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release white flare", self.FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.White, "Released a white flare!" ) + self.FlightMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release yellow flare", self.FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) + end + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) @@ -549,6 +634,26 @@ end function AI_ESCORT:MenuSmoke( MenuTextFormat ) self:F() + if not self.FlightMenuReportNavigation then + self.FlightMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", self.FlightMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Smoke" + else + MenuText = MenuTextFormat + end + + if not self.FlightMenuSmoke then + self.FlightMenuSmoke = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Smoke", self.FlightMenuReportNavigation ) + self.FlightMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release green smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) + self.FlightMenuSmokeRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release red smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) + self.FlightMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release white smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) + self.FlightMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release orange smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + self.FlightMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release blue smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) + end + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) @@ -588,27 +693,46 @@ end function AI_ESCORT:MenuReportTargets( Seconds ) self:F( { Seconds } ) + if not self.FlightMenuReportNearbyTargets then + self.FlightMenuReportNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Report targets", self.FlightMenu ) + end + + if not Seconds then + Seconds = 30 + end + + local timer = 1 + + -- Report Targets + self.FlightMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets now!", self.FlightMenuReportNearbyTargets, AI_ESCORT._FlightReportNearbyTargetsNow, self ) + self.FlightMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets on", self.FlightMenuReportNearbyTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, true ) + self.FlightMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets off", self.FlightMenuReportNearbyTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, false ) + + -- Attack Targets + self.FlightMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Attack targets", self.FlightMenu ) + + self.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, 1, Seconds ) + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - if not EscortGroup.EscortMenuReportNearbyTargets then - EscortGroup.EscortMenuReportNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Report targets", EscortGroup.EscortMenu ) + if EscortGroup:IsAir() then + if not EscortGroup.EscortMenuReportNearbyTargets then + EscortGroup.EscortMenuReportNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Report targets", EscortGroup.EscortMenu ) + end + + -- Report Targets + EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets now!", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup ) + EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) + EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) + + -- Attack Targets + EscortGroup.EscortMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Attack targets", EscortGroup.EscortMenu ) + + + EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, timer, Seconds ) + timer=timer+1 end - - if not Seconds then - Seconds = 30 - end - - -- Report Targets - EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets now!", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup ) - EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) - EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) - - -- Attack Targets - EscortGroup.EscortMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Attack targets", EscortGroup.EscortMenu ) - - - EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, 1, Seconds ) end ) @@ -623,9 +747,16 @@ end function AI_ESCORT:MenuAssistedAttack() self:F() - -- Request assistance from other escorts. - -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... - self.EscortMenuTargetAssistance = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Request assistance from", self.EscortMenu ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if not EscortGroup:IsAir() then + -- Request assistance from other escorts. + -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... + self.EscortMenuTargetAssistance = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Request assistance from", EscortGroup.EscortMenu ) + end + end + ) return self end @@ -756,6 +887,29 @@ function AI_ESCORT:_HoldPosition( OrbitGroup, EscortGroup, OrbitHeight, OrbitSec end + +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP OrbitGroup +-- @param #number OrbitHeight +-- @param #number OrbitSeconds +function AI_ESCORT:_FlightHoldPosition( OrbitGroup, OrbitHeight, OrbitSeconds ) + + local EscortUnit = self.EscortUnit + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup, OrbitGroup ) + if EscortGroup:IsAir() then + if OrbitGroup == nil then + OrbitGroup = EscortGroup + end + self:_HoldPosition( OrbitGroup, EscortGroup, OrbitHeight, OrbitSeconds ) + end + end, OrbitGroup + ) + +end + --- @param #MENUPARAM MenuParam function AI_ESCORT:_JoinUpAndFollow( Distance ) @@ -768,7 +922,6 @@ function AI_ESCORT:_JoinUpAndFollow( Distance ) end ---- @param #MENUPARAM MenuParam function AI_ESCORT:_Flare( EscortGroup, Color, Message ) local EscortUnit = self.EscortUnit @@ -777,7 +930,22 @@ function AI_ESCORT:_Flare( EscortGroup, Color, Message ) EscortGroup:MessageTypeToGroup( Message, MESSAGE.Type.Information, EscortUnit:GetGroup() ) end ---- @param #MENUPARAM MenuParam + +function AI_ESCORT:_FlightFlare( Color, Message ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_Flare( EscortGroup, Color, Message ) + end + end + ) + +end + + + function AI_ESCORT:_Smoke( EscortGroup, Color, Message ) local EscortUnit = self.EscortUnit @@ -786,8 +954,20 @@ function AI_ESCORT:_Smoke( EscortGroup, Color, Message ) EscortGroup:MessageTypeToGroup( Message, MESSAGE.Type.Information, EscortUnit:GetGroup() ) end +function AI_ESCORT:_FlightSmoke( Color, Message ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_Smoke( EscortGroup, Color, Message ) + end + end + ) + +end + ---- @param #MENUPARAM MenuParam function AI_ESCORT:_ReportNearbyTargetsNow( EscortGroup ) local EscortUnit = self.EscortUnit @@ -796,6 +976,21 @@ function AI_ESCORT:_ReportNearbyTargetsNow( EscortGroup ) end + +function AI_ESCORT:_FlightReportNearbyTargetsNow() + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_ReportNearbyTargetsNow( EscortGroup ) + end + end + ) + +end + + function AI_ESCORT:_SwitchReportNearbyTargets( EscortGroup, ReportTargets ) local EscortUnit = self.EscortUnit @@ -812,6 +1007,20 @@ function AI_ESCORT:_SwitchReportNearbyTargets( EscortGroup, ReportTargets ) end end + +function AI_ESCORT:_FlightSwitchReportNearbyTargets( ReportTargets ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_SwitchReportNearbyTargets( EscortGroup, ReportTargets ) + end + end + ) + +end + --- @param #MENUPARAM MenuParam function AI_ESCORT:_ScanTargets( ScanDuration ) @@ -918,6 +1127,21 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) end + +function AI_ESCORT:_FlightAttackTarget( DetectedItem ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup, DetectedItem ) + if EscortGroup:IsAir() then + self:_AttackTarget( EscortGroup, DetectedItem ) + end + end, DetectedItem + ) + +end + + --- --- @param #AI_ESCORT self -- @param Wrapper.Group#GROUP EscortGroup The escort group that will attack the detected item. @@ -1036,7 +1260,7 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do self:F( { DetectedItemIndex, DetectedItem } ) - local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) + local DetectedItemReportSummary = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) if EscortGroup:IsAir() then @@ -1087,3 +1311,48 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) return false end + +--- Report Targets Scheduler. +-- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +function AI_ESCORT:_FlightReportTargetsScheduler() + + self:F("FlightReportTargetScheduler") + + if self.EscortUnit:IsAlive() then + + self.FlightMenuAttackNearbyTargets:RemoveSubMenus() + + local DetectedItems = self.Detection:GetDetectedItems() + + local DetectedTargets = false + + local DetectedMsgs = {} + + local ClientEscortTargets = self.Detection + --local EscortUnit = EscortGroupData:GetUnit( 1 ) + + for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do + self:F( { DetectedItemIndex, DetectedItem } ) + + local DetectedItemReportSummary = self.Detection:DetectedItemReportMenu( DetectedItem, nil, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) + + local DetectedMsg = DetectedItemReportSummary:Text("\n") + DetectedMsgs[#DetectedMsgs+1] = DetectedMsg + + self:T( DetectedMsg ) + + MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), + DetectedMsg, + self.FlightMenuAttackNearbyTargets, + AI_ESCORT._FlightAttackTarget, + self, + DetectedItem + ) + end + + return true + end + + return false +end diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index c3d0c21a3..66c98c475 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -2478,6 +2478,37 @@ do -- DETECTION_AREAS end + --- Report summary of a detected item using a given numeric index. + -- @param #DETECTION_AREAS self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for. + -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use. + -- @return Core.Report#REPORT The report of the detection items. + function DETECTION_AREAS:DetectedItemReportMenu( DetectedItem, AttackGroup, Settings ) + self:F( { DetectedItem = DetectedItem } ) + + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) + + if DetectedItem then + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) + local ReportSummaryItem + + local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + local DetectedItemCoordinate = DetectedZone:GetCoordinate() + local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) + + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) + + local Report = REPORT:New() + Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) + Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) + + return Report + end + + return nil + end + --- Report summary of a detected item using a given numeric index. -- @param #DETECTION_AREAS self -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. @@ -2503,9 +2534,9 @@ do -- DETECTION_AREAS local Report = REPORT:New() Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) - Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) + Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) ) - Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) + --Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) return Report end From c2b46ceea11cafbdd89fc874dd8dcd7d2f2de16a Mon Sep 17 00:00:00 2001 From: Wingthor Date: Sun, 7 Apr 2019 02:04:08 +0200 Subject: [PATCH 245/485] Fixes GitHub issue 1140 SPAWN: GetLastAliveGroup() #1140 generating a nil value... "attempt to index field 'SpawnTemplatePrefixself' (a nil value)" --- Moose Development/Moose/Core/Spawn.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 7b25c5367..6594633c9 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2203,7 +2203,7 @@ end -- -- Do actions with the GroupPlane object. -- end function SPAWN:GetLastAliveGroup() - self:F( { self.SpawnTemplatePrefixself.SpawnAliasPrefix } ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) self.SpawnIndex = self:_GetLastIndex() for SpawnIndex = self.SpawnIndex, 1, -1 do From 8126b198bac2b175b9f786537a692cb46e8cc276 Mon Sep 17 00:00:00 2001 From: Wingthor Date: Sun, 7 Apr 2019 02:56:41 +0200 Subject: [PATCH 246/485] Fixed bug with SPAWN:GetLastAliveGroup() issue 1141 Changed line 2208 --- Moose Development/Moose/Core/Spawn.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 6594633c9..206f1e4cb 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2204,9 +2204,8 @@ end -- end function SPAWN:GetLastAliveGroup() self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - self.SpawnIndex = self:_GetLastIndex() - for SpawnIndex = self.SpawnIndex, 1, -1 do + + for SpawnIndex = self.SpawnCount, 1, -1 do -- Added local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) if SpawnGroup and SpawnGroup:IsAlive() then self.SpawnIndex = SpawnIndex From f1d217b6d7a50ab96d0dc8ed08192bc10309ca5f Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 7 Apr 2019 08:31:22 +0200 Subject: [PATCH 247/485] wip --- Moose Development/Moose/AI/AI_Escort.lua | 209 +++++++++++++----- Moose Development/Moose/Core/Report.lua | 7 +- .../Moose/Functional/Detection.lua | 3 +- 3 files changed, 165 insertions(+), 54 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index ec1576c4c..f13f626c0 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -160,6 +160,9 @@ AI_ESCORT = { TaskPoints = {} } +--- @field Functional.Detection#DETECTION_AREAS +AI_ESCORT.Detection = nil + --- AI_ESCORT.Mode class -- @type AI_ESCORT.MODE -- @field #number FOLLOW @@ -268,7 +271,7 @@ end --- Set a Detection method for the EscortUnit to be reported upon. -- Detection methods are based on the derived classes from DETECTION_BASE. -- @param #AI_ESCORT self --- @param Function.Detection#DETECTION_BASE Detection +-- @param Functional.Detection#DETECTION_AREAS Detection function AI_ESCORT:SetDetection( Detection ) self.Detection = Detection @@ -296,9 +299,11 @@ function AI_ESCORT:Menus() -- self:MenuScanForTargets( 100, 60 ) + self:MenuJoinUp() + self:MenuHoldAtEscortPosition( 1000, 500 ) self:MenuHoldAtLeaderPosition( 1000, 500 ) - + self:MenuFlare() self:MenuSmoke() @@ -314,6 +319,76 @@ function AI_ESCORT:Menus() end +--- Defines a menu slot to let the escort to join formation. +-- This menu will appear under **Navigation**. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuJoinUp() + + if not self.FlightMenuReportNavigation then + self.FlightMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", self.FlightMenu ) + end + + if not self.FlightMenuJoinUp then + self.FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Join Up", self.FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) + end + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + if not EscortGroup.EscortMenuReportNavigation then + EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) + end + + if not EscortGroup.EscortMenuJoinUpAndFollow then + EscortGroup.EscortMenuJoinUpAndFollow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Join-Up", EscortGroup.EscortMenuReportNavigation, ESCORT._JoinUp, self, EscortGroup ) + end + + end + end + ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a trail formation. +-- This menu will appear under **Navigation**. +-- @param #AI_ESCORT 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. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationTrail( XStart, XSpace, YStart ) + + if not self.FlightMenuReportNavigation then + self.FlightMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", self.FlightMenu ) + end + + if not self.FlightMenuJoinUp then + self.FlightMenuFormationTrail = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Trail", self.FlightMenuReportNavigation, AI_ESCORT._FlightFormationTrail, self, XStart, XSpace, YStart ) + end + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + if not EscortGroup.EscortMenuReportNavigation then + EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) + end + + if not EscortGroup.EscortMenuFormationTrail then + EscortGroup.EscortMenuFormationTrail = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Trail", EscortGroup.EscortMenuReportNavigation, ESCORT._EscortFormationTrail, self, EscortGroup, XStart, XSpace, YStart ) + end + + end + end + ) + + return self +end + --- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. @@ -711,7 +786,7 @@ function AI_ESCORT:MenuReportTargets( Seconds ) -- Attack Targets self.FlightMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Attack targets", self.FlightMenu ) - self.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, 1, Seconds ) + self.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, 5, Seconds ) self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup @@ -910,15 +985,55 @@ function AI_ESCORT:_FlightHoldPosition( OrbitGroup, OrbitHeight, OrbitSeconds ) end ---- @param #MENUPARAM MenuParam -function AI_ESCORT:_JoinUpAndFollow( Distance ) - local EscortGroup = self.EscortGroup + +function AI_ESCORT:_JoinUp( EscortGroup ) + local EscortUnit = self.EscortUnit - self.Distance = Distance + self:JoinFormation( EscortGroup ) + EscortGroup.EscortMode = AI_ESCORT.MODE.FOLLOW +end + + +function AI_ESCORT:_FlightJoinUp( EscortGroup ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_JoinUp( EscortGroup ) + end + end + ) + +end + + +--- Lets the escort to join in a trail formation. +-- @param #AI_ESCORT 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. +-- @return #AI_ESCORT +function AI_ESCORT:_EscortFormationTrail( EscortGroup, XStart, XSpace, YStart ) + + self:FormationTrail( XStart, XSpace, YStart ) + +end + + +function AI_ESCORT:_EscortFormationTrail( XStart, XSpace, YStart ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:FormationTrail( EscortGroup, XStart, XSpace, YStart ) + end + end + ) - self:JoinUpAndFollow( EscortGroup, EscortUnit, self.Distance ) end @@ -1021,7 +1136,7 @@ function AI_ESCORT:_FlightSwitchReportNearbyTargets( ReportTargets ) end ---- @param #MENUPARAM MenuParam + function AI_ESCORT:_ScanTargets( ScanDuration ) local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP @@ -1055,12 +1170,16 @@ end -- @param Wrapper.Group#GROUP EscortGroup function AI_ESCORT.___Resume( EscortGroup, self ) + local PlayerGroup = self.EscortUnit:GetGroup() + if EscortGroup.EscortMode == AI_ESCORT.MODE.FOLLOW then self:JoinFormation( EscortGroup ) + EscortGroup:MessageTypeToClient( "Destroyed all targets. Rejoining.", MESSAGE.Type.Information, PlayerGroup ) end end + --- @param #AI_ESCORT self -- @param Wrapper.Group#GROUP EscortGroup The escort group that will attack the detected item. -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem @@ -1123,7 +1242,7 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) end - EscortGroup:MessageTypeToGroup( "Engaging Designated Unit!", MESSAGE.Type.Information, EscortUnit ) + EscortGroup:MessageTypeToGroup( "Engaging!", MESSAGE.Type.Information, EscortUnit ) end @@ -1170,11 +1289,11 @@ function AI_ESCORT:_AssistTarget( EscortGroup, DetectedItem ) ) - EscortGroup:MessageTypeToGroup( "Assisting with the destroying the enemy unit!", MESSAGE.Type.Information, EscortUnit:GetGroup() ) + EscortGroup:MessageTypeToGroup( "Assisting attack!", MESSAGE.Type.Information, EscortUnit:GetGroup() ) end ---- @param #MENUPARAM MenuParam + function AI_ESCORT:_ROE( EscortGroup, EscortROEFunction, EscortROEMessage ) local EscortUnit = self.EscortUnit @@ -1183,7 +1302,7 @@ function AI_ESCORT:_ROE( EscortGroup, EscortROEFunction, EscortROEMessage ) EscortGroup:MessageTypeToGroup( EscortROEMessage, MESSAGE.Type.Information, EscortUnit:GetGroup() ) end ---- @param #MENUPARAM MenuParam + function AI_ESCORT:_ROT( EscortGroup, EscortROTFunction, EscortROTMessage ) local EscortUnit = self.EscortUnit @@ -1192,7 +1311,7 @@ function AI_ESCORT:_ROT( EscortGroup, EscortROTFunction, EscortROTMessage ) EscortGroup:MessageTypeToGroup( EscortROTMessage, MESSAGE.Type.Information, EscortUnit:GetGroup() ) end ---- @param #MENUPARAM MenuParam + function AI_ESCORT:_ResumeMission( WayPoint ) local EscortGroup = self.EscortGroup @@ -1212,6 +1331,7 @@ function AI_ESCORT:_ResumeMission( WayPoint ) EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortUnit ) end + --- Registers the waypoints -- @param #AI_ESCORT self -- @return #table @@ -1248,12 +1368,7 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) end local DetectedItems = self.Detection:GetDetectedItems() - self:F( DetectedItems ) - local DetectedTargets = false - - local DetectedMsgs = {} - local ClientEscortTargets = self.Detection --local EscortUnit = EscortGroupData:GetUnit( 1 ) @@ -1262,15 +1377,11 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) local DetectedItemReportSummary = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) + local DetectedMenu = DetectedItemReportSummary:Text("\n") + if EscortGroup:IsAir() then - - local DetectedMsg = DetectedItemReportSummary:Text("\n") - DetectedMsgs[#DetectedMsgs+1] = DetectedMsg - - self:T( DetectedMsg ) - MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), - DetectedMsg, + DetectedMenu, EscortGroup.EscortMenuAttackNearbyTargets, AI_ESCORT._AttackTarget, self, @@ -1279,13 +1390,9 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) ) else if self.EscortMenuTargetAssistance then - - local DetectedMsg = DetectedItemReportSummary:Text("\n") - self:T( DetectedMsg ) - local MenuTargetAssistance = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroupName, EscortGroup.EscortMenuTargetAssistance ) MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), - DetectedMsg, + DetectedMenu, MenuTargetAssistance, AI_ESCORT._AssistTarget, self, @@ -1295,15 +1402,9 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) end end - DetectedTargets = true + end - self:F( DetectedMsgs ) - if DetectedTargets then - EscortGroup:MessageTypeToGroup( "Reporting detected targets:\n" .. table.concat( DetectedMsgs, "\n" ), MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) - else - EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) - end - + return true else end @@ -1312,14 +1413,20 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) return false end ---- Report Targets Scheduler. +--- Report Targets Scheduler for the flight. The report is generated from the perspective of the player plane, and is reported by the first plane in the formation set. -- @param #AI_ESCORT self -- @param Wrapper.Group#GROUP EscortGroup function AI_ESCORT:_FlightReportTargetsScheduler() self:F("FlightReportTargetScheduler") + + local EscortGroup = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP + + local DetectedTargetsReport = REPORT:New( "Reporting detected targets:\n" ) -- A new report to display the detected targets as a message to the player. - if self.EscortUnit:IsAlive() then + if self.EscortUnit:IsAlive() and EscortGroup:IsAlive() then + + local ClientGroup = self.EscortUnit:GetGroup() self.FlightMenuAttackNearbyTargets:RemoveSubMenus() @@ -1327,20 +1434,16 @@ function AI_ESCORT:_FlightReportTargetsScheduler() local DetectedTargets = false - local DetectedMsgs = {} - local ClientEscortTargets = self.Detection - --local EscortUnit = EscortGroupData:GetUnit( 1 ) for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do - self:F( { DetectedItemIndex, DetectedItem } ) - local DetectedItemReportSummary = self.Detection:DetectedItemReportMenu( DetectedItem, nil, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) + DetectedTargets = true -- There are detected targets, when the content of the for loop is executed. We use it to display a message. + + local DetectedItemReportSummary = self.Detection:DetectedItemReportMenu( DetectedItem, ClientGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) - local DetectedMsg = DetectedItemReportSummary:Text("\n") - DetectedMsgs[#DetectedMsgs+1] = DetectedMsg - - self:T( DetectedMsg ) + local DetectedMsg = DetectedItemReportSummary:Text(", ") + DetectedTargetsReport:AddIndent( DetectedMsg, "-" ) MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), DetectedMsg, @@ -1351,6 +1454,12 @@ function AI_ESCORT:_FlightReportTargetsScheduler() ) end + if DetectedTargets then + EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) +-- else +-- EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) + end + return true end diff --git a/Moose Development/Moose/Core/Report.lua b/Moose Development/Moose/Core/Report.lua index 35800c60e..bd860996b 100644 --- a/Moose Development/Moose/Core/Report.lua +++ b/Moose Development/Moose/Core/Report.lua @@ -70,11 +70,12 @@ function REPORT:Add( Text ) return self end ---- Add a new line to a REPORT. +--- Add a new line to a REPORT, but indented. A separator character can be specified to separate the reported lines visually. -- @param #REPORT self --- @param #string Text +-- @param #string Text The report text. +-- @param #string Separator (optional) The start of each report line can begin with an optional separator character. This can be a "-", or "#", or "*". You're free to choose what you find the best. -- @return #REPORT -function REPORT:AddIndent( Text, Separator ) --R2.1 +function REPORT:AddIndent( Text, Separator ) self.Report[#self.Report+1] = ( ( Separator and Separator .. string.rep( " ", self.Indent - 1 ) ) or string.rep(" ", self.Indent ) ) .. Text:gsub("\n","\n"..string.rep( " ", self.Indent ) ) return self end diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 66c98c475..06ecfe87f 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -2500,8 +2500,9 @@ do -- DETECTION_AREAS local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) local Report = REPORT:New() + Report:Add( DetectedItemID ) Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) - Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) + Report:Add( DetectedItemCoordText ) return Report end From 0e14624d394dc9d6c13a37a2117644faa887ce68 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 7 Apr 2019 11:07:20 +0200 Subject: [PATCH 248/485] wip --- Moose Development/Moose/AI/AI_Escort.lua | 74 +++++++++++++++++++++++- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index f13f626c0..512ecbd2a 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -300,6 +300,8 @@ function AI_ESCORT:Menus() -- self:MenuScanForTargets( 100, 60 ) self:MenuJoinUp() + self:MenuFormationTrail( 0, 0, 50 ) + self:MenuFormationStack( 0, 0, 100, 150 ) self:MenuHoldAtEscortPosition( 1000, 500 ) self:MenuHoldAtLeaderPosition( 1000, 500 ) @@ -366,7 +368,7 @@ function AI_ESCORT:MenuFormationTrail( XStart, XSpace, YStart ) self.FlightMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", self.FlightMenu ) end - if not self.FlightMenuJoinUp then + if not self.FlightMenuFormationTrail then self.FlightMenuFormationTrail = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Trail", self.FlightMenuReportNavigation, AI_ESCORT._FlightFormationTrail, self, XStart, XSpace, YStart ) end @@ -390,6 +392,45 @@ function AI_ESCORT:MenuFormationTrail( XStart, XSpace, YStart ) end +--- Defines a menu slot to let the escort to join in a stacked formation. +-- This menu will appear under **Navigation**. +-- @param #AI_ESCORT 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. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationStack( XStart, XSpace, YStart, YSpace ) + + if not self.FlightMenuReportNavigation then + self.FlightMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", self.FlightMenu ) + end + + if not self.FlightMenuFormationStack then + self.FlightMenuFormationStack = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Stack", self.FlightMenuReportNavigation, AI_ESCORT._FlightFormationStack, self, XStart, XSpace, YStart, YSpace ) + end + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + if not EscortGroup.EscortMenuReportNavigation then + EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) + end + + if not EscortGroup.EscortMenuFormationStack then + EscortGroup.EscortMenuFormationStack = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Stack", EscortGroup.EscortMenuReportNavigation, ESCORT._EscortFormationStack, self, EscortGroup, XStart, XSpace, YStart, YSpace ) + end + + end + end + ) + + return self +end + + + --- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. -- This menu will appear under **Hold position**. @@ -1023,13 +1064,40 @@ function AI_ESCORT:_EscortFormationTrail( EscortGroup, XStart, XSpace, YStart ) end -function AI_ESCORT:_EscortFormationTrail( XStart, XSpace, YStart ) +function AI_ESCORT:_FlightFormationTrail( XStart, XSpace, YStart ) self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then - self:FormationTrail( EscortGroup, XStart, XSpace, YStart ) + self:_EscortFormationTrail( EscortGroup, XStart, XSpace, YStart ) + end + end + ) + +end + +--- Lets the escort to join in a stacked formation. +-- @param #AI_ESCORT 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 #number 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 #AI_ESCORT +function AI_ESCORT:_EscortFormationStack( EscortGroup, XStart, XSpace, YStart, YSpace ) + + self:FormationTrail( XStart, XSpace, YStart, YSpace ) + +end + + +function AI_ESCORT:_FlightFormationStack( XStart, XSpace, YStart, YSpace ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_EscortFormationStack( EscortGroup, XStart, XSpace, YStart, YSpace ) end end ) From f9f4b3a9fdc890eea6042039f395afe6940f7a14 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 8 Apr 2019 05:39:57 +0200 Subject: [PATCH 249/485] wip --- Moose Development/Moose/AI/AI_Escort.lua | 235 ++++++++++++++++++----- 1 file changed, 183 insertions(+), 52 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 512ecbd2a..9f4f813aa 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -321,8 +321,49 @@ function AI_ESCORT:Menus() end ---- Defines a menu slot to let the escort to join formation. --- This menu will appear under **Navigation**. + +function AI_ESCORT:MenuFormation( Formation, ... ) + + if not self.FlightMenuFormation then + self.FlightMenuFormation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Formation", self.FlightMenu ) + end + + if not self["FlightMenuFormation"..Formation] then + self["FlightMenuFormation"..Formation] = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), Formation, self.FlightMenuFormation, + function ( self, Formation, ... ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:E({Formation=Formation}) + self["Formation"..Formation]( arg ) + end + end + ) + end, self, Formation, ... + ) + end + +-- self.EscortGroupSet:ForEachGroupAlive( +-- --- @param Core.Group#GROUP EscortGroup +-- function( EscortGroup ) +-- if EscortGroup:IsAir() then +-- if not EscortGroup.EscortMenuReportNavigation then +-- EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) +-- end +-- +-- if not EscortGroup["EscortMenuFormation"..Formation] then +-- EscortGroup["EscortMenuFormation"..Formation] = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), Formation, EscortGroup.EscortMenuReportNavigation, AI_ESCORT["_EscortFormation"..Formation], self, EscortGroup, ... ) +-- end +-- end +-- end +-- ) + +end + + +--- Defines --- Defines a menu slot to let the escort to join formation. +-- This menu will appear under **Formation**. -- @param #AI_ESCORT self -- @return #AI_ESCORT function AI_ESCORT:MenuJoinUp() @@ -356,7 +397,7 @@ end --- Defines a menu slot to let the escort to join in a trail formation. --- This menu will appear under **Navigation**. +-- This menu will appear under **Formation**. -- @param #AI_ESCORT 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. @@ -364,36 +405,13 @@ end -- @return #AI_ESCORT function AI_ESCORT:MenuFormationTrail( XStart, XSpace, YStart ) - if not self.FlightMenuReportNavigation then - self.FlightMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", self.FlightMenu ) - end - - if not self.FlightMenuFormationTrail then - self.FlightMenuFormationTrail = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Trail", self.FlightMenuReportNavigation, AI_ESCORT._FlightFormationTrail, self, XStart, XSpace, YStart ) - end - - self.EscortGroupSet:ForEachGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if EscortGroup:IsAir() then - if not EscortGroup.EscortMenuReportNavigation then - EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) - end - - if not EscortGroup.EscortMenuFormationTrail then - EscortGroup.EscortMenuFormationTrail = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Trail", EscortGroup.EscortMenuReportNavigation, ESCORT._EscortFormationTrail, self, EscortGroup, XStart, XSpace, YStart ) - end - - end - end - ) + self:MenuFormation( "Trail", XStart, XSpace, YStart ) return self end - --- Defines a menu slot to let the escort to join in a stacked formation. --- This menu will appear under **Navigation**. +-- This menu will appear under **Formation**. -- @param #AI_ESCORT 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. @@ -402,34 +420,147 @@ end -- @return #AI_ESCORT function AI_ESCORT:MenuFormationStack( XStart, XSpace, YStart, YSpace ) - if not self.FlightMenuReportNavigation then - self.FlightMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", self.FlightMenu ) - end - - if not self.FlightMenuFormationStack then - self.FlightMenuFormationStack = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Stack", self.FlightMenuReportNavigation, AI_ESCORT._FlightFormationStack, self, XStart, XSpace, YStart, YSpace ) - end - - self.EscortGroupSet:ForEachGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if EscortGroup:IsAir() then - if not EscortGroup.EscortMenuReportNavigation then - EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) - end - - if not EscortGroup.EscortMenuFormationStack then - EscortGroup.EscortMenuFormationStack = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Stack", EscortGroup.EscortMenuReportNavigation, ESCORT._EscortFormationStack, self, EscortGroup, XStart, XSpace, YStart, YSpace ) - end - - end - end - ) + self:MenuFormation( "Stack", XStart, XSpace, YStart, YSpace ) return self end +--- Defines a menu slot to let the escort to join in a leFt wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @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. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationLeftWing( XStart, YStart, ZStart, ZSpace ) + + self:MenuFormation( "LeftWing", XStart, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a leFt wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @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. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationLeftLine( XStart, YStart, ZStart, ZSpace ) + + self:MenuFormation( "LeftLine", XStart, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a right line formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @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. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationRightLine( XStart, YStart, ZStart, ZSpace ) + + self:MenuFormation( "RightLine", XStart, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a left wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT 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. +-- @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 #AI_ESCORT +function AI_ESCORT:MenuFormationLeftWing( XStart, XSpace, YStart, ZStart, ZSpace ) + + self:MenuFormation( "LeftWing", XStart, XSpace, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a right wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT 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. +-- @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 #AI_ESCORT +function AI_ESCORT:MenuFormationRightWing( XStart, XSpace, YStart, ZStart, ZSpace ) + + self:MenuFormation( "RightWing", XStart, XSpace, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a center wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT 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. +-- @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. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationCenterWing( XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + self:MenuFormation( "CenterWing", XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a vic formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT 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. +-- @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. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationVic( XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + self:MenuFormation( "Vic", XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a box formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT 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. +-- @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. +-- @param #number ZLevels The amount of levels on the Z-axis. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationBox( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) + + self:MenuFormation( "Box", XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) + + return self +end --- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. @@ -1086,7 +1217,7 @@ end -- @return #AI_ESCORT function AI_ESCORT:_EscortFormationStack( EscortGroup, XStart, XSpace, YStart, YSpace ) - self:FormationTrail( XStart, XSpace, YStart, YSpace ) + self:FormationStack( XStart, XSpace, YStart, YSpace ) end From ddfc22bb5007a59d865f8808bc242965814c958f Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 8 Apr 2019 06:41:22 +0200 Subject: [PATCH 250/485] wip --- Moose Development/Moose/AI/AI_Escort.lua | 32 ++++++++---------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 9f4f813aa..713129c0d 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -300,8 +300,14 @@ function AI_ESCORT:Menus() -- self:MenuScanForTargets( 100, 60 ) self:MenuJoinUp() - self:MenuFormationTrail( 0, 0, 50 ) - self:MenuFormationStack( 0, 0, 100, 150 ) + self:MenuFormationTrail( 0, 50, 100 ) + self:MenuFormationStack( 0, 0, 100, 100 ) + self:MenuFormationLeftLine( 0, 0, 100, 100 ) + self:MenuFormationRightLine( 0, 0, 100, 100 ) + self:MenuFormationLeftWing( 0, 50, 0, 100, 100 ) + self:MenuFormationRightWing( 0, 50, 0, 100, 100 ) + self:MenuFormationCenterWing( 50, 50, 0, 50, 100, 100 ) + self:MenuFormationBox( 50, 100, 0, 50, 50, 100, 10 ) self:MenuHoldAtEscortPosition( 1000, 500 ) self:MenuHoldAtLeaderPosition( 1000, 500 ) @@ -333,12 +339,12 @@ function AI_ESCORT:MenuFormation( Formation, ... ) function ( self, Formation, ... ) self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) + function( EscortGroup, self, Formation, ... ) if EscortGroup:IsAir() then self:E({Formation=Formation}) - self["Formation"..Formation]( arg ) + self["Formation"..Formation]( self, ... ) end - end + end, self, Formation, ... ) end, self, Formation, ... ) @@ -426,22 +432,6 @@ function AI_ESCORT:MenuFormationStack( XStart, XSpace, YStart, YSpace ) end ---- Defines a menu slot to let the escort to join in a leFt wing formation. --- This menu will appear under **Formation**. --- @param #AI_ESCORT self --- @param #number XStart The start position on the X-axis in meters for the first group. --- @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. --- @return #AI_ESCORT -function AI_ESCORT:MenuFormationLeftWing( XStart, YStart, ZStart, ZSpace ) - - self:MenuFormation( "LeftWing", XStart, YStart, ZStart, ZSpace ) - - return self -end - - --- Defines a menu slot to let the escort to join in a leFt wing formation. -- This menu will appear under **Formation**. -- @param #AI_ESCORT self From 5e67861ea94a42f4ef1af0b0c9c03dce2a597b32 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 8 Apr 2019 17:55:24 +0200 Subject: [PATCH 251/485] Improvements --- Moose Development/Moose/AI/AI_Escort.lua | 97 ++++++++-------- Moose Development/Moose/AI/AI_Formation.lua | 4 +- Moose Development/Moose/Core/Set.lua | 107 +++++++++++++++++- .../Moose/Functional/Detection.lua | 18 +-- 4 files changed, 167 insertions(+), 59 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 713129c0d..b742aad2f 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -198,11 +198,11 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) self.EscortUnit = self.FollowUnit -- Wrapper.Unit#UNIT self.EscortGroupSet = EscortGroupSet + + self.EscortGroupSet:SetSomeIteratorLimit( 5 ) + self.EscortBriefing = EscortBriefing - - - -- if not EscortBriefing then -- EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. @@ -226,8 +226,6 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) EscortGroupSet:ForEachGroup( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName() ) - -- Set EscortGroup known at EscortUnit. if not self.EscortUnit._EscortGroups then self.EscortUnit._EscortGroups = {} @@ -244,6 +242,14 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) end ) + EscortGroupSet:ForSomeGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName() ) + end + ) + + self.Detection = DETECTION_AREAS:New( EscortGroupSet, 5000 ) self.Detection:Start() @@ -337,7 +343,7 @@ function AI_ESCORT:MenuFormation( Formation, ... ) if not self["FlightMenuFormation"..Formation] then self["FlightMenuFormation"..Formation] = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), Formation, self.FlightMenuFormation, function ( self, Formation, ... ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup, self, Formation, ... ) if EscortGroup:IsAir() then @@ -350,7 +356,7 @@ function AI_ESCORT:MenuFormation( Formation, ... ) ) end --- self.EscortGroupSet:ForEachGroupAlive( +-- self.EscortGroupSet:ForSomeGroupAlive( -- --- @param Core.Group#GROUP EscortGroup -- function( EscortGroup ) -- if EscortGroup:IsAir() then @@ -382,7 +388,7 @@ function AI_ESCORT:MenuJoinUp() self.FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Join Up", self.FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) end - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -606,7 +612,7 @@ function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) Speed ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -693,7 +699,7 @@ function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) Speed ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -834,7 +840,7 @@ function AI_ESCORT:MenuFlare( MenuTextFormat ) self.FlightMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release yellow flare", self.FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) end - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if not EscortGroup.EscortMenuReportNavigation then @@ -891,7 +897,7 @@ function AI_ESCORT:MenuSmoke( MenuTextFormat ) self.FlightMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release blue smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) end - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if not EscortGroup:IsAir() then @@ -950,23 +956,23 @@ function AI_ESCORT:MenuReportTargets( Seconds ) self.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, 5, Seconds ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then + if not EscortGroup.EscortMenuReportNearbyTargets then EscortGroup.EscortMenuReportNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Report targets", EscortGroup.EscortMenu ) end -- Report Targets - EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets now!", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup ) + EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets now!", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) -- Attack Targets EscortGroup.EscortMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Attack targets", EscortGroup.EscortMenu ) - EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, timer, Seconds ) timer=timer+1 end @@ -984,7 +990,7 @@ end function AI_ESCORT:MenuAssistedAttack() self:F() - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if not EscortGroup:IsAir() then @@ -1005,7 +1011,7 @@ end function AI_ESCORT:MenuROE( MenuTextFormat ) self:F( MenuTextFormat ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if not EscortGroup.EscortMenuROE then @@ -1038,7 +1044,7 @@ end function AI_ESCORT:MenuEvasion( MenuTextFormat ) self:F( MenuTextFormat ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1133,7 +1139,7 @@ function AI_ESCORT:_FlightHoldPosition( OrbitGroup, OrbitHeight, OrbitSeconds ) local EscortUnit = self.EscortUnit - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup, OrbitGroup ) if EscortGroup:IsAir() then @@ -1160,7 +1166,7 @@ end function AI_ESCORT:_FlightJoinUp( EscortGroup ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1187,7 +1193,7 @@ end function AI_ESCORT:_FlightFormationTrail( XStart, XSpace, YStart ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1214,7 +1220,7 @@ end function AI_ESCORT:_FlightFormationStack( XStart, XSpace, YStart, YSpace ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1237,7 +1243,7 @@ end function AI_ESCORT:_FlightFlare( Color, Message ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1260,7 +1266,7 @@ end function AI_ESCORT:_FlightSmoke( Color, Message ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1283,14 +1289,7 @@ end function AI_ESCORT:_FlightReportNearbyTargetsNow() - self.EscortGroupSet:ForEachGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if EscortGroup:IsAir() then - self:_ReportNearbyTargetsNow( EscortGroup ) - end - end - ) + self:_FlightReportTargetsScheduler() end @@ -1314,7 +1313,7 @@ end function AI_ESCORT:_FlightSwitchReportNearbyTargets( ReportTargets ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1438,7 +1437,7 @@ end function AI_ESCORT:_FlightAttackTarget( DetectedItem ) - self.EscortGroupSet:ForEachGroupAlive( + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup, DetectedItem ) if EscortGroup:IsAir() then @@ -1550,7 +1549,6 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) local EscortGroupName = EscortGroup:GetName() - EscortGroup.EscortMenuAttackNearbyTargets:RemoveSubMenus() if EscortGroup.EscortMenuTargetAssistance then EscortGroup.EscortMenuTargetAssistance:RemoveSubMenus() @@ -1559,10 +1557,10 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) local DetectedItems = self.Detection:GetDetectedItems() local ClientEscortTargets = self.Detection - --local EscortUnit = EscortGroupData:GetUnit( 1 ) + local TimeUpdate = timer.getTime() + for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do - self:F( { DetectedItemIndex, DetectedItem } ) local DetectedItemReportSummary = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) @@ -1576,7 +1574,7 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) self, EscortGroup, DetectedItem - ) + ):SetTag( "Escort" ):SetTime( TimeUpdate ) else if self.EscortMenuTargetAssistance then local MenuTargetAssistance = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroupName, EscortGroup.EscortMenuTargetAssistance ) @@ -1594,6 +1592,8 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) end + EscortGroup.EscortMenuAttackNearbyTargets:RemoveSubMenus( TimeUpdate, "Esort" ) + return true else end @@ -1617,7 +1617,8 @@ function AI_ESCORT:_FlightReportTargetsScheduler() local ClientGroup = self.EscortUnit:GetGroup() - self.FlightMenuAttackNearbyTargets:RemoveSubMenus() + local TimeUpdate = timer.getTime() + local DetectedItems = self.Detection:GetDetectedItems() @@ -1629,20 +1630,24 @@ function AI_ESCORT:_FlightReportTargetsScheduler() DetectedTargets = true -- There are detected targets, when the content of the for loop is executed. We use it to display a message. - local DetectedItemReportSummary = self.Detection:DetectedItemReportMenu( DetectedItem, ClientGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) - - local DetectedMsg = DetectedItemReportSummary:Text(", ") - DetectedTargetsReport:AddIndent( DetectedMsg, "-" ) - + local DetectedItemReportMenu = self.Detection:DetectedItemReportMenu( DetectedItem, ClientGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) + local ReportMenuText = DetectedItemReportMenu:Text(", ") + MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), - DetectedMsg, + ReportMenuText, self.FlightMenuAttackNearbyTargets, AI_ESCORT._FlightAttackTarget, self, DetectedItem - ) + ):SetTag( "Flight" ):SetTime( TimeUpdate ) + + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, ClientGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) + local ReportSummary = DetectedItemReportSummary:Text(", ") + DetectedTargetsReport:AddIndent( ReportSummary, "-" ) end + self.FlightMenuAttackNearbyTargets:RemoveSubMenus( TimeUpdate, "Flight" ) + if DetectedTargets then EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) -- else diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 338d29738..ca82a043d 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -1038,7 +1038,7 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 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 + local Distance = GD * Position + - CS * 0.3 -- Calculate the group direction vector local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } @@ -1056,7 +1056,7 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) local CVI = { x = CV2.x + CS * 10 * math.sin(Ca), - y = GH2.y - ( Distance + FollowFormation.x ) / 5, -- + FollowFormation.y, + y = GH2.y - ( Distance + FollowFormation.x ) / 10, -- + FollowFormation.y, z = CV2.z + CS * 10 * math.cos(Ca), } diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 4399c92f7..5a5f47adb 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -125,7 +125,7 @@ do -- SET_BASE self.Index = {} self.CallScheduler = SCHEDULER:New( self ) - + self:SetEventPriority( 2 ) return self @@ -341,6 +341,25 @@ do -- SET_BASE return self end + --- Define the SET iterator **"limit"**. + -- @param #SET_BASE self + -- @param #number Limit Defines how many objects are evaluated of the set as part of the Some iterators. The default is 1. + -- @return #SET_BASE self + function SET_BASE:SetSomeIteratorLimit( Limit ) + + self.SomeIteratorLimit = Limit or 1 + + return self + end + + --- Get the SET iterator **"limit"**. + -- @param #SET_BASE self + -- @return #number Defines how many objects are evaluated of the set as part of the Some iterators. + function SET_BASE:GetSomeIteratorLimit() + + return self.SomeIteratorLimit or self:Count() + end + --- Filters for the defined collection. -- @param #SET_BASE self @@ -590,6 +609,66 @@ do -- SET_BASE return self end + + --- 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:ForSome( IteratorFunction, arg, Set, Function, FunctionArguments ) + self:F3( arg ) + + Set = Set or self:GetSet() + arg = arg or {} + + local Limit = self:GetSomeIteratorLimit() + + local function CoRoutine() + local Count = 0 + for ObjectID, ObjectData in pairs( Set ) do + local Object = ObjectData + self:T3( Object ) + if Function then + if Function( unpack( FunctionArguments ), Object ) == true then + IteratorFunction( Object, unpack( arg ) ) + end + else + IteratorFunction( Object, unpack( arg ) ) + end + Count = Count + 1 + if Count >= Limit then + break + end + -- if Count % self.YieldInterval == 0 then + -- coroutine.yield( false ) + -- 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. @@ -1144,7 +1223,7 @@ do -- SET_GROUP --- 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. + -- @param #function IteratorFunction The function that will be called for all 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 ) @@ -1154,6 +1233,18 @@ do -- SET_GROUP return self end + --- Iterate the SET_GROUP and call an iterator function for some GROUP objects, providing the GROUP and optional parameters. + -- @param #SET_GROUP self + -- @param #function IteratorFunction The function that will be called for some GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForSomeGroup( IteratorFunction, ... ) + self:F2( arg ) + + self:ForSome( 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. @@ -1166,6 +1257,18 @@ do -- SET_GROUP return self end + --- Iterate the SET_GROUP and call an iterator function for some **alive** GROUP objects, 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:ForSomeGroupAlive( IteratorFunction, ... ) + self:F2( arg ) + + self:ForSome( 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. diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 06ecfe87f..fe078643a 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -305,7 +305,7 @@ do -- DETECTION_BASE --- DETECTION constructor. -- @param #DETECTION_BASE self - -- @param Core.Set#SET_BASE DetectionSet The @{Set} that is used to detect the units. + -- @param Core.Set#SET_GROUP DetectionSet The @{Set} of @{Group}s that is used to detect the units. -- @return #DETECTION_BASE self function DETECTION_BASE:New( DetectionSet ) @@ -541,13 +541,14 @@ do -- DETECTION_BASE end - self.DetectionCount = self.DetectionSet:Count() - for DetectionID, DetectionData in pairs( self.DetectionSet:GetSet() ) do - --self:F( { DetectionGroupData } ) - self:F( { DetectionGroup = DetectionData:GetName() } ) - self:__Detection( DetectDelay, DetectionData, DetectionTimeStamp ) -- Process each detection asynchronously. - DetectDelay = DetectDelay + 1 - end + self.DetectionCount = self.DetectionSet:GetSomeIteratorLimit() + + self.DetectionSet:ForSomeGroupAlive( + function( DetectionGroup ) + self:__Detection( DetectDelay, DetectionGroup, DetectionTimeStamp ) -- Process each detection asynchronously. + DetectDelay = DetectDelay + 1 + end + ) end --- @param #DETECTION_BASE self @@ -2502,7 +2503,6 @@ do -- DETECTION_AREAS local Report = REPORT:New() Report:Add( DetectedItemID ) Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) - Report:Add( DetectedItemCoordText ) return Report end From 4c08002d3e7557fc5e4d5b1bab979825515f31e4 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 9 Apr 2019 14:56:20 +0200 Subject: [PATCH 252/485] AB 0998 --- Moose Development/Moose/Ops/Airboss.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index d49aff897..c54bc60cb 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1667,7 +1667,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.7" +AIRBOSS.version="0.9.9.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -8861,8 +8861,11 @@ function AIRBOSS:_Initial(playerData) -- Relative heading to carrier direction. local relheading=self:_GetRelativeHeading(playerData.unit, false) + -- Alitude of player in feet. + local altitude=UTILS.MetersToFeet(playerData.unit:GetAltitude()) + -- Check if player is in zone and flying roughly in the right direction. - if inzone and math.abs(relheading)<60 then + if inzone and math.abs(relheading)<60 and altitude<=1300 then -- Send message for normal and easy difficulty. if playerData.showhints then From bf449492b55289c013f74ba52622729081080b2e Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 9 Apr 2019 17:45:29 +0200 Subject: [PATCH 253/485] Updated approach speed logic to prevent airplanes to fly beyond the hold position. --- Moose Development/Moose/AI/AI_Escort.lua | 4 ++-- Moose Development/Moose/AI/AI_Formation.lua | 25 ++++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index b742aad2f..f6b0ffeb9 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -306,8 +306,8 @@ function AI_ESCORT:Menus() -- self:MenuScanForTargets( 100, 60 ) self:MenuJoinUp() - self:MenuFormationTrail( 0, 50, 100 ) - self:MenuFormationStack( 0, 0, 100, 100 ) + self:MenuFormationTrail( 50, 100, 50 ) + self:MenuFormationStack( 50, 100, 50, 50 ) self:MenuFormationLeftLine( 0, 0, 100, 100 ) self:MenuFormationRightLine( 0, 0, 100, 100 ) self:MenuFormationLeftWing( 0, 50, 0, 100, 100 ) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index ca82a043d..edf3afe96 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -963,15 +963,11 @@ end --- @param #AI_FORMATION self function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 - self:F( ) - self:T( { self.FollowUnit.UnitName, self.FollowUnit:IsAlive() } ) if self.FollowUnit:IsAlive() then local ClientUnit = self.FollowUnit - self:T( {ClientUnit.UnitName } ) - local CT1, CT2, CV1, CV2 CT1 = ClientUnit:GetState( self, "CT1" ) @@ -1038,7 +1034,7 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 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.3 + 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 } @@ -1056,7 +1052,7 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) local CVI = { x = CV2.x + CS * 10 * math.sin(Ca), - y = GH2.y - ( Distance + FollowFormation.x ) / 10, -- + FollowFormation.y, + y = GH2.y + ( Distance + FollowFormation.x ) / 10, -- + FollowFormation.y, z = CV2.z + CS * 10 * math.cos(Ca), } @@ -1087,13 +1083,22 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 - local Time = 60 + local Time = 120 local Speed = - ( Distance + FollowFormation.x ) / Time - local GS = Speed + CS - if Speed < 0 then - Speed = 0 + + if Distance > -4000 then + Speed = - ( Distance + FollowFormation.x ) / 90 end + + if Distance > -1500 then + Speed = - ( Distance + FollowFormation.x ) / 20 + end + + local GS = Speed + CS + + self:F( { Distance = Distance, Speed = Speed, CS = CS, GS = GS } ) + -- Now route the escort to the desired point with the desired speed. FollowGroup:RouteToVec3( GDV_Formation, GS ) -- DCS models speed in Mps (Miles per second) From eb628109118cdc3cbc4e89442d9a9e517e835b12 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 10 Apr 2019 16:24:45 +0200 Subject: [PATCH 254/485] Range --- Moose Development/Moose/Functional/Range.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 65c591676..52e3a24cf 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -264,6 +264,14 @@ RANGE.Defaults={ foulline=610, } +--- Bomb target. self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed} +-- @type RANGE.BombTarget +-- @field #string name Name of unit? +-- @field Wrapper.Unit#UNIT target Target unit. +-- @field #number goodhitrange Range in meters for a good hit. +-- @field #boolean move If true, unit move randomly. +-- @field #number speed Speed of unit. + --- Global list of all defined range names. -- @field #table Names RANGE.Names={} From 03c47cc9f9c9efdea68c79b26d26de8cd547236b Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 10 Apr 2019 18:49:05 +0200 Subject: [PATCH 255/485] documentation --- Moose Development/Moose/AI/AI_Escort.lua | 85 ++++++++++++++++-------- 1 file changed, 57 insertions(+), 28 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index f6b0ffeb9..07bedf8c8 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -25,11 +25,49 @@ -- -- Allows you to interact with escorting AI on your flight and take the lead. -- --- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10). +-- Each escorting group can be commanded with a complete set of radio commands (radio menu in your flight, and then F10). -- --- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes. +-- The radio commands will vary according the category of the group. The richest set of commands are with helicopters and airPlanes. -- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. --- +-- +-- Escorts detect targets using a built-in detection mechanism. The detected targets are reported at a specified time interval. +-- Once targets are reported, each escort has these targets as menu options to command the attack of these targets. +-- Targets are by default grouped per area of 5000 meters, but the kind of detection and the grouping range can be altered. +-- +-- Different formations can be selected in the Flight menu: Trail, Stack, Left Line, Right Line, Left Wing, Right Wing, Central Wing and Boxed formations are available. +-- The Flight menu also allows for a mass attack, where all of the escorts are commanded to attack a target. +-- +-- Escorts can emit flares to reports their location. They can be commanded to hold at a location, which can be their current or the leader location. +-- In this way, you can spread out the escorts over the battle field before a coordinated attack. +-- +-- But basically, the escort class provides 4 modes of operation, and depending on the mode, you are either leading the flight, or following the flight. +-- +-- ## Leading the flight +-- +-- When leading the flight, you are expected to guide the escorts towards the target areas, +-- and carefully coordinate the attack based on the threat levels reported, and the available weapons +-- carried by the escorts. Ground ships or ground troops can execute A-assisted attacks, when they have long-range ground precision weapons for attack. +-- +-- ## Following the flight +-- +-- Escorts can be commanded to execute a specific mission path. In this mode, the escorts are in the lead. +-- You as a player, are following the escorts, and are commanding them to progress the mission while +-- ensuring that the escorts survive. You are joining the escorts in the battlefield. They will detect and report targets +-- and you will ensure that the attacks are well coordinated, assigning the correct escort type for the detected target +-- type. Once the attack is finished, the escort will resume the mission it was assigned. +-- In other words, you can use the escorts for reconnaissance, and for guiding the attack. +-- Imagine you as a mi-8 pilot, assigned to pickup cargo. Two ka-50s are guiding the way, and you are +-- following. You are in control. The ka-50s detect targets, report them, and you command how the attack +-- will commence and from where. You can control where the escorts are holding position and which targets +-- are attacked first. You are in control how the ka-50s will follow their mission path. +-- +-- Escorts can act as part of a AI A2G dispatcher offensive. In this way, You was a player are in control. +-- The mission is defined by the A2G dispatcher, and you are responsible to join the flight and ensure that the +-- attack is well coordinated. +-- +-- It is with great proud that I present you this class, and I hope you will enjoy the functionality and the dynamism +-- it brings in your DCS world simulations. +-- -- # RADIO MENUs that can be created: -- -- Find a summary below of the current available commands: @@ -38,7 +76,7 @@ -- -- Escort group navigation functions: -- --- * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you. +-- * **"Join-Up":** The escort group fill follow you in the assigned formation. -- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. -- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. -- @@ -46,32 +84,33 @@ -- -- Escort group navigation functions: -- --- * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. --- * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. +-- * **"At current location":** The escort group will hover above the ground at the position they were. The altitude can be specified as a parameter. +-- * **"At my location":** The escort group will hover or orbit at the position where you are. The escort will fly to your location and hold position. The altitude can be specified as a parameter. -- -- ## Report targets ...: -- --- Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). +-- Report targets will make the escort group to report any target that it identifies within detection range. Any detected target can be attacked using the "Attack Targets" menu function. (see below). -- -- * **"Report now":** Will report the current detected targets. --- * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list. +-- * **"Report targets on":** Will make the escorts to report the detected targets and will fill the "Attack Targets" menu list. -- * **"Report targets off":** Will stop detecting targets. -- --- ## Scan targets ...: --- --- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. --- --- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. --- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. --- -- ## Attack targets ...: -- -- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. +-- This menu will be available in Flight menu or in each Escort menu. +-- +-- ## Scan targets ...: +-- +-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or rejoin formation. +-- +-- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. +-- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. -- -- ## Request assistance from ...: -- --- This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. --- This menu item allows to request attack support from other escorts supporting the current client group. +-- This menu item will list all detected targets within a 15km range, similar as with the menu item **Attack Targets**. +-- This menu item allows to request attack support from other ground based escorts supporting the current escort. -- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. -- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. -- @@ -111,22 +150,12 @@ --- @type AI_ESCORT -- @extends AI.AI_Formation#AI_FORMATION --- @field Wrapper.Client#CLIENT EscortUnit --- @field Wrapper.Group#GROUP EscortGroup --- @field #string EscortName --- @field #AI_ESCORT.MODE EscortMode The mode the escort is in. --- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. --- @field #number FollowDistance The current follow distance. --- @field #boolean ReportTargets If true, nearby targets are reported. --- @Field DCS#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. --- @field DCS#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. --- @field FunctionalMENU_GROUPDETECTION_BASE Detection --- AI_ESCORT class -- -- # AI_ESCORT construction methods. -- --- Create a new SPAWN object with the @{#AI_ESCORT.New} method: +-- Create a new AI_ESCORT object with the @{#AI_ESCORT.New} method: -- -- * @{#AI_ESCORT.New}: Creates a new AI_ESCORT object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text. -- From 208761fa4610d45d446775e54e6b4434a194abf1 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 11 Apr 2019 21:46:48 +0200 Subject: [PATCH 256/485] Static and Delay --- Moose Development/Moose/Core/Point.lua | 17 ++- Moose Development/Moose/Functional/Range.lua | 2 +- .../Moose/Functional/Warehouse.lua | 3 + .../Moose/Wrapper/Controllable.lua | 122 ++++++++++++++++-- Moose Development/Moose/Wrapper/Group.lua | 63 +++++++++ Moose Development/Moose/Wrapper/Static.lua | 63 ++++++--- Moose Development/Moose/Wrapper/Unit.lua | 24 ++-- 7 files changed, 250 insertions(+), 44 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index a0cebabb4..314b6460c 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1505,15 +1505,24 @@ do -- COORDINATE --- Creates an explosion at the point of a certain intensity. -- @param #COORDINATE self - -- @param #number ExplosionIntensity Intensity of the explosion in kg TNT. - function COORDINATE:Explosion( ExplosionIntensity ) + -- @param #number ExplosionIntensity Intensity of the explosion in kg TNT. Default 100 kg. + -- @param #number Delay Delay before explosion in seconds. + -- @return #COORDINATE self + function COORDINATE:Explosion( ExplosionIntensity, Delay ) self:F2( { ExplosionIntensity } ) - trigger.action.explosion( self:GetVec3(), ExplosionIntensity ) + ExplosionIntensity=ExplosionIntensity or 100 + if Delay and Delay>0 then + SCHEDULER:New(nil, self.Explosion, {self,ExplosionIntensity}, Delay) + else + trigger.action.explosion( self:GetVec3(), ExplosionIntensity ) + end + return self end --- Creates an illumination bomb at the point. -- @param #COORDINATE self - -- @param #number power + -- @param #number power Power of illumination bomb in Candela. + -- @return #COORDINATE self function COORDINATE:IlluminationBomb(power) self:F2() trigger.action.illuminationBomb( self:GetVec3(), power ) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 52e3a24cf..c3f9ee12e 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -1185,7 +1185,7 @@ function RANGE:OnEventShot(EventData) -- Weapon data. local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName - local _weaponStrArray = self:_split(_weapon,"%.") + local _weaponStrArray = UTILS.Split(_weapon,"%.") local _weaponName = _weaponStrArray[#_weaponStrArray] -- Debug info. diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 3ce11bb07..3537c42d5 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -3675,6 +3675,8 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, local SpeedMax=group:GetSpeedMax() local RangeMin=group:GetRange() local smax,sx,sy,sz=_GetObjectSize(Descriptors) + + --self:E(Descriptors) -- Get weight and cargo bay size in kg. local weight=0 @@ -6379,6 +6381,7 @@ function WAREHOUSE:_CheckRequestValid(request) if inwater then self:E("ERROR: Incorrect request. Ground asset requested but at least one spawn zone is in water!") + --valid=false valid=false end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index a068d6a72..039afe898 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -830,6 +830,101 @@ function CONTROLLABLE:TaskEPLRS(SwitchOnOff, idx) end +--- Used in conjunction with the embarking task for a transport helicopter group. The Ground units will move to the specified location and wait to be picked up by a helicopter. +-- The helicopter will then fly them to their dropoff point defined by another task for the ground forces; DisembarkFromTransport task. +-- The controllable has to be an infantry group! +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. +-- @param #number Radius Radius in meters. +-- @return #table Embark to transport task. +function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius) + + local EmbarkToTransport = { + id="EmbarkToTransport ", + enabled=true, + auto=false, + params={ + x=Coordinate.x, + y=Coordinate.z, + zoneRadius=Radius, + selectedType="UH-1H", + } + } + + self:E(EmbarkToTransport) + return EmbarkToTransport +end + +--- Specifies the location an infantry group that is being transported by helicopters will be unloaded at. Used in conjunction with the EmbarkToTransport task. +-- The CONTROLLABLE has to be an infantry group! +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. +-- @param #number Radius Radius in meters. +-- @return #table Embark to transport task. +function CONTROLLABLE:TaskDisembarkFromTransport(Coordinate, Radius) + + local DisembarkFromTransport={ + id="DisembarkFromTransport", + params = { + x=Coordinate.x, + y=Coordinate.y, + zoneRadius=Radius, + }} + + return DisembarkFromTransport +end + +--- Used in conjunction with the EmbarkToTransport task for a ground infantry group, the controlled helicopter flight will land at the specified coordinates, +-- pick up boarding troops and transport them to that groups DisembarkFromTransport task. +-- The CONTROLLABLE has to be a helicopter group! +-- @param #CONTROLLABLE self +-- @param Core.Set#SET_GROUP GroupSet Set of groups to be embarked by the controllable. +-- @param Core.Point#COORDINATE Coordinate Coordinate of embarking. +-- @param #number Duration Duration of embarking in seconds. +-- @return #table Embarking task. +function CONTROLLABLE:TaskEmbarking(GroupSet, Coordinate, Duration) + + -- Create table of group IDs. + local gids={} + for _,_group in pairs(GroupSet:GetAliveSet()) do + local group=_group --Wrapper.Group#GROUP + table.insert(gids, group:GetID()) + end + + -- Group ID of controllable. + local id=self:GetID() + + -- Distribution + local distribution={} + distribution[id]=gids + + local durationFlag=false + if Duration then + durationFlag=true + else + Duration=300 + end + + local DCStask={ + id="Embarking", + params={ + selectedTransport=self:GetID(), + distributionFlag=true, + distribution=distribution, + groupsForEmbarking=gids, + durationFlag=durationFlag, + distribution=distribution, + duration=Duration, + x=Coordinate.x, + y=Coordinate.z, + } + } + + self:E(DCStask) + + return DCStask +end + -- TASKS FOR AIR CONTROLLABLES --- (AIR) Attack a Controllable. -- @param #CONTROLLABLE self @@ -890,17 +985,16 @@ end --- (AIR) Attack the Unit. -- @param #CONTROLLABLE self --- @param Wrapper.Unit#UNIT AttackUnit The UNIT. --- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @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 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 #number Altitude (optional) The altitude from where to attack. --- @param #boolean Visible (optional) not a clue. --- @param #number WeaponType (optional) The WeaponType. +-- @param Wrapper.Unit#UNIT AttackUnit The UNIT to be attacked +-- @param #boolean GroupAttack (Optional) If true, all units in the group will attack the Unit when found. Default false. +-- @param DCS#AI.Task.WeaponExpend WeaponExpend (Optional) Determines how many weapons 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) Limits maximal quantity of attack. The aicraft/controllable will not make more attacks 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 DCS#Azimuth Direction (Optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. +-- @param #number Altitude (Optional) The (minimum) altitude in meters from where to attack. Default 2000 m. +-- @param #number WeaponType (optional) The WeaponType. See [DCS Enumerator Weapon Type](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) on Hoggit. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskAttackUnit( AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, WeaponType ) - self:F2( { self.ControllableName, AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, WeaponType } ) +function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType) + self:F2({self.ControllableName, AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType}) local DCSTask DCSTask = { @@ -908,12 +1002,11 @@ function CONTROLLABLE:TaskAttackUnit( AttackUnit, GroupAttack, WeaponExpend, Att params = { unitId = AttackUnit:GetID(), groupAttack = GroupAttack or false, - visible = Visible or false, expend = WeaponExpend or "Auto", directionEnabled = Direction and true or false, - direction = Direction, + direction = math.rad(Direction or 0), altitudeEnabled = Altitude and true or false, - altitude = Altitude or 30, + altitude = Altitude or 2000, attackQtyLimit = AttackQty and true or false, attackQty = AttackQty, weaponType = WeaponType @@ -1798,6 +1891,7 @@ end +--[[ --- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. -- @param #CONTROLLABLE self @@ -1846,6 +1940,8 @@ function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) return DCSTask end +]] + --- This creates a Task element, with an action to call a function as part of a Wrapped Task. -- This Task can then be embedded at a Waypoint by calling the method @{#CONTROLLABLE.SetTaskWaypoint}. -- @param #CONTROLLABLE self diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 25b6a9f91..4b6e77bea 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -653,6 +653,49 @@ function GROUP:GetSize() return nil end +--- Count number of alive units in the group. +-- @param #GROUP self +-- @return #number Number of alive units +function GROUP:CountAliveUnits() + self:F3( { self.GroupName } ) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local units=self:GetUnits() + local n=0 + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + n=n+1 + end + end + return n + end + + return nil +end + +--- Get the first unit of the group which is alive. +-- @param #GROUP self +-- @return Wrapper.Unit#UNIT First unit alive. +function GROUP:GetFirstUnitAlive() + self:F3({self.GroupName}) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local units=self:GetUnits() + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + return unit + end + end + end + + return nil +end + + --- Returns the average velocity Vec3 vector. -- @param Wrapper.Group#GROUP self @@ -1463,6 +1506,16 @@ function GROUP:InitRandomizePositionRadius( OuterRadius, InnerRadius ) return self end +--- Set respawn coordinate. +-- @param #GROUP self +-- @param Core.Point#COORDINATE coordinate Coordinate where the group should be respawned. +-- @return #GROUP self +function GROUP:InitCoordinate(coordinate) + self:F({coordinate=coordinate}) + self.InitCoord=coordinate + 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 #boolean switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group. @@ -1603,6 +1656,11 @@ function GROUP:Respawn( Template, Reset ) end end + -- Coordinate where the group should be respawned. + if self.InitCoord then + GroupUnitVec3=self.InitCoord:GetVec3() + end + -- Altitude Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y @@ -1647,6 +1705,11 @@ function GROUP:Respawn( Template, Reset ) end end + -- Coordinate where the group should be respawned. + if self.InitCoord then + GroupUnitVec3=self.InitCoord:GetVec3() + end + -- Set altitude. Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y diff --git a/Moose Development/Moose/Wrapper/Static.lua b/Moose Development/Moose/Wrapper/Static.lua index 68c224922..1f2568a73 100644 --- a/Moose Development/Moose/Wrapper/Static.lua +++ b/Moose Development/Moose/Wrapper/Static.lua @@ -142,7 +142,9 @@ function STATIC:Destroy( GenerateEvent ) end - +--- Get DCS object of static of static. +-- @param #STATIC self +-- @return DCS static object function STATIC:GetDCSObject() local DCSStatic = StaticObject.getByName( self.StaticName ) @@ -172,45 +174,72 @@ function STATIC:GetUnits() end - - +--- Get threat level of static. +-- @param #STATIC self +-- @return #number Threat level 1. +-- @return #string "Static" function STATIC:GetThreatLevel() - return 1, "Static" end ---- Respawn the @{Wrapper.Unit} using a (tweaked) template of the parent Group. +--- Spawn the @{Wrapper.Static} at a specific coordinate and heading. -- @param #STATIC self -- @param Core.Point#COORDINATE Coordinate The coordinate where to spawn the new Static. --- @param #number Heading The heading of the unit respawn. -function STATIC:SpawnAt( Coordinate, Heading ) +-- @param #number Heading The heading of the static respawn in degrees. Default is 0 deg. +-- @param #number Delay Delay in seconds before the static is spawned. +function STATIC:SpawnAt( Coordinate, Heading, Delay ) - local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName ) + Heading=Heading or 0 + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.SpawnAt, {self, Coordinate, Heading}, Delay) + else + + local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName ) - SpawnStatic:SpawnFromPointVec2( Coordinate, Heading, self.StaticName ) + SpawnStatic:SpawnFromPointVec2( Coordinate, Heading, self.StaticName ) + + end end --- Respawn the @{Wrapper.Unit} at the same location with the same properties. -- This is useful to respawn a cargo after it has been destroyed. -- @param #STATIC self --- @param DCS#country.id countryid The country ID used for spawning the new static. -function STATIC:ReSpawn(countryid) +-- @param DCS#country.id countryid The country ID used for spawning the new static. Default is same as currently. +-- @param #number Delay Delay in seconds before static is respawned. +function STATIC:ReSpawn(countryid, Delay) - local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName, countryid ) + countryid=countryid or self:GetCountry() + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.ReSpawn, {self, countryid}, Delay) + else + + local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName, countryid ) - SpawnStatic:ReSpawn() + SpawnStatic:ReSpawn() + + end end --- Respawn the @{Wrapper.Unit} at a defined Coordinate with an optional heading. -- @param #STATIC self -- @param Core.Point#COORDINATE Coordinate The coordinate where to spawn the new Static. --- @param #number Heading The heading of the unit respawn. -function STATIC:ReSpawnAt( Coordinate, Heading ) +-- @param #number Heading The heading of the static respawn in degrees. Default is 0 deg. +-- @param #number Delay Delay in seconds before static is respawned. +function STATIC:ReSpawnAt( Coordinate, Heading, Delay ) - local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName ) + Heading=Heading or 0 + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.ReSpawnAt, {self, Coordinate, Heading}, Delay) + else - SpawnStatic:ReSpawnAt( Coordinate, Heading ) + local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName ) + + SpawnStatic:ReSpawnAt( Coordinate, Heading ) + end end diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 100f27206..1ef55065d 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -817,16 +817,22 @@ 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) + local DCSUnit = self:GetDCSObject() + if DCSUnit then + + -- 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 - - return self + + return nil end -- Is functions From d4f6ba2bdb91b6dc8e88d1de8620bbb364084939 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 12 Apr 2019 21:59:53 +0200 Subject: [PATCH 257/485] Airboss v0.9.9.8 --- Moose Development/Moose/Ops/Airboss.lua | 26 ++- .../Moose/Wrapper/Controllable.lua | 191 +++++++++--------- 2 files changed, 115 insertions(+), 102 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index c54bc60cb..1aa0b1f6f 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -226,6 +226,7 @@ -- @field #boolean trapsheet If true, players can save their trap sheets. -- @field #string trappath Path where to save the trap sheets. -- @field #string trapprefix File prefix for trap sheet files. +-- @field #number initialmaxalt Max altitude in meters to register in the inital zone. -- @extends Core.Fsm#FSM --- Be the boss! @@ -267,7 +268,9 @@ -- 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. +-- position, which is 3 NM astern of the boat. Note that you need to be below 1300 feet to be registered in the initial zone. +-- The altitude can be set via the function @{AIRBOSS.SetInitialMaxAlt}(*altitude*) function. +-- As described belwo, the initial zone can be smoked or flared via the AIRBOSS F10 Help radio menu. -- -- ### Landing Pattern -- @@ -1193,7 +1196,7 @@ AIRBOSS = { marshalradius = nil, airbossnice = nil, staticweather = nil, - windowcount = 0, + windowcount = 0, LSOdT = nil, senderac = nil, radiorelayLSO = nil, @@ -1221,6 +1224,7 @@ AIRBOSS = { trapsheet = nil, trappath = nil, trapprefix = nil, + initialmaxalt = nil, } --- Aircraft types capable of landing on carrier (human+AI). @@ -1850,7 +1854,10 @@ function AIRBOSS:New(carriername, alias) self:SetHoldingOffsetAngle() -- Set Marshal stack radius. Default 2.75 NM, which gives a diameter of 5.5 NM. - self:SetMarshalRadius() + self:SetMarshalRadius() + + -- Set max alt at initial. Default 1300 ft. + self:SetInitialMaxAlt() -- Default player skill EASY. self:SetDefaultPlayerSkill(AIRBOSS.Difficulty.EASY) @@ -2531,6 +2538,15 @@ function AIRBOSS:SetRefuelAI(lowfuelthreshold) return self end +--- Set max alitude to register flights in the initial zone. Aircraft above this altitude will not be registerered. +-- @param #AIRBOSS self +-- @param #number altitude Max alitude in feet. Default 1300 ft. +-- @return #AIRBOSS self +function AIRBOSS:SetInitialMaxAlt(altitude) + self.initialmaxalt=UTILS.FeetToMeters(altitude or 1300) + 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. @@ -8862,10 +8878,10 @@ function AIRBOSS:_Initial(playerData) local relheading=self:_GetRelativeHeading(playerData.unit, false) -- Alitude of player in feet. - local altitude=UTILS.MetersToFeet(playerData.unit:GetAltitude()) + local altitude=playerData.unit:GetAltitude() -- Check if player is in zone and flying roughly in the right direction. - if inzone and math.abs(relheading)<60 and altitude<=1300 then + if inzone and math.abs(relheading)<60 and altitude<=self.initialmaxalt then -- Send message for normal and easy difficulty. if playerData.showhints then diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 039afe898..7cfb56abb 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -830,100 +830,6 @@ function CONTROLLABLE:TaskEPLRS(SwitchOnOff, idx) end ---- Used in conjunction with the embarking task for a transport helicopter group. The Ground units will move to the specified location and wait to be picked up by a helicopter. --- The helicopter will then fly them to their dropoff point defined by another task for the ground forces; DisembarkFromTransport task. --- The controllable has to be an infantry group! --- @param #CONTROLLABLE self --- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. --- @param #number Radius Radius in meters. --- @return #table Embark to transport task. -function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius) - - local EmbarkToTransport = { - id="EmbarkToTransport ", - enabled=true, - auto=false, - params={ - x=Coordinate.x, - y=Coordinate.z, - zoneRadius=Radius, - selectedType="UH-1H", - } - } - - self:E(EmbarkToTransport) - return EmbarkToTransport -end - ---- Specifies the location an infantry group that is being transported by helicopters will be unloaded at. Used in conjunction with the EmbarkToTransport task. --- The CONTROLLABLE has to be an infantry group! --- @param #CONTROLLABLE self --- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. --- @param #number Radius Radius in meters. --- @return #table Embark to transport task. -function CONTROLLABLE:TaskDisembarkFromTransport(Coordinate, Radius) - - local DisembarkFromTransport={ - id="DisembarkFromTransport", - params = { - x=Coordinate.x, - y=Coordinate.y, - zoneRadius=Radius, - }} - - return DisembarkFromTransport -end - ---- Used in conjunction with the EmbarkToTransport task for a ground infantry group, the controlled helicopter flight will land at the specified coordinates, --- pick up boarding troops and transport them to that groups DisembarkFromTransport task. --- The CONTROLLABLE has to be a helicopter group! --- @param #CONTROLLABLE self --- @param Core.Set#SET_GROUP GroupSet Set of groups to be embarked by the controllable. --- @param Core.Point#COORDINATE Coordinate Coordinate of embarking. --- @param #number Duration Duration of embarking in seconds. --- @return #table Embarking task. -function CONTROLLABLE:TaskEmbarking(GroupSet, Coordinate, Duration) - - -- Create table of group IDs. - local gids={} - for _,_group in pairs(GroupSet:GetAliveSet()) do - local group=_group --Wrapper.Group#GROUP - table.insert(gids, group:GetID()) - end - - -- Group ID of controllable. - local id=self:GetID() - - -- Distribution - local distribution={} - distribution[id]=gids - - local durationFlag=false - if Duration then - durationFlag=true - else - Duration=300 - end - - local DCStask={ - id="Embarking", - params={ - selectedTransport=self:GetID(), - distributionFlag=true, - distribution=distribution, - groupsForEmbarking=gids, - durationFlag=durationFlag, - distribution=distribution, - duration=Duration, - x=Coordinate.x, - y=Coordinate.z, - } - } - - self:E(DCStask) - - return DCStask -end -- TASKS FOR AIR CONTROLLABLES --- (AIR) Attack a Controllable. @@ -1892,6 +1798,99 @@ end --[[ +--- Used in conjunction with the embarking task for a transport helicopter group. The Ground units will move to the specified location and wait to be picked up by a helicopter. +-- The helicopter will then fly them to their dropoff point defined by another task for the ground forces; DisembarkFromTransport task. +-- The controllable has to be an infantry group! +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. +-- @param #number Radius Radius in meters. +-- @return #table Embark to transport task. +function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius) + + local EmbarkToTransport = { + id="EmbarkToTransport", + params={ + x=Coordinate.x, + y=Coordinate.z, + zoneRadius=Radius, + --selectedType="UH-1H", + } + } + + self:E(EmbarkToTransport) + return EmbarkToTransport +end + +--- Used in conjunction with the EmbarkToTransport task for a ground infantry group, the controlled helicopter flight will land at the specified coordinates, +-- pick up boarding troops and transport them to that groups DisembarkFromTransport task. +-- The CONTROLLABLE has to be a helicopter group! +-- @param #CONTROLLABLE self +-- @param Core.Set#SET_GROUP GroupSet Set of groups to be embarked by the controllable. +-- @param Core.Point#COORDINATE Coordinate Coordinate of embarking. +-- @param #number Duration Duration of embarking in seconds. +-- @return #table Embarking task. +function CONTROLLABLE:TaskEmbarking(GroupSet, Coordinate, Duration) + + -- Create table of group IDs. + local gids={} + for _,_group in pairs(GroupSet:GetAliveSet()) do + local group=_group --Wrapper.Group#GROUP + table.insert(gids, group:GetID()) + end + + -- Group ID of controllable. + local id=self:GetID() + + -- Distribution + local distribution={} + distribution[id]=gids + + local durationFlag=false + if Duration then + durationFlag=true + else + Duration=300 + end + + local DCStask={ + id="Embarking", + params={ + selectedTransport=self:GetID(), + distributionFlag=true, + distribution=distribution, + groupsForEmbarking=gids, + durationFlag=durationFlag, + distribution=distribution, + duration=Duration, + x=Coordinate.x, + y=Coordinate.z, + } + } + + self:E(DCStask) + + return DCStask +end + +--- Specifies the location an infantry group that is being transported by helicopters will be unloaded at. Used in conjunction with the EmbarkToTransport task. +-- The CONTROLLABLE has to be an infantry group! +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. +-- @param #number Radius Radius in meters. +-- @return #table Embark to transport task. +function CONTROLLABLE:TaskDisembarkFromTransport(Coordinate, Radius) + + local DisembarkFromTransport={ + id="DisembarkFromTransport", + params = { + x=Coordinate.x, + y=Coordinate.y, + zoneRadius=Radius, + }} + + return DisembarkFromTransport +end +]] --- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. -- @param #CONTROLLABLE self @@ -1919,8 +1918,7 @@ function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) end --- (GROUND) Embark to a Transport landed at a location. - ---- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. +-- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Point The point where to wait. -- @param #number Radius The radius of the embarking zone around the Point. @@ -1940,7 +1938,6 @@ function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) return DCSTask end -]] --- This creates a Task element, with an action to call a function as part of a Wrapped Task. -- This Task can then be embedded at a Waypoint by calling the method @{#CONTROLLABLE.SetTaskWaypoint}. From 95b67550eb2afb6e18849251540f1370e3d79465 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 13 Apr 2019 10:07:55 +0200 Subject: [PATCH 258/485] Persian Gulf Airbases Update (Final) Thanks to Shadowze. --- Moose Development/Moose/Wrapper/Airbase.lua | 92 +++++++++++---------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 953ba2457..565bc22ad 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -218,64 +218,68 @@ AIRBASE.Normandy = { } --- These are all airbases of the Persion Gulf Map: --- +-- +-- * AIRBASE.PersianGulf.Abu_Dhabi_International_Airport -- * AIRBASE.PersianGulf.Abu_Musa_Island_Airport +-- * AIRBASE.PersianGulf.Al-Bateen_Airport +-- * AIRBASE.PersianGulf.Al_Ain_International_Airport +-- * AIRBASE.PersianGulf.Al_Dhafra_AB +-- * AIRBASE.PersianGulf.Al_Maktoum_Intl +-- * AIRBASE.PersianGulf.Al_Minhad_AB +-- * AIRBASE.PersianGulf.Bandar-e-Jask_airfield -- * AIRBASE.PersianGulf.Bandar_Abbas_Intl -- * AIRBASE.PersianGulf.Bandar_Lengeh --- * 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.Sirri_Island --- * AIRBASE.PersianGulf.Tunb_Kochak --- * AIRBASE.PersianGulf.Sir_Abu_Nuayr --- * AIRBASE.PersianGulf.Kerman_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 +-- * AIRBASE.PersianGulf.Kerman_Airport +-- * AIRBASE.PersianGulf.Khasab +-- * AIRBASE.PersianGulf.Kish_International_Airport +-- * AIRBASE.PersianGulf.Lar_Airbase +-- * AIRBASE.PersianGulf.Lavan_Island_Airport +-- * AIRBASE.PersianGulf.Liwa_Airbase +-- * AIRBASE.PersianGulf.Qeshm_Island +-- * AIRBASE.PersianGulf.Ras_Al_Khaimah_International_Airport +-- * AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport +-- * AIRBASE.PersianGulf.Sharjah_Intl +-- * AIRBASE.PersianGulf.Shiraz_International_Airport +-- * AIRBASE.PersianGulf.Sir_Abu_Nuayr +-- * AIRBASE.PersianGulf.Sirri_Island +-- * AIRBASE.PersianGulf.Tunb_Island_AFB +-- * AIRBASE.PersianGulf.Tunb_Kochak -- @field PersianGulf AIRBASE.PersianGulf = { + ["Abu_Dhabi_International_Airport"] = "Abu Dhabi International Airport", ["Abu_Musa_Island_Airport"] = "Abu Musa Island Airport", + ["Al_Ain_International_Airport"] = "Al Ain International Airport", + ["Al_Bateen_Airport"] = "Al-Bateen Airport", + ["Al_Dhafra_AB"] = "Al Dhafra AB", + ["Al_Maktoum_Intl"] = "Al Maktoum Intl", + ["Al_Minhad_AB"] = "Al Minhad AB", ["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", - ["Sir_Abu_Nuayr"] = "Sir Abu Nuayr", - ["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", - ["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", + ["Dubai_Intl"] = "Dubai Intl", + ["Fujairah_Intl"] = "Fujairah Intl", + ["Havadarya"] = "Havadarya", ["Jiroft_Airport"] = "Jiroft Airport", - } + ["Kerman_Airport"] = "Kerman Airport", + ["Khasab"] = "Khasab", + ["Kish_International_Airport"] = "Kish International Airport", + ["Lar_Airbase"] = "Lar Airbase", + ["Lavan_Island_Airport"] = "Lavan Island Airport", + ["Liwa_Airbase"] = "Liwa Airbase", + ["Qeshm_Island"] = "Qeshm Island", + ["Ras_Al_Khaimah_International_Airport"] = "Ras Al Khaimah International Airport", + ["Sas_Al_Nakheel_Airport"] = "Sas Al Nakheel Airport", + ["Sharjah_Intl"] = "Sharjah Intl", + ["Shiraz_International_Airport"] = "Shiraz International Airport", + ["Sir_Abu_Nuayr"] = "Sir Abu Nuayr", + ["Sirri_Island"] = "Sirri Island", + ["Tunb_Island_AFB"] = "Tunb Island AFB", + ["Tunb_Kochak"] = "Tunb Kochak", +} --- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". -- @type AIRBASE.ParkingSpot From d985d9a41d5241322b6fafd7d46699892aadc896 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 16 Apr 2019 19:49:46 +0200 Subject: [PATCH 259/485] WIP --- Moose Development/Moose/Core/Database.lua | 3 ++- Moose Development/Moose/Core/Event.lua | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 5a07d5f5a..be7a7c924 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -890,7 +890,7 @@ end -- @param #DATABASE self -- @param Core.Event#EVENTDATA Event function DATABASE:_EventOnBirth( Event ) - self:F2( { Event } ) + self:F( { Event } ) if Event.IniDCSUnit then if Event.IniObjectCategory == 3 then @@ -907,6 +907,7 @@ function DATABASE:_EventOnBirth( Event ) local PlayerName = Event.IniUnit:GetPlayerName() if PlayerName then self:I( { "Player Joined:", PlayerName } ) + self:AddClient( Event.IniDCSUnitName ) if not self.PLAYERS[PlayerName] then self:AddPlayer( Event.IniUnitName, PlayerName ) end diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 386169cb0..89cfc22cd 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -986,7 +986,7 @@ function EVENT:onEvent( Event ) local PriorityEnd = PriorityOrder == -1 and 1 or 5 if Event.IniObjectCategory ~= Object.Category.STATIC then - self:T( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) + self:F( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) end for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do From b21ff707ba8c579a72635bcf8e0236625f4d8a24 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 23 Apr 2019 19:59:11 +0300 Subject: [PATCH 260/485] Updates escort before plane trip. --- Moose Development/Moose/AI/AI_Escort.lua | 11 ++ Moose Development/Moose/AI/AI_Formation.lua | 8 +- Moose Development/Moose/Core/Point.lua | 2 +- Moose Development/Moose/Core/Set.lua | 33 +++++ Moose Development/Moose/Core/Spawn.lua | 137 ++++++++++++-------- 5 files changed, 129 insertions(+), 62 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 07bedf8c8..784799ef9 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -286,6 +286,8 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) return self end +--- @param #AI_ESCORT self +-- @param Core.Set#SET_GROUP EscortGroupSet function AI_ESCORT:onafterStart( EscortGroupSet ) self:E("Start") @@ -300,6 +302,15 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) EscortGroup:OptionROEOpenFire() end ) + + local LeaderEscort = EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP + + local Report = REPORT:New( "Escorts Reporting." ) + Report:Add( "Current coordinate: " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) + Report:Add( "Configuration: " .. EscortGroupSet:GetUnitTypeNames():Text( ", " ) ) + Report:Add( "Joining Up ..." ) + + LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.EscortUnit ) end diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index edf3afe96..e0981a599 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -740,7 +740,7 @@ end -- @return #AI_FORMATION function AI_FORMATION:onafterFormationLeftLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,ZStart,ZSpace) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,-ZStart,-ZSpace) return self end @@ -759,7 +759,7 @@ end -- @return #AI_FORMATION function AI_FORMATION:onafterFormationRightLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,-ZStart,-ZSpace) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,ZStart,ZSpace) return self end @@ -778,7 +778,7 @@ end -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. function AI_FORMATION:onafterFormationLeftWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,ZStart,ZSpace) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,-ZStart,-ZSpace) return self end @@ -798,7 +798,7 @@ end -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. function AI_FORMATION:onafterFormationRightWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,-ZStart,-ZSpace) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,ZStart,ZSpace) return self end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index a0cebabb4..8e14da386 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -2050,7 +2050,7 @@ do -- COORDINATE -- * Uses default settings in 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 Wrapper.Controllable#CONTROLLABLE Controllable The controllable to retrieve the settings from, otherwise the default settings will be chosen. -- @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. -- @param Tasking.Task#TASK Task The task for which coordinates need to be calculated. -- @return #string The coordinate Text in the configured coordinate system. diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 5a5f47adb..f0e24adf5 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -924,7 +924,40 @@ do -- SET_GROUP return AliveSet.Set or {} end + + --- Returns a report of of unit types. + -- @param #SET_GROUP self + -- @return Core.Report#REPORT A report of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. + function SET_GROUP:GetUnitTypeNames() + self:F2() + local MT = {} -- Message Text + local UnitTypes = {} + + local ReportUnitTypes = REPORT:New() + + for GroupID, GroupData in pairs( self:GetSet() ) do + local Units = GroupData:GetUnits() + for UnitID, UnitData in pairs( Units ) do + if UnitData:IsAlive() then + local UnitType = UnitData:GetTypeName() + + if not UnitTypes[UnitType] then + UnitTypes[UnitType] = 1 + else + UnitTypes[UnitType] = UnitTypes[UnitType] + 1 + end + end + end + end + + for UnitTypeID, UnitType in pairs( UnitTypes ) do + ReportUnitTypes:Add( UnitType .. " of " .. UnitTypeID ) + end + + return ReportUnitTypes + 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 diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index d65aae7a2..6724e1b89 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1156,6 +1156,18 @@ function SPAWN:ReSpawn( SpawnIndex ) return SpawnGroup end + +--- Set the spawn index to a specified index number. +-- This method can be used to "reset" the spawn counter to a specific index number. +-- This will actually enable a respawn of groups from the specific index. +-- @param #SPAWN self +-- @param #string SpawnIndex The index of the group from where the spawning will start again. The default value would be 0, which means a complete reset of the spawnindex. +-- @return #SPAWN self +function SPAWN:SetSpawnIndex( SpawnIndex ) + self.SpawnIndex = SpawnIndex or 0 +end + + --- Will spawn a group with a specified index number. -- Uses @{DATABASE} global object defined in MOOSE. -- @param #SPAWN self @@ -1465,6 +1477,11 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate if SpawnTemplate then + + -- Check if the aircraft with the specified SpawnIndex is already spawned. + -- If yes, ensure that the aircraft is spawned at the same aircraft spot. + + local GroupAlive = self:GetGroupFromIndex( self.SpawnIndex ) -- Debug output self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) @@ -1535,7 +1552,8 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT local spots -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship. - if spawnonground then + if spawnonground and not SpawnTemplate.parked then + -- Number of free parking spots. local nfree=0 @@ -1708,67 +1726,72 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT end - -- Translate the position of the Group Template to the Vec3. - for UnitID = 1, nunits do - self:T2('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) - - -- Template of the current unit. - local UnitTemplate = SpawnTemplate.units[UnitID] - - -- Tranlate position and preserve the relative position/formation of all aircraft. - local SX = UnitTemplate.x - local SY = UnitTemplate.y - local BX = SpawnTemplate.route.points[1].x - local BY = SpawnTemplate.route.points[1].y - local TX = PointVec3.x + (SX-BX) - local TY = PointVec3.z + (SY-BY) - - if spawnonground then - - -- Ships and FARPS seem to have a build in queue. - if spawnonship or spawnonfarp or spawnonrunway then - - self:T(string.format("Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + if not SpawnTemplate.parked then + -- Translate the position of the Group Template to the Vec3. - -- Spawn on ship. We take only the position of the ship. - SpawnTemplate.units[UnitID].x = PointVec3.x --TX - SpawnTemplate.units[UnitID].y = PointVec3.z --TY - SpawnTemplate.units[UnitID].alt = PointVec3.y - - else + SpawnTemplate.parked = true - self:T(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) - - -- Get coordinates of parking spot. - SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x - SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z - SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y - - --parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) - end + for UnitID = 1, nunits do + self:T2('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + + -- Template of the current unit. + local UnitTemplate = SpawnTemplate.units[UnitID] + + -- Tranlate position and preserve the relative position/formation of all aircraft. + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = PointVec3.x + (SX-BX) + local TY = PointVec3.z + (SY-BY) - else - - self:T(string.format("Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + if spawnonground then + + -- Ships and FARPS seem to have a build in queue. + if spawnonship or spawnonfarp or spawnonrunway then + + self:T(string.format("Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn on ship. We take only the position of the ship. + SpawnTemplate.units[UnitID].x = PointVec3.x --TX + SpawnTemplate.units[UnitID].y = PointVec3.z --TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + else + + self:T(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + + -- Get coordinates of parking spot. + SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x + SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z + SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y + + --parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + end + + else - -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. - SpawnTemplate.units[UnitID].x = TX - SpawnTemplate.units[UnitID].y = TY - SpawnTemplate.units[UnitID].alt = PointVec3.y + self:T(string.format("Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + end + + -- Parking spot id. + UnitTemplate.parking = nil + UnitTemplate.parking_id = nil + if parkingindex[UnitID] then + UnitTemplate.parking = parkingindex[UnitID] + end + -- Debug output. + self:T2(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) + self:T2(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) + self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) end - - -- Parking spot id. - UnitTemplate.parking = nil - UnitTemplate.parking_id = nil - if parkingindex[UnitID] then - UnitTemplate.parking = parkingindex[UnitID] - end - - -- Debug output. - self:T2(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) - self:T2(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) - self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) end -- Set gereral spawnpoint position. @@ -2088,7 +2111,7 @@ end function SPAWN:InitUnControlled( UnControlled ) self:F2( { self.SpawnTemplatePrefix, UnControlled } ) - self.SpawnUnControlled = UnControlled or true + self.SpawnUnControlled = ( UnControlled == true ) and true or nil for SpawnGroupID = 1, self.SpawnMaxGroups do self.SpawnGroups[SpawnGroupID].UnControlled = self.SpawnUnControlled From 3dbb013e16d5f3dcb2e0481c4e4df031b50010cb Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 23 Apr 2019 19:13:03 +0200 Subject: [PATCH 261/485] Ops and Range RANGE v1.3.0 - Generalized weapon handling. - Decreased time before weapon tracking starts from 1.0 to 0.1 sec. RESCUE HELO v1.0.6 - Got rid of IsAlive() check in rescue operation. AIRBOSS v0.9.9.9.9 - Fixed bug in trap sheet output. --- Moose Development/Moose/Functional/Range.lua | 34 +++-- Moose Development/Moose/Ops/Airboss.lua | 136 +++++++++---------- Moose Development/Moose/Ops/RescueHelo.lua | 21 +-- 3 files changed, 100 insertions(+), 91 deletions(-) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index c3f9ee12e..62e39cadf 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -290,7 +290,7 @@ RANGE.id="RANGE | " --- Range script version. -- @field #string version -RANGE.version="1.2.5" +RANGE.version="1.3.0" --TODO list: --TODO: Verbosity level for messages. @@ -1183,25 +1183,39 @@ end function RANGE:OnEventShot(EventData) self:F({eventshot = EventData}) + -- Nil checks. + if EventData.Weapon==nil then + return + end + if EventData.IniDCSUnit==nil then + return + end + -- Weapon data. local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName local _weaponStrArray = UTILS.Split(_weapon,"%.") local _weaponName = _weaponStrArray[#_weaponStrArray] + -- Weapon descriptor. + local desc=EventData.Weapon:getDesc() + + -- Weapon category: 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X) + local weaponcategory=desc.category + -- 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) - + self:T(RANGE.id.."EVENT SHOT: Weapon cate = "..weaponcategory) -- 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") + --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 _missiles=string.match(_weapon, "weapons.missiles") or _viggen + local _bombs = weaponcategory==Weapon.Category.BOMB --string.match(_weapon, "weapons.bombs") + local _rockets = weaponcategory==Weapon.Category.ROCKET --string.match(_weapon, "weapons.nurs") + local _missiles = weaponcategory==Weapon.Category.MISSILE --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) @@ -1221,8 +1235,8 @@ function RANGE:OnEventShot(EventData) self:T(RANGE.id..string.format("Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR/1000)) end - -- Only track if distance player to range is < 25 km. - if _track and dPR<=self.BombtrackThreshold then + -- Only track if distance player to range is < 25 km. Also check that a player shot. No need to track AI weapons. + if _track and dPR<=self.BombtrackThreshold and _unit and _playername then -- 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())) @@ -1346,8 +1360,8 @@ function RANGE:OnEventShot(EventData) 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) + self:T(RANGE.id..string.format("Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername)) + timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime()+0.1) end --if _track (string.match) and player-range distance < threshold. diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 1aa0b1f6f..792194f9b 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1671,7 +1671,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.8" +AIRBOSS.version="0.9.9.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -2408,7 +2408,7 @@ function AIRBOSS:DeleteAllRecoveryWindows(delay) -- Loop over all recovery windows. for _,recovery in pairs(self.recoverytimes) do - env.info("FF deleting recovery window ID "..tostring(recovery.ID)) + self:I(self.lid..string.format("Deleting recovery window ID %s", tostring(recovery.ID))) self:DeleteRecoveryWindow(recovery, delay) end @@ -7978,29 +7978,6 @@ end -- EVENT functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- General DCS 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("Skipping onEvent. Event or Event.initiator unknown.") - return true - end - if Unit.getByName(Event.initiator:getName()) == nil then - self:T3("Skipping onEvent. Initiator unit name unknown.") - return true - end - ]] - - --env.info("FF DCS Event") - --self:E(Event) - -end - - --- Airboss event handler for event birth. -- @param #AIRBOSS self -- @param Core.Event#EVENTDATA EventData @@ -9394,6 +9371,8 @@ function AIRBOSS:_GetGrooveData(playerData) groovedata.Grade=Gg groovedata.GradePoints=Gp groovedata.GradeDetail=Gd + + --env.info(string.format(", %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f", groovedata.Time, groovedata.Rho, groovedata.X, groovedata.Alt, groovedata.GSE, groovedata.LUE, groovedata.AoA)) return groovedata end @@ -9439,7 +9418,7 @@ function AIRBOSS:_Final(playerData, nocheck) -- TODO: could add angled approach if lineup<5 and relhead>5. This would mean the player has not turned in correctly! -- Groove data. - playerData.groove.X0=groovedata + playerData.groove.X0=UTILS.DeepCopy(groovedata) -- Set time stamp. Next call in 4 seconds. playerData.Tlso=timer.getTime() @@ -9447,6 +9426,9 @@ function AIRBOSS:_Final(playerData, nocheck) -- Next step: X start. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) end + + -- Groovedata step. + groovedata.Step=playerData.step end @@ -9454,15 +9436,6 @@ end -- @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=self:_GetDistances(playerData.unit) - - -- Check abort conditions. - if self:_CheckAbort(X, Z, self.Groove) then - self:_AbortPattern(playerData, X, Z, self.Groove, true) - return - end -- Ranges in the groove. local RX0=UTILS.NMToMeters(1.000) -- Everything before X 1.00 = 1852 m @@ -9475,7 +9448,17 @@ function AIRBOSS:_Groove(playerData) local groovedata=self:_GetGrooveData(playerData) -- Add data to trapsheet. - table.insert(playerData.trapsheet, groovedata) + table.insert(playerData.trapsheet, groovedata) + + -- Coords. + local X=groovedata.X + local Z=groovedata.Z + + -- Check abort conditions. + if self:_CheckAbort(groovedata.X, groovedata.Z, self.Groove) then + self:_AbortPattern(playerData, groovedata.X, groovedata.Z, self.Groove, true) + return + end -- Shortcuts. local rho=groovedata.Rho @@ -9500,7 +9483,7 @@ function AIRBOSS:_Groove(playerData) self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, false, nil, 2, true) -- Store data. - playerData.groove.XX=groovedata + playerData.groove.XX=UTILS.DeepCopy(groovedata) -- This is a valid approach and player did not miss any important steps in the pattern. playerData.valid=true @@ -9511,7 +9494,7 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then -- Store data. - playerData.groove.IM=groovedata + playerData.groove.IM=UTILS.DeepCopy(groovedata) -- Next step: in close. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IC) @@ -9519,7 +9502,7 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RIC and playerData.step==AIRBOSS.PatternStep.GROOVE_IC then -- Store data. - playerData.groove.IC=groovedata + playerData.groove.IC=UTILS.DeepCopy(groovedata) -- Next step: AR at the ramp. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_AR) @@ -9527,7 +9510,7 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then -- Store data. - playerData.groove.AR=groovedata + playerData.groove.AR=UTILS.DeepCopy(groovedata) -- Next step: in the wires. if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then @@ -9539,7 +9522,7 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AL then -- Store data. - playerData.groove.AL=groovedata + playerData.groove.AL=UTILS.DeepCopy(groovedata) -- Get zone abeam LDG spot. local ZoneALS=self:_GetZoneAbeamLandingSpot() @@ -9569,7 +9552,7 @@ function AIRBOSS:_Groove(playerData) elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_LC then -- Store data. - playerData.groove.LC=groovedata + playerData.groove.LC=UTILS.DeepCopy(groovedata) -- Get zone primary LDG spot. local ZoneLS=self:_GetZoneLandingSpot() @@ -9625,25 +9608,31 @@ function AIRBOSS:_Groove(playerData) end + -- Groovedata step. + groovedata.Step=playerData.step + ----------------- -- Groove Data -- ----------------- - - -- Get groove step short hand of the previous step. - local gs=self:_GS(playerData.step, -1) -- 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 Lineup error at %s: LUE %.3f>%.3f.", gs, lineupError, gd.LUE)) + self:T(self.lid..string.format("Got bigger LUE at step %s, d=%.3f: LUE %.3f>%.3f", gs, d, lineupError, gd.LUE)) gd.LUE=lineupError end @@ -9651,22 +9640,19 @@ function AIRBOSS:_Groove(playerData) 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)) + self:T(self.lid..string.format("Got Fly through DOWN at step %s, d=%.3f: Max GSE=%.3f, lower GSE=%.3f", gs, d, 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)) + self:E(self.lid..string.format("Got Fly through UP at step %s, d=%.3f: Min GSE=%.3f, lower GSE=%.3f", gs, d, gd.GSE, glideslopeError)) end -- Update max deviation of glideslope error. if math.abs(glideslopeError)>math.abs(gd.GSE) then - self:T(self.lid..string.format("Got bigger glideslope error at %s: GSE |%.3f|>|%.3f|.", gs, glideslopeError, gd.GSE)) + self:T(self.lid..string.format("Got bigger GSE at step %s, d=%.3f: GSE |%.3f|>|%.3f|", gs, d, glideslopeError, gd.GSE)) gd.GSE=glideslopeError end - -- Get current AoA. - local aoa=playerData.unit:GetAoA() - -- Get aircraft AoA parameters. local aircraftaoa=self:_GetAircraftAoA(playerData) @@ -9674,11 +9660,14 @@ function AIRBOSS:_Groove(playerData) 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 + if math.abs(AoA-aoaopt)>math.abs(gd.AoA-aoaopt) then + self:T(self.lid..string.format("Got bigger AoA error at step %s, d=%.3f: AoA %.3f>%.3f.", gs, d, AoA, gd.AoA)) + gd.AoA=AoA end + --local gs2=self:_GS(groovedata.Step, -1) + --env.info(string.format("groovestep %s %s d=%.3f NM: GSE=%.3f %.3f, LUE=%.3f %.3f, AoA=%.3f %.3f", gs, gs2, d, groovedata.GSE, gd.GSE, groovedata.LUE, gd.LUE, groovedata.AoA, gd.AoA)) + end --------------- @@ -10836,8 +10825,9 @@ function AIRBOSS:_AttitudeMonitor(playerData) -- Get carrier velocity in km/h. local vcarrier=self.carrier:GetVelocityKMH() -- Speed difference. - local dv=math.abs(vplayer-vcarrier) - text=text..string.format("\nDist=%.1f m Alt=%.1f m delta|V|=%.1f km/h", dist, self:_GetAltCarrier(playerData.unit), dv) + local dv=math.abs(vplayer-vcarrier) + local alt=self:_GetAltCarrier(playerData.unit) + text=text..string.format("\nDist=%.1f m Alt=%.1f m delta|V|=%.1f km/h", dist, alt, dv) -- If in the groove, provide line up and glide slope error. if playerData.step==AIRBOSS.PatternStep.FINAL or playerData.step==AIRBOSS.PatternStep.GROOVE_XX or @@ -10849,7 +10839,6 @@ function AIRBOSS:_AttitudeMonitor(playerData) playerData.step==AIRBOSS.PatternStep.GROOVE_IW then local lue=self:_Lineup(playerData.unit, true) local gle=self:_Glideslope(playerData.unit) - --local gle2=self:_Glideslope2(playerData.unit) text=text..string.format("\nGamma=%.1f° | Rho=%.1f°", relhead, phi) text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f Units", lue, gle, self:_AoADeg2Units(playerData, aoa)) local grade, points, analysis=self:_LSOgrade(playerData) @@ -10885,7 +10874,7 @@ function AIRBOSS:_Glideslope(unit, optangle) -- Altitude of unit corrected by the deck height of the carrier. local h=self:_GetAltCarrier(unit) - + -- Harrier should be 40-50 ft above the deck. if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then h=unit:GetAltitude()-(UTILS.FeetToMeters(50)+self.carrierparam.deckheight+2) @@ -10893,16 +10882,10 @@ function AIRBOSS:_Glideslope(unit, optangle) -- Glide slope. local glideslope=math.atan(h/x) - + -- Glide slope (error) in degrees. local gs=math.deg(glideslope)-optangle - - -- Debug. - self:T2(self.lid..string.format("Glide slope error = %.1f, x=%.1f h=%.1f", gs, x, h)) - - --local gs2=self:_Glideslope2(unit) - --self:E(self.lid..string.format("Glide slope error = %.1f =%.1f, x=%.1f h=%.1f", gs, gs2, x, h)) - + return gs end @@ -10966,10 +10949,11 @@ function AIRBOSS:_Lineup(unit, runway) local C=UTILS.VecSubstract(A, B) -- Only in 2D plane. - C.y=0 + C.y=0.0 -- Orientation of carrier. - local X=self.carrier:GetOrientationX() + local X=self.carrier:GetOrientationX() + X.y=0.0 -- Rotate orientation to angled runway. if runway then @@ -10981,6 +10965,7 @@ function AIRBOSS:_Lineup(unit, runway) -- Orientation of carrier. local Z=self.carrier:GetOrientationZ() + Z.y=0.0 -- Rotate orientation to angled runway. if runway then @@ -10990,8 +10975,10 @@ function AIRBOSS:_Lineup(unit, runway) -- Projection of player pos on z component. local z=UTILS.VecDot(Z, C) - --- + --- + local lineup=math.deg(math.atan2(z, x)) + --[[ -- Position of the aircraft in the new coordinate system. local a={x=x, y=0, z=z} @@ -11003,7 +10990,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)) - + ]] + return lineup end @@ -17001,8 +16989,10 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) local g0=playerData.trapsheet[1] --#AIRBOSS.GrooveData local T0=g0.Time - for _,_groove in ipairs(playerData.trapsheet) do - local groove=_groove --#AIRBOSS.GrooveData + --for _,_groove in ipairs(playerData.trapsheet) do + for i=1,#playerData.trapsheet do + --local groove=_groove --#AIRBOSS.GrooveData + local groove=playerData.trapsheet[i] local t=groove.Time-T0 local a=UTILS.MetersToNM(groove.Rho or 0) local b=-groove.X or 0 diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index cabe8390f..f2fa53b80 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -235,7 +235,7 @@ RESCUEHELO.UID=0 --- Class version. -- @field #string version -RESCUEHELO.version="1.0.5" +RESCUEHELO.version="1.0.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -776,13 +776,18 @@ function RESCUEHELO:_OnEventCrashOrEject(EventData) -- Debug. 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 + self:I(self.lid..text) + + -- Get coordinate of unit. + local coord=unit:GetCoordinate() + if coord and self.rescuezone:IsCoordinateInZone(coord) then + + -- This does not seem to work any more. Is:Alive returns flase on ejection. + -- 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() + --local coord=unit:GetCoordinate() -- Debug mark on map. if self.Debug then @@ -793,7 +798,7 @@ function RESCUEHELO:_OnEventCrashOrEject(EventData) 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 and rightcoalition then + if self:IsRunning() and self.rescueon and rightcoalition then self:Rescue(coord) end @@ -1091,7 +1096,7 @@ function RESCUEHELO:onafterRescue(From, Event, To, RescueCoord) -- Debug message. local text=string.format("Helo %s is send to rescue mission.", self.helo:GetName()) MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) - self:T(self.lid..text) + self:I(self.lid..text) -- Waypoint array. local wp={} From 315d6bf82a1ae4fafc5085acc223caefb1970848 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 23 Apr 2019 20:33:21 +0300 Subject: [PATCH 262/485] ParkAtAirbase new method. --- Moose Development/Moose/Core/Spawn.lua | 364 +++++++++++++++++++++++++ 1 file changed, 364 insertions(+) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 6724e1b89..615bfcffe 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1824,6 +1824,370 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT return nil end + +--- Will park a group at an @{Wrapper.Airbase}. +-- This method is mostly advisable to be used if you want to simulate parking units at an airbase and be visible. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- +-- All groups that are in the spawn collection and that are alive, and not in the air, are parked. +-- +-- The @{Wrapper.Airbase#AIRBASE} object must refer to a valid airbase known in the sim. +-- You can use the following enumerations to search for the pre-defined airbases on the current known maps of DCS: +-- +-- * @{Wrapper.Airbase#AIRBASE.Caucasus}: The airbases on the Caucasus map. +-- * @{Wrapper.Airbase#AIRBASE.Nevada}: The airbases on the Nevada (NTTR) map. +-- * @{Wrapper.Airbase#AIRBASE.Normandy}: The airbases on the Normandy map. +-- +-- Use the method @{Wrapper.Airbase#AIRBASE.FindByName}() to retrieve the airbase object. +-- The known AIRBASE objects are automatically imported at mission start by MOOSE. +-- Therefore, there isn't any New() constructor defined for AIRBASE objects. +-- +-- Ships and Farps are added within the mission, and are therefore not known. +-- For these AIRBASE objects, there isn't an @{Wrapper.Airbase#AIRBASE} enumeration defined. +-- You need to provide the **exact name** of the airbase as the parameter to the @{Wrapper.Airbase#AIRBASE.FindByName}() method! +-- +-- @param #SPAWN self +-- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group. +-- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}. +-- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactily these spots! +-- @return #nil Nothing is returned! +-- @usage +-- Spawn_Plane = SPAWN:New( "Plane" ) +-- Spawn_Plane:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ) ) +-- +-- Spawn_Heli = SPAWN:New( "Heli") +-- +-- Spawn_Heli:ParkAtAirbase( AIRBASE:FindByName( "FARP Cold" ) ) +-- +-- Spawn_Heli:ParkAtAirbase( AIRBASE:FindByName( "Carrier" ) ) +-- +-- Spawn_Plane:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), AIRBASE.TerminalType.OpenBig ) +-- +function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, R2.4, R2.5 + self:F( { self.SpawnTemplatePrefix, SpawnAirbase, TerminalType } ) + + -- Get position of airbase. + local PointVec3 = SpawnAirbase:GetCoordinate() + self:T2(PointVec3) + + -- Set take off type. Default is hot. + local Takeoff = SPAWN.Takeoff.Cold + + for SpawnIndex = 1, self.SpawnMaxGroups do + + -- Get group template. + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + + if SpawnTemplate then + + -- Check if the aircraft with the specified SpawnIndex is already spawned. + -- If yes, ensure that the aircraft is spawned at the same aircraft spot. + + local GroupAlive = self:GetGroupFromIndex( SpawnIndex ) + + -- Debug output + self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) + + -- Template group, unit and its attributes. + local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) + local TemplateUnit=TemplateGroup:GetUnit(1) + local ishelo=TemplateUnit:HasAttribute("Helicopters") + local isbomber=TemplateUnit:HasAttribute("Bombers") + local istransport=TemplateUnit:HasAttribute("Transports") + local isfighter=TemplateUnit:HasAttribute("Battleplanes") + + -- Number of units in the group. With grouping this can actually differ from the template group size! + local nunits=#SpawnTemplate.units + + -- First waypoint of the group. + local SpawnPoint = SpawnTemplate.route.points[1] + + -- These are only for ships and FARPS. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Get airbase ID and category. + local AirbaseID = SpawnAirbase:GetID() + local AirbaseCategory = SpawnAirbase:GetDesc().category + self:F( { AirbaseCategory = AirbaseCategory } ) + + -- Set airdromeId. + if AirbaseCategory == Airbase.Category.SHIP then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.HELIPAD then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.AIRDROME then + SpawnPoint.airdromeId = AirbaseID + end + + -- Set waypoint type/action. + SpawnPoint.alt = 0 + SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + + -- Check if we spawn on ground. + local spawnonground=not (Takeoff==SPAWN.Takeoff.Air) + self:T({spawnonground=spawnonground, TOtype=Takeoff, TOair=Takeoff==SPAWN.Takeoff.Air}) + + -- Check where we actually spawn if we spawn on ground. + local spawnonship=false + local spawnonfarp=false + local spawnonrunway=false + local spawnonairport=false + if spawnonground then + if AirbaseCategory == Airbase.Category.SHIP then + spawnonship=true + elseif AirbaseCategory == Airbase.Category.HELIPAD then + spawnonfarp=true + elseif AirbaseCategory == Airbase.Category.AIRDROME then + spawnonairport=true + end + spawnonrunway=Takeoff==SPAWN.Takeoff.Cold + end + + -- Array with parking spots coordinates. + local parkingspots={} + local parkingindex={} + local spots + + -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship. + if spawnonground and not SpawnTemplate.parked then + + + -- Number of free parking spots. + local nfree=0 + + -- Set terminal type. + local termtype=TerminalType + + -- Scan options. Might make that input somehow. + local scanradius=50 + local scanunits=true + local scanstatics=true + local scanscenery=false + local verysafe=false + + -- Number of free parking spots at the airbase. + if spawnonship or spawnonfarp or spawnonrunway then + -- These places work procedural and have some kind of build in queue ==> Less effort. + self:T(string.format("Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype, true) + spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype, true) + elseif Parkingdata~=nil then + -- Parking data explicitly set by user as input parameter. + nfree=#Parkingdata + spots=Parkingdata + else + if ishelo then + if termtype==nil then + -- Helo is spawned. Try exclusive helo spots first. + self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly)) + spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) + nfree=#spots + if nfree=1 then + + -- All units get the same spot. DCS takes care of the rest. + for i=1,nunits do + table.insert(parkingspots, spots[1].Coordinate) + table.insert(parkingindex, spots[1].TerminalID) + end + -- This is actually used... + PointVec3=spots[1].Coordinate + + else + -- If there is absolutely no spot ==> air start! + _notenough=true + end + + elseif spawnonairport then + + if nfree>=nunits then + + for i=1,nunits do + table.insert(parkingspots, spots[i].Coordinate) + table.insert(parkingindex, spots[i].TerminalID) + end + + else + -- Not enough spots for the whole group ==> air start! + _notenough=true + end + end + + -- Not enough spots ==> Prepare airstart. + if _notenough then + + if not self.SpawnUnControlled then + else + self:E(string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + return nil + end + end + + else + + end + + if not SpawnTemplate.parked then + -- Translate the position of the Group Template to the Vec3. + + SpawnTemplate.parked = true + + for UnitID = 1, nunits do + self:T2('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + + -- Template of the current unit. + local UnitTemplate = SpawnTemplate.units[UnitID] + + -- Tranlate position and preserve the relative position/formation of all aircraft. + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = PointVec3.x + (SX-BX) + local TY = PointVec3.z + (SY-BY) + + if spawnonground then + + -- Ships and FARPS seem to have a build in queue. + if spawnonship or spawnonfarp or spawnonrunway then + + self:T(string.format("Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn on ship. We take only the position of the ship. + SpawnTemplate.units[UnitID].x = PointVec3.x --TX + SpawnTemplate.units[UnitID].y = PointVec3.z --TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + else + + self:T(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + + -- Get coordinates of parking spot. + SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x + SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z + SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y + + --parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + end + + else + + self:T(string.format("Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + end + + -- Parking spot id. + UnitTemplate.parking = nil + UnitTemplate.parking_id = nil + if parkingindex[UnitID] then + UnitTemplate.parking = parkingindex[UnitID] + end + + -- Debug output. + self:T2(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) + self:T2(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) + self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + end + end + + -- Set gereral spawnpoint position. + SpawnPoint.x = PointVec3.x + SpawnPoint.y = PointVec3.z + SpawnPoint.alt = PointVec3.y + + SpawnTemplate.x = PointVec3.x + SpawnTemplate.y = PointVec3.z + + -- Spawn group. + local GroupSpawned = self:SpawnWithIndex( SpawnIndex ) + + -- 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 ) + end + end + + -- Check if we accidentally spawned on the runway. Needs to be schedules, because group is not immidiately alive. + if Takeoff~=SPAWN.Takeoff.Runway and Takeoff~=SPAWN.Takeoff.Air and spawnonairport then + SCHEDULER:New(nil, AIRBASE.CheckOnRunWay, {SpawnAirbase, GroupSpawned, 75, true} , 1.0) + end + + return GroupSpawned + end + end + + return nil +end + --- Will spawn a group from a Vec3 in 3D space. -- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. From 49217291add22064b8629f20be2ade1bc6fe0f50 Mon Sep 17 00:00:00 2001 From: TommyC81 Date: Wed, 24 Apr 2019 17:22:41 +0400 Subject: [PATCH 263/485] Corrected AIRBASE name comment. Corrected array name for Bandar-e-Jask Airfield. --- Moose Development/Moose/Wrapper/Airbase.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 565bc22ad..8b1efea83 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -226,7 +226,7 @@ AIRBASE.Normandy = { -- * AIRBASE.PersianGulf.Al_Dhafra_AB -- * AIRBASE.PersianGulf.Al_Maktoum_Intl -- * AIRBASE.PersianGulf.Al_Minhad_AB --- * AIRBASE.PersianGulf.Bandar-e-Jask_airfield +-- * AIRBASE.PersianGulf.Bandar_e_Jask_airfield -- * AIRBASE.PersianGulf.Bandar_Abbas_Intl -- * AIRBASE.PersianGulf.Bandar_Lengeh -- * AIRBASE.PersianGulf.Dubai_Intl @@ -931,4 +931,4 @@ function AIRBASE._CheckTerminalType(Term_Type, termtype) end return match -end \ No newline at end of file +end From b4d6f78e42587b7a4c6a8dea1b4d743693cc855a Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 25 Apr 2019 08:59:07 +0300 Subject: [PATCH 264/485] WIP, need to make a new class next week, AI_ESCORT_REQUEST. Need to solve the issue with the formation flying uof multiple escort classes working together. Planes may not crash into each other. --- Moose Development/Moose/AI/AI_Escort.lua | 275 +++----------------- Moose Development/Moose/AI/AI_Formation.lua | 6 +- Moose Development/Moose/Core/Spawn.lua | 36 ++- Moose Development/Moose/Modules.lua | 1 + Moose Development/Moose/Utilities/Enums.lua | 9 + 5 files changed, 78 insertions(+), 249 deletions(-) create mode 100644 Moose Development/Moose/Utilities/Enums.lua diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 784799ef9..59932b443 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -250,7 +250,7 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) self.GT1 = 0 - self.FlightMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Flight" ) + self.FlightMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortName ) EscortGroupSet:ForEachGroup( --- @param Core.Group#GROUP EscortGroup @@ -271,17 +271,6 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) end ) - EscortGroupSet:ForSomeGroup( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName() ) - end - ) - - - self.Detection = DETECTION_AREAS:New( EscortGroupSet, 5000 ) - - self.Detection:Start() return self end @@ -295,7 +284,7 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) EscortGroupSet:ForEachGroup( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName() ) + --EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName() ) EscortGroup:WayPointInitialize( 1 ) EscortGroup:OptionROTVertical() @@ -311,6 +300,10 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) Report:Add( "Joining Up ..." ) LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.EscortUnit ) + + self.Detection = DETECTION_AREAS:New( EscortGroupSet, 5000 ) + + self.Detection:__Start( 30 ) end @@ -364,7 +357,7 @@ function AI_ESCORT:Menus() self:MenuReportTargets( 60 ) self:MenuAssistedAttack() self:MenuROE() - self:MenuEvasion() +-- self:MenuEvasion() -- self:MenuResumeMission() @@ -396,21 +389,6 @@ function AI_ESCORT:MenuFormation( Formation, ... ) ) end --- self.EscortGroupSet:ForSomeGroupAlive( --- --- @param Core.Group#GROUP EscortGroup --- function( EscortGroup ) --- if EscortGroup:IsAir() then --- if not EscortGroup.EscortMenuReportNavigation then --- EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) --- end --- --- if not EscortGroup["EscortMenuFormation"..Formation] then --- EscortGroup["EscortMenuFormation"..Formation] = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), Formation, EscortGroup.EscortMenuReportNavigation, AI_ESCORT["_EscortFormation"..Formation], self, EscortGroup, ... ) --- end --- end --- end --- ) - end @@ -428,22 +406,6 @@ function AI_ESCORT:MenuJoinUp() self.FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Join Up", self.FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) end - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if EscortGroup:IsAir() then - if not EscortGroup.EscortMenuReportNavigation then - EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) - end - - if not EscortGroup.EscortMenuJoinUpAndFollow then - EscortGroup.EscortMenuJoinUpAndFollow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Join-Up", EscortGroup.EscortMenuReportNavigation, ESCORT._JoinUp, self, EscortGroup ) - end - - end - end - ) - return self end @@ -652,36 +614,6 @@ function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) Speed ) - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if EscortGroup:IsAir() then - - if not EscortGroup.EscortMenuHold then - EscortGroup.EscortMenuHold = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold position", EscortGroup.EscortMenu ) - end - - - if not EscortGroup.EscortMenuHoldPosition then - EscortGroup.EscortMenuHoldPosition = {} - end - - EscortGroup.EscortMenuHoldPosition[#EscortGroup.EscortMenuHoldPosition+1] = MENU_GROUP_COMMAND - :New( - self.EscortUnit:GetGroup(), - MenuText, - EscortGroup.EscortMenuHold, - AI_ESCORT._HoldPosition, - self, - EscortGroup, - EscortGroup, - Height, - Speed - ) - end - end - ) - return self end @@ -739,58 +671,6 @@ function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) Speed ) - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if EscortGroup:IsAir() then - - if not EscortGroup.EscortMenuHold then - EscortGroup.EscortMenuHold = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold position", EscortGroup.EscortMenu ) - end - - if not Height then - Height = 30 - end - - if not Speed then - Speed = 0 - end - - local MenuText = "" - if not MenuTextFormat then - if Speed == 0 then - MenuText = string.format( "Rejoin and hold at %d meter", Height ) - else - MenuText = string.format( "Rejoin and hold at %d meter at %d", Height, Speed ) - end - else - if Speed == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Speed ) - end - end - - if not self.EscortMenuHoldAtLeaderPosition then - self.EscortMenuHoldAtLeaderPosition = {} - end - - self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1] = MENU_GROUP_COMMAND - :New( - self.EscortUnit:GetGroup(), - MenuText, - EscortGroup.EscortMenuHold, - AI_ESCORT._HoldPosition, - self, - self.EscortUnit:GetGroup(), - EscortGroup, - Height, - Speed - ) - end - end - ) - return self end @@ -880,30 +760,6 @@ function AI_ESCORT:MenuFlare( MenuTextFormat ) self.FlightMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release yellow flare", self.FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) end - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if not EscortGroup.EscortMenuReportNavigation then - EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) - end - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Flare" - else - MenuText = MenuTextFormat - end - - if not EscortGroup.EscortMenuFlare then - EscortGroup.EscortMenuFlare = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, EscortGroup.EscortMenuReportNavigation ) - EscortGroup.EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release green flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green, "Released a green flare!" ) - EscortGroup.EscortMenuFlareRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release red flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Red, "Released a red flare!" ) - EscortGroup.EscortMenuFlareWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release white flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.White, "Released a white flare!" ) - EscortGroup.EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release yellow flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Yellow, "Released a yellow flare!" ) - end - end - ) - return self end @@ -937,33 +793,6 @@ function AI_ESCORT:MenuSmoke( MenuTextFormat ) self.FlightMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release blue smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) end - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if not EscortGroup:IsAir() then - if not EscortGroup.EscortMenuReportNavigation then - EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) - end - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Smoke" - else - MenuText = MenuTextFormat - end - - if not EscortGroup.EscortMenuSmoke then - EscortGroup.EscortMenuSmoke = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Smoke", EscortGroup.EscortMenuReportNavigation ) - EscortGroup.EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release green smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green, "Releasing green smoke!" ) - EscortGroup.EscortMenuSmokeRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release red smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Red, "Releasing red smoke!" ) - EscortGroup.EscortMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release white smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.White, "Releasing white smoke!" ) - EscortGroup.EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release orange smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Orange, "Releasing orange smoke!" ) - EscortGroup.EscortMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release blue smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Blue, "Releasing blue smoke!" ) - end - end - end - ) - return self end @@ -996,29 +825,6 @@ function AI_ESCORT:MenuReportTargets( Seconds ) self.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, 5, Seconds ) - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if EscortGroup:IsAir() then - - if not EscortGroup.EscortMenuReportNearbyTargets then - EscortGroup.EscortMenuReportNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Report targets", EscortGroup.EscortMenu ) - end - - -- Report Targets - EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets now!", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) - EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) - EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) - - -- Attack Targets - EscortGroup.EscortMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Attack targets", EscortGroup.EscortMenu ) - - EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, timer, Seconds ) - timer=timer+1 - end - end - ) - return self end @@ -1051,28 +857,23 @@ end function AI_ESCORT:MenuROE( MenuTextFormat ) self:F( MenuTextFormat ) - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if not EscortGroup.EscortMenuROE then - -- Rules of Engagement - EscortGroup.EscortMenuROE = MENU_GROUP:New( self.EscortUnit:GetGroup(), "ROE", EscortGroup.EscortMenu ) - if EscortGroup:OptionROEHoldFirePossible() then - EscortGroup.EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Hold Fire", EscortGroup.EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup:OptionROEHoldFire(), "Holding weapons!" ) - end - if EscortGroup:OptionROEReturnFirePossible() then - EscortGroup.EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Return Fire", EscortGroup.EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup:OptionROEReturnFire(), "Returning fire!" ) - end - if EscortGroup:OptionROEOpenFirePossible() then - EscortGroup.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Open Fire", EscortGroup.EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup:OptionROEOpenFire(), "Opening fire on designated targets!!" ) - end - if EscortGroup:OptionROEWeaponFreePossible() then - EscortGroup.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Weapon Free", EscortGroup.EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup:OptionROEWeaponFree(), "Opening fire on targets of opportunity!" ) - end - end - end - ) + if not self.FlightMenuROE then + self.FlightMenuROE = MENU_GROUP:New( self.EscortUnit:GetGroup(), "ROE", self.FlightMenu ) + end + if not self.FlightMenuROEHoldFire then + self.FlightMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Hold fire", self.FlightMenuROE, AI_ESCORT._ROE, self, ENUMS.ROE.HoldFire, "Holding weapons!" ) + end + if not self.FlightMenuROEReturnFire then + self.FlightMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Return fire", self.FlightMenuROE, AI_ESCORT._ROE, self, ENUMS.ROE.ReturnFire, "Returning fire!" ) + end + if not self.FlightMenuROEOpenFire then + self.FlightMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Open fire", self.FlightMenuROE, AI_ESCORT._ROE, self, ENUMS.ROE.OpenFire, "Open fire at designated targets!" ) + end + if not self.FlightMenuROEWeaponFree then + self.FlightMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Engage all targets", self.FlightMenuROE, AI_ESCORT._ROE, self, ENUMS.ROE.WeaponFree, "Engaging all targets!" ) + end + return self end @@ -1318,15 +1119,6 @@ function AI_ESCORT:_FlightSmoke( Color, Message ) end -function AI_ESCORT:_ReportNearbyTargetsNow( EscortGroup ) - - local EscortUnit = self.EscortUnit - - self:_ReportTargetsScheduler( EscortGroup ) - -end - - function AI_ESCORT:_FlightReportNearbyTargetsNow() self:_FlightReportTargetsScheduler() @@ -1522,12 +1314,27 @@ function AI_ESCORT:_AssistTarget( EscortGroup, DetectedItem ) end -function AI_ESCORT:_ROE( EscortGroup, EscortROEFunction, EscortROEMessage ) +function AI_ESCORT:_ROE( EscortAction, EscortROEMessage ) + + self:F({EscortAction=EscortAction,EscortROEMessage=EscortROEMessage}) local EscortUnit = self.EscortUnit - pcall( function() EscortROEFunction() end ) - EscortGroup:MessageTypeToGroup( EscortROEMessage, MESSAGE.Type.Information, EscortUnit:GetGroup() ) + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortAction == ENUMS.ROE.HoldFire then + EscortGroup:OptionROEHoldFire() + elseif EscortAction == ENUMS.ROE.ReturnFire then + EscortGroup:OptionROEReturnFire() + elseif EscortAction == ENUMS.ROE.OpenFire then + EscortGroup:OptionROEOpenFire() + elseif EscortAction == ENUMS.ROE.WeaponFree then + EscortGroup:OptionROEWeaponFree() + end + EscortGroup:MessageTypeToGroup( EscortROEMessage, MESSAGE.Type.Information, EscortUnit:GetGroup() ) + end + ) end diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index e0981a599..5eae434e2 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -1087,11 +1087,11 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 local Speed = - ( Distance + FollowFormation.x ) / Time - if Distance > -4000 then - Speed = - ( Distance + FollowFormation.x ) / 90 + if Distance > -10000 then + Speed = - ( Distance + FollowFormation.x ) / 60 end - if Distance > -1500 then + if Distance > -2500 then Speed = - ( Distance + FollowFormation.x ) / 20 end diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 615bfcffe..a7bb1a76f 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1173,7 +1173,7 @@ end -- @param #SPAWN self -- @param #string SpawnIndex The index of the group to be spawned. -- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:SpawnWithIndex( SpawnIndex ) +function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } ) if self:_GetSpawnIndex( SpawnIndex ) then @@ -1275,14 +1275,16 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) SpawnTemplate.CoalitionID = self.SpawnInitCoalition or SpawnTemplate.CoalitionID - if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then - if SpawnTemplate.route.points[1].type == "TakeOffParking" then - SpawnTemplate.uncontrolled = self.SpawnUnControlled - end - end +-- if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then +-- if SpawnTemplate.route.points[1].type == "TakeOffParking" then +-- SpawnTemplate.uncontrolled = self.SpawnUnControlled +-- end +-- end end - self:HandleEvent( EVENTS.Birth, self._OnBirth ) + if not NoBirth then + self:HandleEvent( EVENTS.Birth, self._OnBirth ) + end self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash ) self:HandleEvent( EVENTS.RemoveUnit, self._OnDeadOrCrash ) @@ -1471,17 +1473,23 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT EmergencyAirSpawn=true end + self:F( { SpawnIndex = self.SpawnIndex } ) + if self:_GetSpawnIndex( self.SpawnIndex + 1 ) then -- Get group template. local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + self:F( { SpawnTemplate = SpawnTemplate } ) + if SpawnTemplate then -- Check if the aircraft with the specified SpawnIndex is already spawned. -- If yes, ensure that the aircraft is spawned at the same aircraft spot. local GroupAlive = self:GetGroupFromIndex( self.SpawnIndex ) + + self:F( { GroupAlive = GroupAlive } ) -- Debug output self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) @@ -1802,6 +1810,8 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT SpawnTemplate.x = PointVec3.x SpawnTemplate.y = PointVec3.z + SpawnTemplate.uncontrolled = nil + -- Spawn group. local GroupSpawned = self:SpawnWithIndex( self.SpawnIndex ) @@ -1868,13 +1878,14 @@ function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, -- Get position of airbase. local PointVec3 = SpawnAirbase:GetCoordinate() - self:T2(PointVec3) -- Set take off type. Default is hot. local Takeoff = SPAWN.Takeoff.Cold for SpawnIndex = 1, self.SpawnMaxGroups do + self:F( { SpawnIndex = SpawnIndex, SpawnMaxGroups = self.SpawnMaxGroups } ) + -- Get group template. local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate @@ -1945,7 +1956,7 @@ function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, elseif AirbaseCategory == Airbase.Category.AIRDROME then spawnonairport=true end - spawnonrunway=Takeoff==SPAWN.Takeoff.Cold + spawnonrunway=Takeoff==SPAWN.Takeoff.Runway end -- Array with parking spots coordinates. @@ -2096,7 +2107,7 @@ function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, SpawnTemplate.parked = true for UnitID = 1, nunits do - self:T2('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + self:F('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) -- Template of the current unit. local UnitTemplate = SpawnTemplate.units[UnitID] @@ -2166,8 +2177,10 @@ function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, SpawnTemplate.x = PointVec3.x SpawnTemplate.y = PointVec3.z + SpawnTemplate.uncontrolled = true + -- Spawn group. - local GroupSpawned = self:SpawnWithIndex( SpawnIndex ) + local GroupSpawned = self:SpawnWithIndex( SpawnIndex, true ) -- When spawned in the air, we need to generate a Takeoff Event. if Takeoff == GROUP.Takeoff.Air then @@ -2181,7 +2194,6 @@ function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, SCHEDULER:New(nil, AIRBASE.CheckOnRunWay, {SpawnAirbase, GroupSpawned, 75, true} , 1.0) end - return GroupSpawned end end diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 68e0125d9..42ce0a74e 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -1,3 +1,4 @@ +__Moose.Include( 'Scripts/Moose/Utilities/Enums.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/Routines.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/Utils.lua' ) diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua new file mode 100644 index 000000000..b5f1e0fde --- /dev/null +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -0,0 +1,9 @@ +ENUMS = {} + +ENUMS.ROE = { + HoldFire = 1, + ReturnFire = 2, + OpenFire = 3, + WeaponFree = 4 + } + From ab423b3ba482c7d50ff8e405aa944772b883547e Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 25 Apr 2019 18:52:46 +0200 Subject: [PATCH 265/485] First working version of AI_ESCORT_REQUEST. --- .../Moose/AI/AI_Escort_Request.lua | 278 ++++++++++++++++++ Moose Development/Moose/Core/Spawn.lua | 2 + Moose Development/Moose/Modules.lua | 1 + 3 files changed, 281 insertions(+) create mode 100644 Moose Development/Moose/AI/AI_Escort_Request.lua diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua new file mode 100644 index 000000000..e16499974 --- /dev/null +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -0,0 +1,278 @@ +--- **Functional** -- Taking the lead of AI escorting your flight or of other AI, upon request using the menu. +-- +-- === +-- +-- ## Features: +-- +-- * Escort navigation commands. +-- * Escort hold at position commands. +-- * Escorts reporting detected targets. +-- * Escorts scanning targets in advance. +-- * Escorts attacking specific targets. +-- * Request assistance from other groups for attack. +-- * Manage rule of engagement of escorts. +-- * Manage the allowed evasion techniques of escorts. +-- * Make escort to execute a defined mission or path. +-- * Escort tactical situation reporting. +-- +-- === +-- +-- ## Missions: +-- +-- [ESC - Escorting](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ESC%20-%20Escorting) +-- +-- === +-- +-- Allows you to interact with escorting AI on your flight and take the lead. +-- +-- Each escorting group can be commanded with a complete set of radio commands (radio menu in your flight, and then F10). +-- +-- The radio commands will vary according the category of the group. The richest set of commands are with helicopters and airPlanes. +-- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. +-- +-- Escorts detect targets using a built-in detection mechanism. The detected targets are reported at a specified time interval. +-- Once targets are reported, each escort has these targets as menu options to command the attack of these targets. +-- Targets are by default grouped per area of 5000 meters, but the kind of detection and the grouping range can be altered. +-- +-- Different formations can be selected in the Flight menu: Trail, Stack, Left Line, Right Line, Left Wing, Right Wing, Central Wing and Boxed formations are available. +-- The Flight menu also allows for a mass attack, where all of the escorts are commanded to attack a target. +-- +-- Escorts can emit flares to reports their location. They can be commanded to hold at a location, which can be their current or the leader location. +-- In this way, you can spread out the escorts over the battle field before a coordinated attack. +-- +-- But basically, the escort class provides 4 modes of operation, and depending on the mode, you are either leading the flight, or following the flight. +-- +-- ## Leading the flight +-- +-- When leading the flight, you are expected to guide the escorts towards the target areas, +-- and carefully coordinate the attack based on the threat levels reported, and the available weapons +-- carried by the escorts. Ground ships or ground troops can execute A-assisted attacks, when they have long-range ground precision weapons for attack. +-- +-- ## Following the flight +-- +-- Escorts can be commanded to execute a specific mission path. In this mode, the escorts are in the lead. +-- You as a player, are following the escorts, and are commanding them to progress the mission while +-- ensuring that the escorts survive. You are joining the escorts in the battlefield. They will detect and report targets +-- and you will ensure that the attacks are well coordinated, assigning the correct escort type for the detected target +-- type. Once the attack is finished, the escort will resume the mission it was assigned. +-- In other words, you can use the escorts for reconnaissance, and for guiding the attack. +-- Imagine you as a mi-8 pilot, assigned to pickup cargo. Two ka-50s are guiding the way, and you are +-- following. You are in control. The ka-50s detect targets, report them, and you command how the attack +-- will commence and from where. You can control where the escorts are holding position and which targets +-- are attacked first. You are in control how the ka-50s will follow their mission path. +-- +-- Escorts can act as part of a AI A2G dispatcher offensive. In this way, You was a player are in control. +-- The mission is defined by the A2G dispatcher, and you are responsible to join the flight and ensure that the +-- attack is well coordinated. +-- +-- It is with great proud that I present you this class, and I hope you will enjoy the functionality and the dynamism +-- it brings in your DCS world simulations. +-- +-- # RADIO MENUs that can be created: +-- +-- Find a summary below of the current available commands: +-- +-- ## Navigation ...: +-- +-- Escort group navigation functions: +-- +-- * **"Join-Up":** The escort group fill follow you in the assigned formation. +-- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. +-- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. +-- +-- ## Hold position ...: +-- +-- Escort group navigation functions: +-- +-- * **"At current location":** The escort group will hover above the ground at the position they were. The altitude can be specified as a parameter. +-- * **"At my location":** The escort group will hover or orbit at the position where you are. The escort will fly to your location and hold position. The altitude can be specified as a parameter. +-- +-- ## Report targets ...: +-- +-- Report targets will make the escort group to report any target that it identifies within detection range. Any detected target can be attacked using the "Attack Targets" menu function. (see below). +-- +-- * **"Report now":** Will report the current detected targets. +-- * **"Report targets on":** Will make the escorts to report the detected targets and will fill the "Attack Targets" menu list. +-- * **"Report targets off":** Will stop detecting targets. +-- +-- ## Attack targets ...: +-- +-- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. +-- This menu will be available in Flight menu or in each Escort menu. +-- +-- ## Scan targets ...: +-- +-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or rejoin formation. +-- +-- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. +-- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. +-- +-- ## Request assistance from ...: +-- +-- This menu item will list all detected targets within a 15km range, similar as with the menu item **Attack Targets**. +-- This menu item allows to request attack support from other ground based escorts supporting the current escort. +-- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. +-- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. +-- +-- ## ROE ...: +-- +-- Sets the Rules of Engagement (ROE) of the escort group when in flight. +-- +-- * **"Hold Fire":** The escort group will hold fire. +-- * **"Return Fire":** The escort group will return fire. +-- * **"Open Fire":** The escort group will open fire on designated targets. +-- * **"Weapon Free":** The escort group will engage with any target. +-- +-- ## Evasion ...: +-- +-- Will define the evasion techniques that the escort group will perform during flight or combat. +-- +-- * **"Fight until death":** The escort group will have no reaction to threats. +-- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. +-- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. +-- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. +-- +-- ## Resume Mission ...: +-- +-- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. +-- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. +-- +-- === +-- +-- ### Authors: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Escort +-- @image Escorting.JPG + + + +--- @type AI_ESCORT_REQUEST +-- @extends AI.AI_Formation#AI_FORMATION + +--- AI_ESCORT_REQUEST class +-- +-- # AI_ESCORT_REQUEST construction methods. +-- +-- Create a new AI_ESCORT_REQUEST object with the @{#AI_ESCORT_REQUEST.New} method: +-- +-- * @{#AI_ESCORT_REQUEST.New}: Creates a new AI_ESCORT_REQUEST object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text. +-- +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = AI_ESCORT_REQUEST:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +-- +-- @field #AI_ESCORT_REQUEST +AI_ESCORT_REQUEST = { + ClassName = "AI_ESCORT_REQUEST", +} + +--- AI_ESCORT_REQUEST.Mode class +-- @type AI_ESCORT_REQUEST.MODE +-- @field #number FOLLOW +-- @field #number MISSION + +--- MENUPARAM type +-- @type MENUPARAM +-- @field #AI_ESCORT_REQUEST ParamSelf +-- @field #Distance ParamDistance +-- @field #function ParamFunction +-- @field #string ParamMessage + +--- AI_ESCORT_REQUEST class constructor for an AI group +-- @param #AI_ESCORT_REQUEST self +-- @param Wrapper.Client#CLIENT EscortUnit The client escorted by the EscortGroup. +-- @param Core.Spawn#SPAWN EscortSpawn The spawn object of AI, escorting the EscortUnit. +-- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where escorts will be spawned once requested. +-- @param #string EscortName Name of the escort. +-- @param #string EscortBriefing A text showing the AI_ESCORT_REQUEST briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #AI_ESCORT_REQUEST self +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = AI_ESCORT_REQUEST:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +function AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) + + self.EscortGroupSet = SET_GROUP:New() + self.EscortSpawn = EscortSpawn + self.EscortAirbase = EscortAirbase + + local self = BASE:Inherit( self, AI_ESCORT:New( EscortUnit, self.EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT_REQUEST + self:F( { EscortUnit } ) + + self.LeaderGroup = self.EscortUnit:GetGroup() + + self.Detection = DETECTION_AREAS:New( self.EscortGroupSet, 5000 ) + self.Detection:__Start( 30 ) + + + return self +end + +--- @param #AI_ESCORT_REQUEST self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT_REQUEST:SpawnEscort() + + local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Cold ) + self.EscortGroupSet:AddGroup( EscortGroup ) + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEHoldFire() + + self:ScheduleOnce( 5, + function() + local LeaderEscort = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP + + local Report = REPORT:New( "Escorts Reporting." ) + Report:Add( "Current coordinate: " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) + Report:Add( "Configuration: " .. self.EscortGroupSet:GetUnitTypeNames():Text( ", " ) ) + Report:Add( "Joining Up ..." ) + + LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.EscortUnit ) + end + ) + + self:__FormationTrail( 30, 50, 100, 100 ) + self:JoinFormation( EscortGroup ) +end + +--- @param #AI_ESCORT_REQUEST self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) + + self:E("Start") + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + --EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName() ) + EscortGroup:WayPointInitialize( 1 ) + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEOpenFire() + end + ) + + if not self.MenuRequestEscort then + self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request A2G Escort", nil, + function() + env.info("call") + self:SpawnEscort() + end + ) + end + +end + diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index a7bb1a76f..d042f49e6 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2197,6 +2197,8 @@ function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, end end + self:SetSpawnIndex() + return nil end diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 42ce0a74e..43c168b77 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -86,6 +86,7 @@ __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_Escort.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Escort_Request.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' ) From 3657a196457d3512f5f2c57f9bbba2051abc2f50 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 26 Apr 2019 18:42:42 +0200 Subject: [PATCH 266/485] Updates of working version. --- Moose Development/Moose/AI/AI_Escort.lua | 30 ++++++++++------ .../Moose/AI/AI_Escort_Request.lua | 36 ++++++------------- .../Moose/Functional/Detection.lua | 10 +++--- 3 files changed, 37 insertions(+), 39 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 59932b443..0ecd87ffe 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -332,22 +332,29 @@ end --- Defines the default menus -- @param #AI_ESCORT 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. +-- @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. +-- @param #number ZLevels The amount of levels on the Z-axis. -- @return #AI_ESCORT -function AI_ESCORT:Menus() +function AI_ESCORT:Menus( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) self:F() -- self:MenuScanForTargets( 100, 60 ) self:MenuJoinUp() - self:MenuFormationTrail( 50, 100, 50 ) - self:MenuFormationStack( 50, 100, 50, 50 ) - self:MenuFormationLeftLine( 0, 0, 100, 100 ) - self:MenuFormationRightLine( 0, 0, 100, 100 ) - self:MenuFormationLeftWing( 0, 50, 0, 100, 100 ) - self:MenuFormationRightWing( 0, 50, 0, 100, 100 ) - self:MenuFormationCenterWing( 50, 50, 0, 50, 100, 100 ) - self:MenuFormationBox( 50, 100, 0, 50, 50, 100, 10 ) - + self:MenuFormationTrail(XStart,XSpace,YStart) + self:MenuFormationStack(XStart,XSpace,YStart,YSpace) + self:MenuFormationLeftLine(XStart,YStart,ZStart,ZSpace) + self:MenuFormationRightLine(XStart,YStart,ZStart,ZSpace) + self:MenuFormationLeftWing(XStart,XSpace,YStart,ZStart,ZSpace) + self:MenuFormationRightWing(XStart,XSpace,YStart,ZStart,ZSpace) + self:MenuFormationVic(XStart,XSpace,YStart,YSpace,ZStart,ZSpace) + self:MenuFormationBox(XStart,XSpace,YStart,YSpace,ZStart,ZSpace,ZLevels) + self:MenuHoldAtEscortPosition( 1000, 500 ) self:MenuHoldAtLeaderPosition( 1000, 500 ) @@ -1474,6 +1481,9 @@ function AI_ESCORT:_FlightReportTargetsScheduler() local ClientEscortTargets = self.Detection for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do + + self:F("FlightReportTargetScheduler Targets") + DetectedTargets = true -- There are detected targets, when the content of the for loop is executed. We use it to display a message. diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index e16499974..2c6b24e00 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -195,14 +195,15 @@ AI_ESCORT_REQUEST = { -- @param #string EscortBriefing A text showing the AI_ESCORT_REQUEST briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. -- @return #AI_ESCORT_REQUEST self -- @usage --- -- Declare a new EscortPlanes object as follows: --- --- -- First find the GROUP object and the CLIENT object. --- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. --- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. --- --- -- Now use these 2 objects to construct the new EscortPlanes object. --- EscortPlanes = AI_ESCORT_REQUEST:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +-- EscortSpawn = SPAWN:NewWithAlias( "Red A2G Escort Template", "Red A2G Escort AI" ):InitLimit( 10, 10 ) +-- EscortSpawn:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Sochi_Adler ), AIRBASE.TerminalType.OpenBig ) +-- +-- local EscortUnit = UNIT:FindByName( "Red A2G Pilot" ) +-- +-- Escort = AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, AIRBASE:FindByName(AIRBASE.Caucasus.Sochi_Adler), "A2G", "Briefing" ) +-- Escort:FormationTrail( 50, 100, 100 ) +-- Escort:Menus() +-- Escort:__Start( 5 ) function AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) self.EscortGroupSet = SET_GROUP:New() @@ -210,7 +211,6 @@ function AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, EscortAirbase, EscortNa self.EscortAirbase = EscortAirbase local self = BASE:Inherit( self, AI_ESCORT:New( EscortUnit, self.EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT_REQUEST - self:F( { EscortUnit } ) self.LeaderGroup = self.EscortUnit:GetGroup() @@ -241,34 +241,20 @@ function AI_ESCORT_REQUEST:SpawnEscort() Report:Add( "Joining Up ..." ) LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.EscortUnit ) + self:FormationTrail( 50, 50, 50 ) + self:JoinFormation( EscortGroup ) end ) - self:__FormationTrail( 30, 50, 100, 100 ) - self:JoinFormation( EscortGroup ) end --- @param #AI_ESCORT_REQUEST self -- @param Core.Set#SET_GROUP EscortGroupSet function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) - self:E("Start") - - EscortGroupSet:ForEachGroup( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - --EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName() ) - EscortGroup:WayPointInitialize( 1 ) - - EscortGroup:OptionROTVertical() - EscortGroup:OptionROEOpenFire() - end - ) - if not self.MenuRequestEscort then self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request A2G Escort", nil, function() - env.info("call") self:SpawnEscort() end ) diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index fe078643a..133706fb9 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -541,14 +541,17 @@ do -- DETECTION_BASE end - self.DetectionCount = self.DetectionSet:GetSomeIteratorLimit() + self.DetectionCount = self.DetectionSet:Count() - self.DetectionSet:ForSomeGroupAlive( + self.DetectionSet:ForEachGroupAlive( function( DetectionGroup ) self:__Detection( DetectDelay, DetectionGroup, DetectionTimeStamp ) -- Process each detection asynchronously. DetectDelay = DetectDelay + 1 end ) + + self:__Detect( -self.RefreshTimeInterval ) + end --- @param #DETECTION_BASE self @@ -803,10 +806,9 @@ do -- DETECTION_BASE self:__DetectedItem( 0.1, DetectedItem ) end end - - self:__Detect( self.RefreshTimeInterval ) end + end From 6c35892f05188f5471d220d606e45cc50cdcfeae Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 1 May 2019 17:56:25 +0200 Subject: [PATCH 267/485] RANGE 2.0 Range v2.0.0 - Turned into FSM. - Added radial to bomb impact info. - Added option to add coordinates as bomb target. - Added option to save bombing results to file. AIRBOSS v1.0.0 - Added option to disable welcome message. - Increased output load/save. --- Moose Development/Moose/Functional/Range.lua | 919 ++++++++++++++----- Moose Development/Moose/Ops/Airboss.lua | 41 +- 2 files changed, 715 insertions(+), 245 deletions(-) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 62e39cadf..09c6be614 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -43,6 +43,7 @@ -- @type RANGE -- @field #string ClassName Name of the Class. -- @field #boolean Debug If true, debug info is send as messages on the screen. +-- @field #string id String id of range for output in DCS log. -- @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. @@ -76,6 +77,7 @@ -- @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. +-- @field #boolean autosafe If true, automatically save results every X seconds. -- @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. @@ -212,6 +214,7 @@ RANGE={ ClassName = "RANGE", Debug=false, + id=nil, rangename=nil, location=nil, rangeradius=5000, @@ -245,6 +248,7 @@ RANGE={ trackrockets=true, trackmissiles=true, defaultsmokebomb=true, + autosave=false, } --- Default range parameters. @@ -264,13 +268,59 @@ RANGE.Defaults={ foulline=610, } ---- Bomb target. self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed} +--- Target type, i.e. unit, static, or coordinate. +-- @type RANGE.TargetType +-- @field #string UNIT Target is a unit. +-- @field #string STATIC Target is a static. +-- @field #string COORD Target is a coordinate. +RANGE.TargetType={ + UNIT="Unit", + STATIC="Static", + COORD="Coordinate", +} + +--- Player settings. +-- @type RANGE.PlayerData +-- @field #boolean smokebombimpact Smoke bomb impact points. +-- @field #boolean flaredirecthits Flare when player directly hits a target. +-- @field #number smokecolor Color of smoke. +-- @field #number flarecolor Color of flares. +-- @field #boolean messages Display info messages. +-- @field #string unitname Name of player aircraft unit. +-- @field #string playername Name of player. +-- @field #string airframe Aircraft type name. + +--- Bomb target data. -- @type RANGE.BombTarget --- @field #string name Name of unit? +-- @field #string name Name of unit. -- @field Wrapper.Unit#UNIT target Target unit. +-- @field Core.Point#COORDINATE coordinate Coordinate of the target. -- @field #number goodhitrange Range in meters for a good hit. -- @field #boolean move If true, unit move randomly. -- @field #number speed Speed of unit. +-- @field #RANGE.TargetType type Type of target. + +--- Strafe target data. +-- @type RANGE.StrafeTarget +-- @field #string name Name of the unit. +-- @field Core.Zone#ZONE_POLYGON polygon Polygon zone. +-- @field Core.Point#COORDINATE coordinate Center coordinate of the pit. +-- @field #number goodPass Number of hits for a good pass. +-- @field #table targets Table of target units. +-- @field #number foulline Foul line +-- @field #number smokepoints Number of smoke points. +-- @field #number heading Heading of pit. + +--- Bomb target result. +-- @type RANGE.BombResult +-- @field #string name Name of closest target. +-- @field #number distance Distance in meters. +-- @field #number radial Radial in degrees. +-- @field #string weapon Name of the weapon. +-- @field #string quality Hit quality. +-- @field #string player Player name. +-- @field #string airframe Aircraft type of player. +-- @field #number time Time via timer.getAbsTime() in seconds of impact. --- Global list of all defined range names. -- @field #table Names @@ -284,13 +334,9 @@ RANGE.MenuF10={} -- @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.3.0" +RANGE.version="2.0.0" --TODO list: --TODO: Verbosity level for messages. @@ -316,19 +362,86 @@ function RANGE:New(rangename) BASE:F({rangename=rangename}) -- Inherit BASE. - local self=BASE:Inherit(self, BASE:New()) -- #RANGE + local self=BASE:Inherit(self, FSM: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" + -- Log id. + self.id=string.format("RANGE %s | ", self.rangename) + -- 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) + local text=string.format("Script version %s - creating new RANGE object %s.", RANGE.version, self.rangename) + self:I(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) -- Defaults self:SetDefaultPlayerSmokeBomb() + + -- Start State. + self:SetStartState("Stopped") + + --- + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start RANGE script. + self:AddTransition("*", "Status", "*") -- Status of RANGE script. + self:AddTransition("*", "Impact", "*") -- Impact of bomb/rocket/missile. + self:AddTransition("*", "Save", "*") -- Save player results. + self:AddTransition("*", "Load", "*") -- Load player results. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the RANGE. Initializes parameters and starts event handlers. + -- @function [parent=#RANGE] Start + -- @param #RANGE self + + --- Triggers the FSM event "Start" after a delay. Starts the RANGE. Initializes parameters and starts event handlers. + -- @function [parent=#RANGE] __Start + -- @param #RANGE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the RANGE and all its event handlers. + -- @param #RANGE self + + --- Triggers the FSM event "Stop" after a delay. Stops the RANGE and all its event handlers. + -- @function [parent=#RANGE] __Stop + -- @param #RANGE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#RANGE] Status + -- @param #RANGE self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#RANGE] __Status + -- @param #RANGE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Impact". + -- @function [parent=#RANGE] Impact + -- @param #RANGE self + -- @param #RANGE.BombResult result Data of bombing run. + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- Triggers the FSM delayed event "Impact". + -- @function [parent=#RANGE] __Impact + -- @param #RANGE self + -- @param #number delay Delay in seconds before the function is called. + -- @param #RANGE.BombResult result Data of the bombing run. + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- On after "Impact" event user function. Called when a bomb/rocket/missile impacted. + -- @function [parent=#RANGE] OnAfterImpact + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.BombResult result Data of the bombing run. + -- @param #RANGE.Playerdata player Data of player settings etc. -- Return object. return self @@ -336,106 +449,102 @@ end --- Initializes number of targets and location of the range. Starts the event handlers. -- @param #RANGE self --- @param #number delay Delay in seconds, before the RANGE is started. Default immediately. --- @return self -function RANGE:Start(delay) - self:F() +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterStart() - if delay and delay>0 then - SCHEDULER:New(nil, self.Start, {self}, delay) - else + -- Location/coordinate of range. + local _location=nil - -- Location/coordinate of range. - local _location=nil + -- Count bomb targets. + local _count=0 + for _,_target in pairs(self.bombingTargets) do + _count=_count+1 - -- Count bomb targets. - local _count=0 - for _,_target in pairs(self.bombingTargets) do - _count=_count+1 - - -- Get range location. + -- Get range location. + if _location==nil then + _location=self:_GetBombTargetCoordinate(_target) + 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=_target.target:GetCoordinate() --Core.Point#COORDINATE + _location=_unit:GetCoordinate() 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() - 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 + end + self.nstrafetargets=_count - -- 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 + -- 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(self.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(self.id..text) + MESSAGE:New(text,10):ToAllIf(self.Debug) + + -- Event handling. + if self.eventmoose then + -- Events are handled my MOOSE. + self:T(self.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(self.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()) + local _static=_target.type==RANGE.TargetType.STATIC - -- 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) + 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 - return self + -- 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 + + self:__Status(-60) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Set maximal strafing altitude. Player entering a strafe pit above that altitude are not registered for a valid pass. -- @param #RANGE self @@ -464,6 +573,22 @@ function RANGE:SetMessageTimeDuration(time) return self end +--- Automatically safe player results to disc. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetAutosafeOn() + self.autosafe=true + return self +end + +--- Switch off auto safe player results. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetAutosafeOff() + self.autosafe=false + 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. @@ -495,7 +620,7 @@ 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. +-- @param #boolean switch 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 @@ -511,7 +636,7 @@ end -- @param #number distance Threshold distance in km. Default 25 km. -- @return #RANGE self function RANGE:SetBombtrackThreshold(distance) - self.BombtrackThreshold=distance*1000 or 25*1000 + self.BombtrackThreshold=(distance or 25)*1000 return self end @@ -544,6 +669,15 @@ function RANGE:SetBombTargetSmokeColor(colorid) return self end +--- Set score bomb distance. +-- @param #RANGE self +-- @param #number distance Distance in meters. Default 1000 m. +-- @return #RANGE self +function RANGE:SetScoreBombDistance(distance) + self.scorebombdistance=distance or 1000 + 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. @@ -670,20 +804,20 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe if _isstatic==true then -- Add static object. - self:T(RANGE.id..string.format("Adding STATIC object %s as strafe target #%d.", _name, _i)) + self:T(self.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)) + self:T(self.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) + self:E(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) end @@ -703,7 +837,7 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe -- 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) + self:E(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) return end @@ -757,13 +891,23 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe -- Create tires --_polygon:BoundZone() + + local st={} --#RANGE.StrafeTarget + st.name=_name + st.polygon=_polygon + st.coordinate=Ccenter + st.goodPass=goodpass + st.targets=_targets + st.foulline=foulline + st.smokepoints=p + st.heading=heading -- Add zone to table. - table.insert(self.strafeTargets, {name=_name, polygon=_polygon, coordinate= Ccenter, goodPass=goodpass, targets=_targets, foulline=foulline, smokepoints=p, heading=heading}) + table.insert(self.strafeTargets, st) -- 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) - self:T(RANGE.id..text) + self:T(self.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) return self @@ -835,14 +979,14 @@ function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) 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)) + self:T2(self.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange, false)) self:AddBombingTargetUnit(_static, goodhitrange) elseif _isstatic==false then local _unit=UNIT:FindByName(name) - self:T2(RANGE.id..string.format("Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove)) + self:T2(self.id..string.format("Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove)) self:AddBombingTargetUnit(_unit, goodhitrange) else - self:E(RANGE.id..string.format("ERROR! Could not find bombing target %s.", name)) + self:E(self.id..string.format("ERROR! Could not find bombing target %s.", name)) end end @@ -875,11 +1019,11 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) -- 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))) + self:T(self.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) elseif _isstatic==false then - self:T(RANGE.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + self:T(self.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(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)) + self:E(self.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. @@ -888,9 +1032,46 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) speed=self:_GetSpeed(unit) end - -- Insert target to table. - table.insert(self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed}) + local target={} --#RANGE.BombTarget + target.name=name + target.target=target + target.goodhitrange=goodhitrange + target.move=randommove + target.speed=speed + target.coordinate=unit:GetCoordinate() + if _isstatic then + target.type=RANGE.TargetType.STATIC + else + target.type=RANGE.TargetType.UNIT + end + -- Insert target to table. + table.insert(self.bombingTargets, target) + + return self +end + + +--- Add a coordinate of a bombing target. This +-- @param #RANGE self +-- @param Core.Point#COORDINATE coord The coordinate. +-- @param #string name Name of target. +-- @param #number goodhitrange Max distance from unit which is considered as a good hit. +-- @return #RANGE self +function RANGE:AddBombingTargetCoordinate(coord, name, goodhitrange) + + local target={} --#RANGE.BombTarget + target.name=name or "Bomb Target" + target.target=nil + target.goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange + target.move=false + target.speed=0 + target.coordinate=coord + target.type=RANGE.TargetType.COORD + + -- Insert target to table. + table.insert(self.bombingTargets, target) + return self end @@ -936,7 +1117,7 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) elseif _staticpit==false then pit=UNIT:FindByName(namepit) 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)) + self:E(self.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. @@ -946,7 +1127,7 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) elseif _staticfoul==false then foul=UNIT:FindByName(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)) + self:E(self.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. @@ -954,15 +1135,16 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) if pit~=nil and foul~=nil then fouldist=pit:GetCoordinate():Get2DDistance(foul:GetCoordinate()) else - self:E(RANGE.id..string.format("ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline)) + self:E(self.id..string.format("ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline)) end - self:T(RANGE.id..string.format("Foul line distance = %.1f m.", fouldist)) + self:T(self.id..string.format("Foul line distance = %.1f m.", fouldist)) return fouldist end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event Handling +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- General event handler. -- @param #RANGE self @@ -1007,12 +1189,12 @@ function RANGE:onEvent(Event) 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))) - self:T3(RANGE.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) - 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))) + self:T3(self.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) + self:T3(self.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) + self:T3(self.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) + self:T3(self.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) + self:T3(self.id..string.format("EVENT: Tgt unit = %s" , tostring(EventData.TgtUnitName))) + self:T3(self.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 @@ -1041,9 +1223,9 @@ function RANGE:OnEventBirth(EventData) 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(self.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T3(self.id.."BIRTH: player = "..tostring(_playername)) if _unit and _playername then @@ -1054,26 +1236,26 @@ function RANGE:OnEventBirth(EventData) -- 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) + self:T(self.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:_GetAmmo(_unitName) - -- Reset current strafe status. self.strafeStatus[_uid] = nil -- Add Menu commands after a delay of 0.1 seconds. - --self:_AddF10Commands(_unitName) SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) -- By default, some bomb impact points and do not flare each hit on target. - self.PlayerSettings[_playername]={} + self.PlayerSettings[_playername]={} --#RANGE.PlayerData 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 + self.PlayerSettings[_playername].unitname=_unitName + self.PlayerSettings[_playername].playername=_playername + self.PlayerSettings[_playername].airframe=EventData.IniUnit:GetTypeName() -- Start check in zone timer. if self.planes[_uid] ~= true then @@ -1091,9 +1273,9 @@ 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)) - self:T3(RANGE.id.."HIT: Tgt target = "..tostring(EventData.TgtUnitName)) + self:T3(self.id.."HIT: Ini unit = "..tostring(EventData.IniUnitName)) + self:T3(self.id.."HIT: Ini group = "..tostring(EventData.IniGroupName)) + self:T3(self.id.."HIT: Tgt target = "..tostring(EventData.TgtUnitName)) -- Player info local _unitName = EventData.IniUnitName @@ -1141,7 +1323,7 @@ function RANGE:OnEventHit(EventData) 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) - self:T2(RANGE.id..text) + self:T2(self.id..text) _currentTarget.pastfoulline=true end end @@ -1203,12 +1385,13 @@ function RANGE:OnEventShot(EventData) local weaponcategory=desc.category -- 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) - self:T(RANGE.id.."EVENT SHOT: Weapon cate = "..weaponcategory) + self:T(self.id.."EVENT SHOT: Range "..self.rangename) + self:T(self.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) + self:T(self.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) + self:T(self.id.."EVENT SHOT: Weapon type = ".._weapon) + self:T(self.id.."EVENT SHOT: Weapon name = ".._weaponName) + self:T(self.id.."EVENT SHOT: Weapon cate = "..weaponcategory) + -- 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") @@ -1232,14 +1415,17 @@ function RANGE:OnEventShot(EventData) -- 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)) + self:T(self.id..string.format("Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR/1000)) end -- Only track if distance player to range is < 25 km. Also check that a player shot. No need to track AI weapons. if _track and dPR<=self.BombtrackThreshold and _unit and _playername then + + -- Player data. + local playerData=self.PlayerSettings[_playername] --#RANGE.PlayerData -- 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())) + self:T(self.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} @@ -1253,22 +1439,30 @@ function RANGE:OnEventShot(EventData) return _ordnance:getPoint() end) - self:T3(RANGE.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status))) + self:T2(self.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status))) if _status then - -- Still in the air. Remember this position. + ---------------------------- + -- Weapon is still in air -- + ---------------------------- + + -- Remember this position. _lastBombPos = {x = _bombPos.x, y = _bombPos.y, z= _bombPos.z } - -- Check again in 0.005 seconds. + -- Check again in ~0.005 seconds ==> 200 checks per second. return timer.getTime() + self.dtBombtrack else - -- Bomb did hit the ground. + ----------------------------- + -- Bomb did hit the ground -- + ----------------------------- + -- Get closet target to last position. - local _closetTarget = nil - local _distance = nil - local _hitquality = "POOR" + local _closetTarget=nil --#RANGE.BombTarget + local _distance=nil + local _closeCoord=nil + local _hitquality="POOR" -- Get callsign. local _callsign=self:_myname(_unitName) @@ -1276,42 +1470,39 @@ function RANGE:OnEventShot(EventData) -- Coordinate of impact point. local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) - -- Check if impact happend in range zone. + -- Check if impact happened 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 - timer.scheduleFunction(self._DelayedSmoke, {coord=impactcoord, color=self.PlayerSettings[_playername].smokecolor}, timer.getTime() + self.TdelaySmoke) + if playerData.smokebombimpact and insidezone then + if playerData.delaysmoke then + timer.scheduleFunction(self._DelayedSmoke, {coord=impactcoord, color=playerData.smokecolor}, timer.getTime() + self.TdelaySmoke) else - impactcoord:Smoke(self.PlayerSettings[_playername].smokecolor) + impactcoord:Smoke(playerData.smokecolor) end end -- Loop over defined bombing targets. for _,_bombtarget in pairs(self.bombingTargets) do - local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + -- Get target coordinate. + local targetcoord=self:_GetBombTargetCoordinate(_bombtarget) - if _target and _target:IsAlive() then + if targetcoord 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)) + local _temp = impactcoord:Get2DDistance(targetcoord) -- Find closest target to last known position of the bomb. if _distance == nil or _temp < _distance then _distance = _temp _closetTarget = _bombtarget + _closeCoord=targetcoord if _distance <= 0.5*_bombtarget.goodhitrange then _hitquality = "EXCELLENT" elseif _distance <= _bombtarget.goodhitrange then @@ -1327,32 +1518,43 @@ function RANGE:OnEventShot(EventData) end -- Count if bomb fell less than ~1 km away from the target. - if _distance <= self.scorebombdistance then - + if _distance and _distance <= self.scorebombdistance then -- Init bomb player results. if not self.bombPlayerResults[_playername] then - self.bombPlayerResults[_playername] = {} + self.bombPlayerResults[_playername]={} end -- Local results. - local _results = self.bombPlayerResults[_playername] + local _results=self.bombPlayerResults[_playername] + + local result={} --#RANGE.BombResult + result.name=_closetTarget.name or "unknown" + result.distance=_distance + result.radial=_closeCoord:HeadingTo(impactcoord) + result.weapon=_weaponName or "unknown" + result.quality=_hitquality + result.player=playerData.playername + result.time=timer.getAbsTime() + result.airframe=playerData.airframe -- Add to table. - table.insert(_results, {name=_closetTarget.name, distance =_distance, weapon = _weaponName, quality=_hitquality }) + table.insert(_results, result) + + -- Call impact. + self:Impact(result, playerData) - -- Send message to player. - local _message = string.format("%s, impact %d m from bullseye of target %s. %s hit.", _callsign, _distance, _closetTarget.name, _hitquality) - - -- Send message. - self:_DisplayMessageToGroup(_unit, _message, nil, true) elseif insidezone then - -- Send message + + -- 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, false) + + else + self:T(self.id.."Weapon impacted outside range zone.") end --Terminate the timer - self:T(RANGE.id..string.format("Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername)) + self:T(self.id..string.format("Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername)) return nil end -- _status check @@ -1360,15 +1562,224 @@ function RANGE:OnEventShot(EventData) 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 0.1 seconds.", self.rangename, _playername)) + self:T(self.id..string.format("Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername)) timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime()+0.1) end --if _track (string.match) and player-range distance < threshold. end +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check spawn queue and spawn aircraft if necessary. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterStatus(From, Event, To) + + self:I(self.id..string.format("Range status: %s", self:GetState())) + + --self:_CheckMissileStatus() + + -- Save results. + if self.autosafe then + self:Save() + end + + self:__Status(-60) +end + + +--- Function called after bomb impact on range. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #RANGE.BombResult result Result of bomb impact. +-- @param #RANGE.PlayerData player +function RANGE:onafterImpact(From, Event, To, result, player) + + -- Only display target name if there is more than one bomb target. + local targetname=nil + if #self.bombingTargets>1 then + local targetname=result.name + end + + -- Send message to player. + local text=string.format("%s, impact %03d° for %d ft", player.playername, result.radial, UTILS.MetersToFeet(result.distance)) + if targetname then + text=text..string.format(" from bulls of target %s.") + else + text=text.."." + end + text=text..string.format(" %s hit.", result.quality) + + -- Unit. + local unit=UNIT:FindByName(player.unitname) + + -- Send message. + self:_DisplayMessageToGroup(unit, text, nil, true) + self:T(self.id..text) + +end + +--- Function called before save event. Checks that io and lfs are desanitized. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onbeforeSave(From, Event, To) + if io and lfs then + return true + else + self:E(self.id..string.format("WARNING: io and/or lfs not desanitized. Cannot save player results.")) + return false + end +end + +--- Function called after save. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterSave(From, Event, To) + + local function _savefile(filename, data) + local f=io.open(filename, "wb") + if f then + f:write(data) + f:close() + self:I(self.id..string.format("Saving player results to file %s", tostring(filename))) + else + self:E(self.id..string.format("ERROR: Could not save results to file %s", tostring(filename))) + end + end + + -- Path. + local path=lfs.writedir() + + -- Set file name. + local filename=path..string.format("\\RANGE-%s_BombingResults.csv", self.rangename) + + -- Header line. + local scores="Name,Pass,Target,Distance,Radial,Quality,Weapon,Airframe,Mission Time" + + -- Loop over all players. + for playername,results in pairs(self.bombPlayerResults) do + + -- Loop over player grades table. + for i,_result in pairs(results) do + local result=_result --#RANGE.BombResult + local distance=result.distance + local weapon=result.weapon + local target=result.name + local radial=result.radial + local quality=result.quality + local time=UTILS.SecondsToClock(result.time) + local airframe=result.airframe + scores=scores..string.format("\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time) + end + end + + _savefile(filename, scores) +end + +--- Function called before save event. Checks that io and lfs are desanitized. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onbeforeLoad(From, Event, To) + if io and lfs then + return true + else + self:E(self.id..string.format("WARNING: io and/or lfs not desanitized. Cannot load player results.")) + return false + end +end + +--- On after "Load" event. Loads results of all players from file. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterLoad(From, Event, To) + + --- Function that load data from a file. + local function _loadfile(filename) + local f=io.open(filename, "rb") + if f then + --self:I(self.id..string.format("Loading player results from file %s", tostring(filename))) + local data=f:read("*all") + f:close() + return data + else + self:E(self.id..string.format("ERROR: Could not load player results from file %s", tostring(filename))) + return nil + end + end + + -- Path. + local path=lfs.writedir() + + -- Set file name. + local filename=path..string.format("\\RANGE-%s_BombingResults.csv", self.rangename) + + -- Info message. + local text=string.format("Loading player bomb results from file %s", filename) + self:I(self.id..text) + + -- Load asset data from file. + local data=_loadfile(filename) + + if data then + + -- Split by line break. + local results=UTILS.Split(data,"\n") + + -- Remove first header line. + table.remove(results, 1) + + -- Init player scores table. + self.bombPlayerResults={} + + -- Loop over all lines. + for _,_result in pairs(results) do + + -- Parameters are separated by commata. + local resultdata=UTILS.Split(_result, ",") + + -- Grade table + local result={} --#RANGE.BombResult + + -- Player name. + local playername=resultdata[1] + result.player=playername + + -- Results data. + result.name=tostring(resultdata[3]) + result.distance=tonumber(resultdata[4]) + result.radial=tonumber(resultdata[5]) + result.quality=tostring(resultdata[6]) + result.weapon=tostring(resultdata[7]) + result.airframe=tostring(resultdata[8]) + result.time=UTILS.ClockToSeconds(resultdata[9] or "00:00:00") + + -- Create player array if necessary. + self.bombPlayerResults[playername]=self.bombPlayerResults[playername] or {} + + -- Add result to table. + table.insert(self.bombPlayerResults[playername], result) + end + end +end + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Display Messages +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Start smoking a coordinate with a delay. -- @param #table _args Argements passed. @@ -1520,24 +1931,22 @@ function RANGE:_DisplayMyBombingResults(_unitName) -- Loop over results. local _bestMsg = "" - local _count = 1 - for _,_result in pairs(_results) do + for i,_result in pairs(_results) do + local result=_result --#RANGE.BombResult -- 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) + _message = _message.."\n"..string.format("[%d] %d m %03d° - %s - %s - %s hit", i, result.distance, result.radial, 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) + _bestMsg = string.format("%d m %03d° - %s - %s - %s hit", result.distance, result.radial, result.name, result.weapon, result.quality) end -- Best 10 runs only. - if _count == self.ndisplayresult then + if i==self.ndisplayresult then break end - -- Increase counter. - _count = _count+1 end -- Message. @@ -1625,8 +2034,12 @@ function RANGE:_DisplayRangeInfo(_unitname) if self.location then + local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS + -- Direction vector from current position (coord) to target (position). local position=self.location --Core.Point#COORDINATE + local bulls=position:ToStringBULLS(unit:GetCoalition(), settings) + local lldms=position:ToStringLLDMS(settings) local rangealt=position:GetLandHeight() local vec3=coord:GetDirectionVec3(position) local angle=coord:GetAngleDegrees(vec3) @@ -1654,8 +2067,7 @@ function RANGE:_DisplayRangeInfo(_unitname) textdelay=string.format("Smoke bomb delay: OFF") end - -- Player unit settings. - local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS + -- Player unit settings. local trange=string.format("%.1f km", range/1000) local trangealt=string.format("%d m", rangealt) local tstrafemaxalt=string.format("%d m", self.strafemaxalt) @@ -1669,6 +2081,8 @@ function RANGE:_DisplayRangeInfo(_unitname) text=text..string.format("Information on %s:\n", self.rangename) text=text..string.format("-------------------------------------------------------\n") text=text..string.format("Bearing %s, Range %s\n", Bs, trange) + text=text..string.format("%s\n", bulls) + text=text..string.format("%s\n", lldms) text=text..string.format("Altitude ASL: %s\n", trangealt) text=text..string.format("Max strafing alt AGL: %s\n", tstrafemaxalt) text=text..string.format("# of strafe targets: %d\n", self.nstrafetargets) @@ -1681,7 +2095,7 @@ function RANGE:_DisplayRangeInfo(_unitname) self:_DisplayMessageToGroup(unit, text, nil, true, true) -- Debug output. - self:T2(RANGE.id..text) + self:T2(self.id..text) end end end @@ -1706,12 +2120,14 @@ function RANGE:_DisplayBombTargets(_unitname) 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 + -- Coordinate of bombtarget. + local coord=self:_GetBombTargetCoordinate(_bombtarget) + + if coord then + local mycoord=coord:ToStringA2G(_unit, _settings) - _text=_text..string.format("\n- %s: %s",_bombtarget.name, mycoord) + _text=_text..string.format("\n- %s: %s",_bombtarget.name or "unknown", mycoord) end end @@ -1752,7 +2168,7 @@ function RANGE:_DisplayStrafePits(_unitname) end local mycoord=coord:ToStringA2G(_unit, _settings) - _text=_text..string.format("\n- %s: %s - heading %03d",_strafepit.name, mycoord, heading) + _text=_text..string.format("\n- %s: %s - heading %03d°",_strafepit.name, mycoord, heading) end self:_DisplayMessageToGroup(_unit,_text, nil, true, true) @@ -1820,9 +2236,9 @@ function RANGE:_DisplayRangeWeather(_unitname) self:_DisplayMessageToGroup(unit, text, nil, true, true) -- Debug output. - self:T2(RANGE.id..text) + self:T2(self.id..text) else - self:T(RANGE.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) + self:T(self.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) end end @@ -1862,7 +2278,7 @@ function RANGE:_CheckInZone(_unitName) -- Debug output 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) + self:T2(self.id..text) -- Check if player is in strafe zone and below max alt. if unitinzone then @@ -1953,7 +2369,7 @@ function RANGE:_CheckInZone(_unitName) -- 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) + self:T2(self.id..text) -- Player is inside zone. if unitinzone then @@ -1983,6 +2399,7 @@ end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Menu Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Add menu commands for player. -- @param #RANGE self @@ -2077,16 +2494,55 @@ function RANGE:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName) end else - self:T(RANGE.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) + self:T(self.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) end else - self:T(RANGE.id.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) + self:T(self.id.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Helper Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get the number of shells a unit currently has. +-- @param #RANGE self +-- @param #RANGE.BombTarget target Bomb target data. +-- @return Core.Point#COORDINATE Target coordinate. +function RANGE:_GetBombTargetCoordinate(target) + + local coord=nil --Core.Point#COORDINATE + + if target.type==RANGE.TargetType.UNIT then + + if not target.move then + -- Target should not move. + coord=target.coordinate + else + -- Moving target. Check if alive and get current position + if target.target and target.target:IsAlive() then + coord=target.target:GetCoordinate() + end + end + + elseif target.type==RANGE.TargetType.STATIC then + + -- Static targets dont move. + coord=target.coordinate + + elseif target.type==RANGE.TargetType.COORD then + + -- Coordinates dont move. + coord=target.coordinate + + else + self:E(self.id.."ERROR: Unknown target type.") + end + + return coord +end + --- Get the number of shells a unit currently has. -- @param #RANGE self @@ -2110,7 +2566,7 @@ function RANGE:_GetAmmo(unitname) if ammotable ~= nil then local weapons=#ammotable - self:T2(RANGE.id..string.format("Number of weapons %d.", weapons)) + self:T2(self.id..string.format("Number of weapons %d.", weapons)) for w=1,weapons do @@ -2124,11 +2580,11 @@ function RANGE:_GetAmmo(unitname) ammo=ammo+Nammo local text=string.format("Player %s has %d rounds ammo of type %s", playername, Nammo, Tammo) - self:T(RANGE.id..text) + self:T(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) else local text=string.format("Player %s has %d ammo of type %s", playername, Nammo, Tammo) - self:T(RANGE.id..text) + self:T(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) end end @@ -2197,8 +2653,8 @@ function RANGE:_IlluminateBombTargets(_unitName) for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - if _target and _target:IsAlive() then - local coord=_target:GetCoordinate() --Core.Point#COORDINATE + local coord=self:_GetBombTargetCoordinate(_bombtarget) + if coord then table.insert(bomb, coord) end end @@ -2271,26 +2727,30 @@ function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) _clear=true end - -- Group ID. - local _gid=_unit:GetGroup():GetID() + -- Check if unit is alive. + if _unit and _unit:IsAlive() then - -- Get playername and player settings - local _, playername=self:_GetPlayerUnitAndName(_unit:GetName()) - local playermessage=self.PlayerSettings[playername].messages + -- Group ID. + local _gid=_unit:GetGroup():GetID() + + -- 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 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) + -- Send message to examiner. + if self.examinergroupname~=nil then + local _examinerid=GROUP:FindByName(self.examinergroupname):GetID() + if _examinerid then + trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) + end + end end - - -- Send message to examiner. - if self.examinergroupname~=nil then - local _examinerid=GROUP:FindByName(self.examinergroupname):GetID() - if _examinerid then - trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) - end - end - + end --- Toggle status of smoking bomb impact points. @@ -2384,8 +2844,8 @@ function RANGE:_SmokeBombTargets(unitname) for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - if _target and _target:IsAlive() then - local coord = _target:GetCoordinate() --Core.Point#COORDINATE + local coord=self:_GetBombTargetCoordinate(_bombtarget) + if coord then coord:Smoke(self.BombSmokeColor) end end @@ -2535,20 +2995,20 @@ function RANGE:_CheckStatic(name) -- 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)) + self:T(self.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)) + self:T3(self.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 else - self:T3(RANGE.id..string.format("No unit object with name %s exists.", name)) + self:T3(self.id..string.format("No unit object with name %s exists.", name)) end -- If not unit or static exist, we return nil. @@ -2620,21 +3080,4 @@ function RANGE:_myname(unitname) return string.format("%s (%s)", csign, pname) end ---- Split string. Cf http://stackoverflow.com/questions/1426954/split-string-in-lua --- @param #RANGE self --- @param #string str Sting to split. --- @param #string sep Speparator for split. --- @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/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 792194f9b..8bc34ee99 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -227,6 +227,7 @@ -- @field #string trappath Path where to save the trap sheets. -- @field #string trapprefix File prefix for trap sheet files. -- @field #number initialmaxalt Max altitude in meters to register in the inital zone. +-- @field #boolean welcome If true, display welcome message to player. -- @extends Core.Fsm#FSM --- Be the boss! @@ -1225,6 +1226,7 @@ AIRBOSS = { trappath = nil, trapprefix = nil, initialmaxalt = nil, + welcome = nil, } --- Aircraft types capable of landing on carrier (human+AI). @@ -1671,7 +1673,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="0.9.9.9" +AIRBOSS.version="1.0.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1890,6 +1892,9 @@ function AIRBOSS:New(carriername, alias) self:SetMenuSmokeZones() self:SetMenuSingleCarrier(false) + -- Welcome players. + self:SetWelcomePlayers(true) + -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then self:_InitStennis() @@ -2224,6 +2229,18 @@ end -- USER API Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Set welcome messages for players. +-- @param #AIRBOSS self +-- @param #boolean switch If true, display welcome message to player. +-- @return #AIRBOSS self +function AIRBOSS:SetWelcomePlayers(switch) + + self.welcome=switch + + return self +end + + --- Set carrier controlled area (CCA). -- This is a large zone around the carrier, which is constantly updated wrt the carrier position. -- @param #AIRBOSS self @@ -7126,7 +7143,9 @@ 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), string.format("AIRBOSS %s", self.alias), "", 5) + if self.welcome then + self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), string.format("AIRBOSS %s", self.alias), "", 5) + end end @@ -17071,15 +17090,11 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) 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,Wind,Modex,Airframe,Carrier Type,Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n" -- Loop over all players. + local n=0 for playername,grades in pairs(self.playerscores) do -- Loop over player grades table. @@ -17106,9 +17121,14 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case, grade.wind, grade.modex, grade.airframe, grade.carriertype, grade.carriername, grade.theatre, grade.mitime, grade.midate, grade.osdate) + n=n+1 end end + -- Info + local text=string.format("Saving %d player LSO grades to file %s", n, filename) + self:I(self.lid..text) + -- Save file. _savefile(filename, scores) end @@ -17219,6 +17239,7 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) self.playerscores={} -- Loop over all lines. + local n=0 for _,gradeline in pairs(playergrades) do -- Parameters are separated by commata. @@ -17264,10 +17285,16 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) -- Add grade to table. table.insert(self.playerscores[playername], grade) + n=n+1 + -- Debug info. self:T2({playername, self.playerscores[playername]}) end + -- Info message. + local text=string.format("Loaded %d player LSO grades from file %s", n, filename) + self:I(self.lid..text) + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ From 1440f6c6268c5b1c3f512be18c5166795cf8b2be Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 3 May 2019 19:11:53 +0200 Subject: [PATCH 268/485] WIP, not for play. --- Moose Development/Moose/AI/AI_Escort.lua | 336 ++++++++++++++---- .../Moose/AI/AI_Escort_Request.lua | 3 +- Moose Development/Moose/Utilities/Enums.lua | 6 + 3 files changed, 281 insertions(+), 64 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 0ecd87ffe..3d3a9c912 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -270,8 +270,7 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) EscortGroup.EscortMode = AI_ESCORT.MODE.FOLLOW end ) - - + return self end @@ -279,12 +278,10 @@ end -- @param Core.Set#SET_GROUP EscortGroupSet function AI_ESCORT:onafterStart( EscortGroupSet ) - self:E("Start") - EscortGroupSet:ForEachGroup( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - --EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName() ) + EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName(), self.MainMenu ) EscortGroup:WayPointInitialize( 1 ) EscortGroup:OptionROTVertical() @@ -294,7 +291,7 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) local LeaderEscort = EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP - local Report = REPORT:New( "Escorts Reporting." ) + local Report = REPORT:New( "Escort reporting:" ) Report:Add( "Current coordinate: " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) Report:Add( "Configuration: " .. EscortGroupSet:GetUnitTypeNames():Text( ", " ) ) Report:Add( "Joining Up ..." ) @@ -345,15 +342,23 @@ function AI_ESCORT:Menus( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevel -- self:MenuScanForTargets( 100, 60 ) + self.XStart = XStart or self.XStart + self.XSpace = XSpace or self.XSpace + self.YStart = YStart or self.YStart + self.YSpace = YSpace or self.YSpace + self.ZStart = ZStart or self.ZStart + self.ZSpace = ZSpace or self.ZSpace + self.ZLevels = ZLevels or self.ZLevels + self:MenuJoinUp() - self:MenuFormationTrail(XStart,XSpace,YStart) - self:MenuFormationStack(XStart,XSpace,YStart,YSpace) - self:MenuFormationLeftLine(XStart,YStart,ZStart,ZSpace) - self:MenuFormationRightLine(XStart,YStart,ZStart,ZSpace) - self:MenuFormationLeftWing(XStart,XSpace,YStart,ZStart,ZSpace) - self:MenuFormationRightWing(XStart,XSpace,YStart,ZStart,ZSpace) - self:MenuFormationVic(XStart,XSpace,YStart,YSpace,ZStart,ZSpace) - self:MenuFormationBox(XStart,XSpace,YStart,YSpace,ZStart,ZSpace,ZLevels) + self:MenuFormationTrail(self.XStart,self.XSpace,self.YStart) + self:MenuFormationStack(self.XStart,self.XSpace,self.YStart,self.YSpace) + self:MenuFormationLeftLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationRightLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationLeftWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationRightWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationVic(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace) + self:MenuFormationBox(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace,self.ZLevels) self:MenuHoldAtEscortPosition( 1000, 500 ) self:MenuHoldAtLeaderPosition( 1000, 500 ) @@ -364,7 +369,7 @@ function AI_ESCORT:Menus( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevel self:MenuReportTargets( 60 ) self:MenuAssistedAttack() self:MenuROE() --- self:MenuEvasion() + self:MenuROT() -- self:MenuResumeMission() @@ -377,7 +382,7 @@ end function AI_ESCORT:MenuFormation( Formation, ... ) if not self.FlightMenuFormation then - self.FlightMenuFormation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Formation", self.FlightMenu ) + self.FlightMenuFormation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Formation", self.MainMenu ) end if not self["FlightMenuFormation"..Formation] then @@ -412,6 +417,23 @@ function AI_ESCORT:MenuJoinUp() if not self.FlightMenuJoinUp then self.FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Join Up", self.FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) end + + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName(), self.MainMenu ) + if not EscortGroup.EscortMenuReportNavigation then + EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) + end + + if not EscortGroup.EscortMenuJoinUpAndFollow then + EscortGroup.EscortMenuJoinUpAndFollow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Join-Up", EscortGroup.EscortMenuReportNavigation, ESCORT._JoinUp, self, EscortGroup ) + end + + end + end + ) return self end @@ -601,25 +623,48 @@ function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) end end - if not self.FlightMenuHold then - self.FlightMenuHold = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold position", self.FlightMenu ) + self.FlightMenuHold = self.FlightMenuHold or {} + + if not self.FlightMenuHold[MenuText] then + self.FlightMenuHold[MenuText] = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, self.FlightMenu ) end - if not self.FlightMenuHoldPosition then - self.FlightMenuHoldPosition = {} - end - - self.FlightMenuHoldPosition[#self.FlightMenuHoldPosition+1] = MENU_GROUP_COMMAND + self.FlightMenuHoldPosition = self.FlightMenuHoldPosition or {} + self.FlightMenuHoldPosition[MenuText] = MENU_GROUP_COMMAND :New( self.EscortUnit:GetGroup(), - MenuText, - self.FlightMenuHold, + "Flight", + self.FlightMenuHold[MenuText], AI_ESCORT._FlightHoldPosition, self, nil, Height, Speed ) + + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + + EscortGroup.EscortMenuHoldPosition = EscortGroup.EscortMenuHoldPosition or {} + EscortGroup.EscortMenuHoldPosition[MenuText] = MENU_GROUP_COMMAND + :New( + self.EscortUnit:GetGroup(), + EscortGroupName, + self.FlightMenuHold[MenuText], + AI_ESCORT._HoldPosition, + self, + EscortGroup, + EscortGroup, + Height, + Speed + ) + end + end + ) return self end @@ -635,10 +680,6 @@ end function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) self:F( { Height, Speed, MenuTextFormat } ) - if not self.FlightMenuHold then - self.FlightMenuHold = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold position", self.FlightMenu ) - end - if not Height then Height = 30 end @@ -662,15 +703,18 @@ function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) end end - if not self.FlightMenuHoldAtLeaderPosition then - self.FlightMenuHoldAtLeaderPosition = {} + self.FlightMenuHold = self.FlightMenuHold or {} + + if not self.FlightMenuHold[MenuText] then + self.FlightMenuHold[MenuText] = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, self.FlightMenu ) end - self.FlightMenuHoldAtLeaderPosition[#self.FlightMenuHoldAtLeaderPosition+1] = MENU_GROUP_COMMAND + self.FlightMenuHoldAtLeaderPosition = self.FlightMenuHoldAtLeaderPosition or {} + self.FlightMenuHoldAtLeaderPosition[MenuText] = MENU_GROUP_COMMAND :New( self.EscortUnit:GetGroup(), MenuText, - self.FlightMenuHold, + self.FlightMenuHold[MenuText], AI_ESCORT._FlightHoldPosition, self, self.EscortUnit:GetGroup(), @@ -678,6 +722,30 @@ function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) Speed ) + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + + EscortGroup.EscortMenuHoldAtLeaderPosition = EscortGroup.EscortMenuHoldAtLeaderPosition or {} + EscortGroup.EscortMenuHoldAtLeaderPosition[MenuText] = MENU_GROUP_COMMAND + :New( + self.EscortUnit:GetGroup(), + EscortGroupName, + self.FlightMenuHold[MenuText], + AI_ESCORT._HoldPosition, + self, + self.EscortUnit:GetGroup(), + EscortGroup, + Height, + Speed + ) + end + end + ) + return self end @@ -767,6 +835,33 @@ function AI_ESCORT:MenuFlare( MenuTextFormat ) self.FlightMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release yellow flare", self.FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) end + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + + EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName(), self.MainMenu ) + + if not EscortGroup.EscortMenuReportNavigation then + EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Flare" + else + MenuText = MenuTextFormat + end + + if not EscortGroup.EscortMenuFlare then + EscortGroup.EscortMenuFlare = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, EscortGroup.EscortMenuReportNavigation ) + EscortGroup.EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release green flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green, "Released a green flare!" ) + EscortGroup.EscortMenuFlareRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release red flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Red, "Released a red flare!" ) + EscortGroup.EscortMenuFlareWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release white flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.White, "Released a white flare!" ) + EscortGroup.EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release yellow flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Yellow, "Released a yellow flare!" ) + end + end + ) + return self end @@ -800,6 +895,36 @@ function AI_ESCORT:MenuSmoke( MenuTextFormat ) self.FlightMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release blue smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) end + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if not EscortGroup:IsAir() then + + EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName(), self.MainMenu ) + + if not EscortGroup.EscortMenuReportNavigation then + EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Smoke" + else + MenuText = MenuTextFormat + end + + if not EscortGroup.EscortMenuSmoke then + EscortGroup.EscortMenuSmoke = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Smoke", EscortGroup.EscortMenuReportNavigation ) + EscortGroup.EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release green smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green, "Releasing green smoke!" ) + EscortGroup.EscortMenuSmokeRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release red smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Red, "Releasing red smoke!" ) + EscortGroup.EscortMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release white smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.White, "Releasing white smoke!" ) + EscortGroup.EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release orange smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + EscortGroup.EscortMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release blue smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Blue, "Releasing blue smoke!" ) + end + end + end + ) + return self end @@ -819,6 +944,8 @@ function AI_ESCORT:MenuReportTargets( Seconds ) if not Seconds then Seconds = 30 end + + self.FlightMenuReportTargetsInterval = Seconds local timer = 1 @@ -832,6 +959,31 @@ function AI_ESCORT:MenuReportTargets( Seconds ) self.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, 5, Seconds ) + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + + --EscortGroup.EscortMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName() ) + + --if not EscortGroup.EscortMenuReportNearbyTargets then + -- EscortGroup.EscortMenuReportNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Report targets", EscortGroup.EscortMenu ) + --end + + -- Report Targets + --EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets now!", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) + --EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) + --EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) + + -- Attack Targets + EscortGroup.EscortMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Attack targets", EscortGroup.EscortMenu ) + + --EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, timer, Seconds ) + timer=timer+1 + end + end + ) + return self end @@ -869,17 +1021,45 @@ function AI_ESCORT:MenuROE( MenuTextFormat ) end if not self.FlightMenuROEHoldFire then - self.FlightMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Hold fire", self.FlightMenuROE, AI_ESCORT._ROE, self, ENUMS.ROE.HoldFire, "Holding weapons!" ) + self.FlightMenuROEHoldFire = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold fire", self.FlightMenuROE ) + self.FlightMenuROEHoldFireFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROEHoldFire, AI_ESCORT._ROE, self, GROUP.OptionROEHoldFire, "Holding weapons!" ) end if not self.FlightMenuROEReturnFire then - self.FlightMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Return fire", self.FlightMenuROE, AI_ESCORT._ROE, self, ENUMS.ROE.ReturnFire, "Returning fire!" ) + self.FlightMenuROEReturnFire = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Return fire", self.FlightMenuROE ) + self.FlightMenuROEReturnFireFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROEReturnFire, AI_ESCORT._ROE, self, GROUP.OptionROEReturnFire, "Returning fire!" ) end if not self.FlightMenuROEOpenFire then - self.FlightMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Open fire", self.FlightMenuROE, AI_ESCORT._ROE, self, ENUMS.ROE.OpenFire, "Open fire at designated targets!" ) + self.FlightMenuROEOpenFire = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Open fire", self.FlightMenuROE ) + self.FlightMenuROEOpenFireFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROEOpenFire, AI_ESCORT._ROE, self, GROUP.OptionROEOpenFire, "Open fire at designated targets!" ) end if not self.FlightMenuROEWeaponFree then - self.FlightMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Engage all targets", self.FlightMenuROE, AI_ESCORT._ROE, self, ENUMS.ROE.WeaponFree, "Engaging all targets!" ) + self.FlightMenuROEWeaponFree = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Engage all targets", self.FlightMenuROE ) + self.FlightMenuROEWeaponFreeFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROEWeaponFree, AI_ESCORT._ROE, self, GROUP.OptionROEWeaponFree, "Engaging all targets!" ) end + + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if not EscortGroup.EscortMenuROE then + -- Rules of Engagement + + local EscortGroupName = EscortGroup:GetName() + + if EscortGroup:OptionROEHoldFirePossible() then + EscortGroup.EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROEHoldFire, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEHoldFire, "Holding weapons!" ) + end + if EscortGroup:OptionROEReturnFirePossible() then + EscortGroup.EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROEReturnFire, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEReturnFire, "Returning fire!" ) + end + if EscortGroup:OptionROEOpenFirePossible() then + EscortGroup.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROEOpenFire, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEOpenFire, "Opening fire on designated targets!!" ) + end + if EscortGroup:OptionROEWeaponFreePossible() then + EscortGroup.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROEWeaponFree, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEWeaponFree, "Opening fire on targets of opportunity!" ) + end + end + end + ) return self end @@ -889,27 +1069,50 @@ end -- All rules of engagement will appear under the menu **Evasion**. -- @param #AI_ESCORT self -- @return #AI_ESCORT -function AI_ESCORT:MenuEvasion( MenuTextFormat ) +function AI_ESCORT:MenuROT( MenuTextFormat ) self:F( MenuTextFormat ) + if not self.FlightMenuROT then + self.FlightMenuROT = MENU_GROUP:New( self.EscortUnit:GetGroup(), "ROT", self.FlightMenu ) + end + + if not self.FlightMenuROTNoReaction then + self.FlightMenuROTNoReaction = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Fight until death", self.FlightMenuROT ) + self.FlightMenuROTNoReactionFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROTNoReaction, AI_ESCORT._ROT, self, GROUP.OptionROTNoReaction, "Fighting until death!" ) + end + if not self.FlightMenuROTPassiveDefense then + self.FlightMenuROTPassiveDefense = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Use flares, chaff and jammers", self.FlightMenuROT ) + self.FlightMenuROTPassiveDefenseFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROTPassiveDefense, AI_ESCORT._ROT, self, GROUP.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) + end + if not self.FlightMenuROTEvadeFire then + self.FlightMenuROTEvadeFire = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Open fire", self.FlightMenuROT ) + self.FlightMenuROTEvadeFireFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Evade enemy fire", self.FlightMenuROTEvadeFire, AI_ESCORT._ROT, self, GROUP.OptionROTEvadeFire, "Evading on enemy fire!" ) + end + if not self.FlightMenuROTVertical then + self.FlightMenuROTVertical = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Avoid radar and evade fire", self.FlightMenuROT ) + self.FlightMenuROTVerticalFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROTVertical, AI_ESCORT._ROT, self, GROUP.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) + end + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + if not EscortGroup.EscortMenuEvasion then -- Reaction to Threats - EscortGroup.EscortMenuEvasion = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Evasion", EscortGroup.EscortMenu ) if EscortGroup:OptionROTNoReactionPossible() then - EscortGroup.EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Fight until death", EscortGroup.EscortMenuEvasion, AI_ESCORT._ROT, self, EscortGroup, EscortGroup:OptionROTNoReaction(), "Fighting until death!" ) + EscortGroup.EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROTNoReaction, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTNoReaction, "Fighting until death!" ) end if EscortGroup:OptionROTPassiveDefensePossible() then - EscortGroup.EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Use flares, chaff and jammers", EscortGroup.EscortMenuEvasion, AI_ESCORT._ROT, self, EscortGroup, EscortGroup:OptionROTPassiveDefense(), "Defending using jammers, chaff and flares!" ) + EscortGroup.EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROTPassiveDefense, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) end if EscortGroup:OptionROTEvadeFirePossible() then - EscortGroup.EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Evade enemy fire", EscortGroup.EscortMenuEvasion, AI_ESCORT._ROT, self, EscortGroup, EscortGroup:OptionROTEvadeFire(), "Evading on enemy fire!" ) + EscortGroup.EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROTEvadeFire, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTEvadeFire, "Evading on enemy fire!" ) end if EscortGroup:OptionROTVerticalPossible() then - EscortGroup.EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Go below radar and evade fire", EscortGroup.EscortMenuEvasion, AI_ESCORT._ROT, self, EscortGroup, EscortGroup:OptionROTVertical(), "Evading on enemy fire with vertical manoeuvres!" ) + EscortGroup.EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROTVertical, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) end end end @@ -1126,6 +1329,15 @@ function AI_ESCORT:_FlightSmoke( Color, Message ) end +function AI_ESCORT:_ReportNearbyTargetsNow( EscortGroup ) + + local EscortUnit = self.EscortUnit + + self:_ReportTargetsScheduler( EscortGroup ) + +end + + function AI_ESCORT:_FlightReportNearbyTargetsNow() self:_FlightReportTargetsScheduler() @@ -1320,40 +1532,38 @@ function AI_ESCORT:_AssistTarget( EscortGroup, DetectedItem ) end +function AI_ESCORT:_ROE( EscortGroup, EscortROEFunction, EscortROEMessage ) + pcall( function() EscortROEFunction() end ) + EscortGroup:MessageTypeToGroup( EscortROEMessage, MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) +end -function AI_ESCORT:_ROE( EscortAction, EscortROEMessage ) - - self:F({EscortAction=EscortAction,EscortROEMessage=EscortROEMessage}) - - local EscortUnit = self.EscortUnit - +function AI_ESCORT:_FlightROE( EscortROEFunction, EscortROEMessage ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Wrapper.Group#GROUP EscortGroup function( EscortGroup ) - if EscortAction == ENUMS.ROE.HoldFire then - EscortGroup:OptionROEHoldFire() - elseif EscortAction == ENUMS.ROE.ReturnFire then - EscortGroup:OptionROEReturnFire() - elseif EscortAction == ENUMS.ROE.OpenFire then - EscortGroup:OptionROEOpenFire() - elseif EscortAction == ENUMS.ROE.WeaponFree then - EscortGroup:OptionROEWeaponFree() - end - EscortGroup:MessageTypeToGroup( EscortROEMessage, MESSAGE.Type.Information, EscortUnit:GetGroup() ) + self:_ROE( EscortGroup, EscortROEFunction, EscortROEMessage ) end ) end function AI_ESCORT:_ROT( EscortGroup, EscortROTFunction, EscortROTMessage ) - - local EscortUnit = self.EscortUnit - pcall( function() EscortROTFunction() end ) - EscortGroup:MessageTypeToGroup( EscortROTMessage, MESSAGE.Type.Information, EscortUnit:GetGroup() ) + EscortGroup:MessageTypeToGroup( EscortROTMessage, MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) end +function AI_ESCORT:_FlightROT( EscortROTFunction, EscortROTMessage ) + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROT( EscortGroup, EscortROTFunction, EscortROTMessage ) + end + ) +end + + + function AI_ESCORT:_ResumeMission( WayPoint ) local EscortGroup = self.EscortGroup diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index 2c6b24e00..372f09d22 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -243,6 +243,7 @@ function AI_ESCORT_REQUEST:SpawnEscort() LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.EscortUnit ) self:FormationTrail( 50, 50, 50 ) self:JoinFormation( EscortGroup ) + self:Menus() end ) @@ -253,7 +254,7 @@ end function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) if not self.MenuRequestEscort then - self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request A2G Escort", nil, + self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request A2G Escort", self.FlightMenu, function() self:SpawnEscort() end diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index b5f1e0fde..a5bd06237 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -7,3 +7,9 @@ ENUMS.ROE = { WeaponFree = 4 } +ENUMS.ROT = { + NoReaction = 1, + PassiveDefense = 2, + EvadeFire = 3, + Vertical = 4 +} \ No newline at end of file From 93951dd4b485675df4c014faded1eaf8cef15ce8 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 4 May 2019 07:10:09 +0200 Subject: [PATCH 269/485] WIP, not for playing. --- Moose Development/Moose/AI/AI_Escort.lua | 73 ++++++++++++------------ 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 3d3a9c912..d09aedaec 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -828,23 +828,26 @@ function AI_ESCORT:MenuFlare( MenuTextFormat ) end if not self.FlightMenuFlare then + self.FlightMenuFlare = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, self.FlightMenuReportNavigation ) - self.FlightMenuFlareGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release green flare", self.FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Green, "Released a green flare!" ) - self.FlightMenuFlareRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release red flare", self.FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Red, "Released a red flare!" ) - self.FlightMenuFlareWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release white flare", self.FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.White, "Released a white flare!" ) - self.FlightMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release yellow flare", self.FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) + + self.FlightMenuFlareGreen = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release green flare", self.FlightMenuFlare ) + self.FlightMenuFlareRed = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release red flare", self.FlightMenuFlare ) + self.FlightMenuFlareWhite = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release white flare", self.FlightMenuFlare ) + self.FlightMenuFlareYellow = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release yellow flare", self.FlightMenuFlare ) + + self.FlightMenuFlareGreenFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuFlareGreen, AI_ESCORT._FlightFlare, self, FLARECOLOR.Green, "Released a green flare!" ) + self.FlightMenuFlareRedFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuFlareRed, AI_ESCORT._FlightFlare, self, FLARECOLOR.Red, "Released a red flare!" ) + self.FlightMenuFlareWhiteFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuFlareWhite, AI_ESCORT._FlightFlare, self, FLARECOLOR.White, "Released a white flare!" ) + self.FlightMenuFlareYellowFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuFlareYellow, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) end self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName(), self.MainMenu ) - - if not EscortGroup.EscortMenuReportNavigation then - EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) - end - + local EscortGroupName = EscortGroup:GetCallsign() + local MenuText = "" if not MenuTextFormat then MenuText = "Flare" @@ -852,13 +855,10 @@ function AI_ESCORT:MenuFlare( MenuTextFormat ) MenuText = MenuTextFormat end - if not EscortGroup.EscortMenuFlare then - EscortGroup.EscortMenuFlare = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, EscortGroup.EscortMenuReportNavigation ) - EscortGroup.EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release green flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green, "Released a green flare!" ) - EscortGroup.EscortMenuFlareRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release red flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Red, "Released a red flare!" ) - EscortGroup.EscortMenuFlareWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release white flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.White, "Released a white flare!" ) - EscortGroup.EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release yellow flare", EscortGroup.EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Yellow, "Released a yellow flare!" ) - end + EscortGroup.EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuFlareGreen, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green, "Released a green flare!" ) + EscortGroup.EscortMenuFlareRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuFlareRed, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Red, "Released a red flare!" ) + EscortGroup.EscortMenuFlareWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuFlareWhite, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.White, "Released a white flare!" ) + EscortGroup.EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuFlareYellow, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Yellow, "Released a yellow flare!" ) end ) @@ -887,12 +887,20 @@ function AI_ESCORT:MenuSmoke( MenuTextFormat ) end if not self.FlightMenuSmoke then - self.FlightMenuSmoke = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Smoke", self.FlightMenuReportNavigation ) - self.FlightMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release green smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) - self.FlightMenuSmokeRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release red smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) - self.FlightMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release white smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) - self.FlightMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release orange smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) - self.FlightMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release blue smoke", self.FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) + + self.FlightMenuSmoke = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, self.FlightMenuReportNavigation ) + + self.FlightMenuSmokeGreen = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release green smoke", self.FlightMenuSmoke ) + self.FlightMenuSmokeRed = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release red smoke", self.FlightMenuSmoke ) + self.FlightMenuSmokeWhite = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release white smoke", self.FlightMenuSmoke ) + self.FlightMenuSmokeOrange = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release orange smoke", self.FlightMenuSmoke ) + self.FlightMenuSmokeBlue = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release blue smoke", self.FlightMenuSmoke ) + + self.FlightMenuSmokeGreenFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuSmokeGreen, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) + self.FlightMenuSmokeRedFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuSmokeRed, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) + self.FlightMenuSmokeWhiteFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuSmokeWhite, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) + self.FlightMenuSmokeOrangeFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuSmokeOrange, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + self.FlightMenuSmokeBlueFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuSmokeBlue, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) end self.EscortGroupSet:ForSomeGroupAlive( @@ -900,11 +908,7 @@ function AI_ESCORT:MenuSmoke( MenuTextFormat ) function( EscortGroup ) if not EscortGroup:IsAir() then - EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName(), self.MainMenu ) - - if not EscortGroup.EscortMenuReportNavigation then - EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) - end + local EscortGroupName = EscortGroup:GetCallsign() local MenuText = "" if not MenuTextFormat then @@ -913,14 +917,11 @@ function AI_ESCORT:MenuSmoke( MenuTextFormat ) MenuText = MenuTextFormat end - if not EscortGroup.EscortMenuSmoke then - EscortGroup.EscortMenuSmoke = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Smoke", EscortGroup.EscortMenuReportNavigation ) - EscortGroup.EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release green smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green, "Releasing green smoke!" ) - EscortGroup.EscortMenuSmokeRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release red smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Red, "Releasing red smoke!" ) - EscortGroup.EscortMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release white smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.White, "Releasing white smoke!" ) - EscortGroup.EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release orange smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Orange, "Releasing orange smoke!" ) - EscortGroup.EscortMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Release blue smoke", EscortGroup.EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Blue, "Releasing blue smoke!" ) - end + EscortGroup.EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuSmokeGreen, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green, "Releasing green smoke!" ) + EscortGroup.EscortMenuSmokeRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuSmokeRed, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Red, "Releasing red smoke!" ) + EscortGroup.EscortMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuSmokeWhite, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.White, "Releasing white smoke!" ) + EscortGroup.EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuSmokeOrange, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + EscortGroup.EscortMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuSmokeBlue, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Blue, "Releasing blue smoke!" ) end end ) From 9bb1c83999a16a273af1510d9694c3e38d46ccb3 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 7 May 2019 22:00:07 +0200 Subject: [PATCH 270/485] MISC RANGE v2.1 - Added events when players enter/exit range zone. - misc RADIO - Added RADIOQUEUE class. WAREHOUSE v0.7.0 - Fixed bug in aircraft speed. - Decreased output if queues are empty. --- Moose Development/Moose/Core/Radio.lua | 477 +++++++++++++++++- Moose Development/Moose/Functional/Range.lua | 118 ++++- .../Moose/Functional/Warehouse.lua | 20 +- 3 files changed, 600 insertions(+), 15 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 9a2e6d570..ffd33255d 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -19,7 +19,7 @@ -- * 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, -- * They need to be added in .\l10n\DEFAULT\ in you .miz file (wich can be decompressed like a .zip file), --- * For simplicty sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. +-- * For simplicity sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. -- -- 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} -- @@ -28,7 +28,7 @@ -- -- 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, -- like the A10C or the Mirage 2000C. They will **hear the transmission** if they are tuned on the **right frequency and modulation** (and if they are close enough - more on that below). --- If a FC3 airacraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. The same is true for TACAN beacons. If your aircaft isn't compatible, +-- If an FC3 aircraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. The same is true for TACAN beacons. If your aircraft isn't compatible, -- you won't hear/be able to use the TACAN beacon informations. -- -- === @@ -39,7 +39,7 @@ -- @image Core_Radio.JPG ---- Models the radio capabilty. +--- Models the radio capability. -- -- ## RADIO usage -- @@ -56,12 +56,12 @@ -- * @{#RADIO.SetModulation}() : Sets the modulation of your transmission. -- * @{#RADIO.SetLoop}() : Choose if you want the transmission to be looped. If you need your transmission to be looped, you might need a @{#BEACON} instead... -- --- Additional Methods to set relevant parameters if the transmiter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} +-- Additional Methods to set relevant parameters if the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} -- -- * @{#RADIO.SetSubtitle}() : Set both the subtitle and its duration, -- * @{#RADIO.NewUnitTransmission}() : Shortcut to set all the relevant parameters in one method call -- --- Additional Methods to set relevant parameters if the transmiter is any other @{Wrapper.Positionable#POSITIONABLE} +-- Additional Methods to set relevant parameters if the transmitter is any other @{Wrapper.Positionable#POSITIONABLE} -- -- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts -- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call @@ -797,3 +797,470 @@ function BEACON:_TACANToFrequency(TACANChannel, TACANMode) return (A + TACANChannel - B) * 1000000 end +--- Manages radio transmissions. +-- +-- @type RADIOQUEUE +-- @field #string ClassName Name of the class "RADIOQUEUE". +-- @field #string lid ID for dcs.log. +-- @field #number frequency The radio frequency in Hz. +-- @field #number modulation The radio modulation. Either radio.modulation.AM or radio.modulation.FM. +-- @field Core.Scheduler#SCHEDULER scheduler The scheduler. +-- @field #string RQid The radio queue scheduler ID. +-- @field #table queue The queue of transmissions. +-- @field #number Tlast Time (abs) when the last transmission finished. +-- @field Core.Point#COORDINATE sendercoord Coordinate from where transmissions are broadcasted. +-- @field #number sendername Name of the sending unit or static. +-- @field Wrapper.Positionable#POSITIONABLE positionable The positionable to broadcast the message. +-- @field #number power Power of radio station in Watts. Default 100 W. +-- @field #table numbers Table of number transmission parameters. +-- @extends Core.Base#BASE +RADIOQUEUE = { + ClassName = "RADIOQUEUE", + lid=nil, + frequency=nil, + modulation=nil, + scheduler=nil, + RQid=nil, + queue={}, + Tlast=nil, + sendercoord=nil, + sendername=nil, + positionable=nil, + power=100, + numbers={}, +} + +--- Radio queue transmission data. +-- @type RADIOQUEUE.Transmission +-- @field #string filename Name of the file to be transmitted. +-- @field #string path Path in miz file where the file is located. +-- @field #number duration Duration in seconds. +-- @field #number Tstarted Mission time (abs) in seconds when the transmission started. +-- @field #boolean isplaying If true, transmission is currently playing. +-- @field #number Tplay Mission time (abs) in seconds when the transmission should be played. + + +--- Create a new RADIOQUEUE object for a given radio frequency/modulation. +-- @param #RADIOQUEUE self +-- @param #number frequency The radio frequency in MHz. +-- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:New(frequency, modulation) + + -- Inherit base + local self=BASE:Inherit(self, BASE:New()) -- Core.Radio#RADIOQUEUE + + self.lid="RADIOQUEUE | " + + if frequency==nil then + self:E(self.lid.."ERROR: No frequency specified as first parameter!") + return nil + end + + -- Frequency in Hz. + self.frequency=frequency*1000000 + + -- Modulation. + self.modulation=modulation or radio.modulation.AM + + -- Scheduler + self.scheduler=SCHEDULER:New() + + return self +end + +--- Start the radio queue. +-- @param #RADIOQUEUE self +-- @param #number delay (Optional) Delay in seconds, before the radio queue is started. Default 1 sec. +-- @param #number dt (Optional) Time step in seconds for checking the queue. Default 0.01 sec. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:Start(delay, dt) + + delay=delay or 1 + + dt=dt or 0.01 + + self:I(self.lid..string.format("Starting RADIOQUEUE in %.1f seconds with interval dt=%.3f seconds.", delay, dt)) + + self.RQid=self.scheduler:Schedule(self, self._CheckRadioQueue, {}, delay, dt) + + return self +end + +--- Stop the radio queue. Stop scheduler and delete queue. +-- @param #RADIOQUEUE self +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:Stop() + self:I(self.lid.."Stopping RADIOQUEUE.") + self.scheduler:Stop(self.RQid) + self.queue={} + return self +end + +--- Set coordinate from where the transmission is broadcasted. +-- @param #RADIOQUEUE self +-- @param Core.Point#COORDINATE coordinate Coordinate of the sender. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSenderCoordinate(coordinate) + self.sendercoord=coordinate + return self +end + +--- Set name of unit or static from which transmissions are made. +-- @param #RADIOQUEUE self +-- @param #string name Name of the unit or static used for transmissions. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSenderUnitName(name) + self.sendername=name + return self +end + +--- Set parameters of a digit. +-- @param #RADIOQUEUE self +-- @param #number digit The digit 0-9. +-- @param #string filename The name of the sound file. +-- @param #number duration The duration of the sound file in seconds. +-- @param #string path The directory within the miz file where the sound is located. Default "l10n/DEFAULT/". +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetDigit(digit, filename, duration, path) + + local transmission={} --#RADIOQUEUE.Transmission + transmission.filename=filename + transmission.duration=duration + transmission.path=path or "l10n/DEFAULT/" + + -- Convert digit to string in case it is given as a number. + if type(digit)=="number" then + digit=tostring(digit) + end + + -- Set transmission. + self.numbers[digit]=transmission + + return self +end + +--- Add a transmission to the radio queue. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission data table. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:AddTransmission(transmission) + self:F({transmission=transmission}) + + -- Init. + transmission.isplaying=false + transmission.Tstarted=nil + + -- Add to queue. + table.insert(self.queue, transmission) + + return self +end + +--- Add a transmission to the radio queue. +-- @param #RADIOQUEUE self +-- @param #string filename Name of the sound file. Usually an ogg or wav file type. +-- @param #number duration Duration in seconds the file lasts. +-- @param #number path Directory path inside the miz file where the sound file is located. Default "l10n/DEFAULT/". +-- @param #number tstart Start time (abs) seconds. Default now. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval) + + -- Sanity checks. + if not filename then + self:E(self.lid.."ERROR: No filename specified.") + return nil + end + if type(filename)~="string" then + self:E(self.lid.."ERROR: Filename specified is NOT a string.") + return nil + end + + if not duration then + self:E(self.lid.."ERROR: No duration of transmission specified.") + return nil + end + if type(duration)~="number" then + self:E(self.lid.."ERROR: Duration specified is NOT a number.") + return nil + end + + + local transmission={} --#RADIOQUEUE.Transmission + transmission.filename=filename + transmission.duration=duration + transmission.path=path or "l10n/DEFAULT/" + transmission.Tplay=tstart or timer.getAbsTime() + + -- Add transmission to queue. + self:AddTransmission(transmission) + + return self +end + +--- Convert a number (as string) into a radio transmission. +-- E.g. for board number or headings. +-- @param #RADIOQUEUE self +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +-- @param #number interval Interval between the next call. +-- @return #number Duration of the call in seconds. +function RADIOQUEUE:Number2Transmission(number, delay, interval) + + --- 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 + + -- Split string into characters. + local numbers=_split(number) + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Radio call. + local transmission=UTILS.DeepCopy(self.numbers[n]) --#RADIOQUEUE.Transmission + + transmission.Tplay=timer.getAbsTime()+(delay or 0) + + if interval and i==1 then + transmission.interval=interval + end + + self:AddTransmission(transmission) + + -- Add up duration of the number. + wait=wait+transmission.duration + end + + -- Return the total duration of the call. + return wait +end + + +--- Broadcast radio message. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission. +function RADIOQUEUE:Broadcast(transmission) + + -- Get unit sending the transmission. + local sender=self:_GetRadioSender() + + -- Construct file name. + local filename=string.format("%s%s", transmission.path, transmission.filename) + + -- Create subtitle for transmission. + --local subtitle=self:_RadioSubtitle(radio, call, loud) + + 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=self.frequency, -- Frequency in Hz. + modulation=self.modulation, + }} + + -- Command to tranmit the call. + local commandTransmit={ + id = "TransmitMessage", + params = { + file=filename, + duration=transmission.subduration or 5, + subtitle=transmission.subtitle or "", + 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().")) + + -- Position from where to transmit. + local vec3=nil + + -- Try to get positon from sender unit/static. + if self.sendername then + local coord=self:_GetRadioSenderCoord() + if coord then + vec3=coord:GetVec3() + end + end + + -- Try to get fixed positon. + if self.sendercoord and not vec3 then + vec3=self.sendercoord:GetVec3() + end + + -- Transmit via trigger. + if vec3 then + trigger.action.radioTransmission(filename, vec3, self.modulation, false, self.frequency, self.power) + end + + end +end + +--- Check radio queue for transmissions to be broadcasted. +-- @param #RADIOQUEUE self +function RADIOQUEUE:_CheckRadioQueue() + + -- Check if queue is empty. + if #self.queue==0 then + return + end + + -- Get current abs time. + local time=timer.getAbsTime() + + local playing=false + local next=nil --#RADIOQUEUE.Transmission + local remove=nil + for i,_transmission in ipairs(self.queue) do + local transmission=_transmission --#RADIOQUEUE.Transmission + + -- 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 ith element in queue. + remove=i + + -- Store time last transmission finished. + self.Tlast=time + + else -- still playing + + -- Transmission is still playing. + playing=true + + end + + else -- not playing yet + + local Tlast=self.Tlast + + if transmission.interval==nil then + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + else + + if time-Tlast>=transmission.interval then + next=transmission + else + + end + end + + -- We got a transmission or one with an interval that is not due yet. No need for anything else. + if next or Tlast then + break + end + + end + + else + + -- Transmission not due yet. + + end + end + + -- Found a new transmission. + if next~=nil and not playing then + self:Broadcast(next) + next.isplaying=true + next.Tstarted=time + end + + -- Remove completed calls from queue. + if remove then + table.remove(self.queue, remove) + end + +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #RADIOQUEUE self +-- @return Wrapper.Unit#UNIT Sending aircraft unit or nil if was not setup, is not an aircraft or is not alive. +function RADIOQUEUE:_GetRadioSender() + + -- Check if we have a sending aircraft. + local sender=nil --Wrapper.Unit#UNIT + + -- Try the general default. + if self.sendername then + -- First try to find a unit + sender=UNIT:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() and sender:IsAir() then + return sender + end + + end + + return nil +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #RADIOQUEUE self +-- @return Core.Point#COORDINATE Coordinate of the sender unit. +function RADIOQUEUE:_GetRadioSenderCoord() + + local vec3=nil + + -- Try the general default. + if self.sendername then + + -- First try to find a unit + local sender=UNIT:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() then + return sender:GetCoodinate() + end + + -- Now try a static. + local sender=STATIC:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender then + return sender:GetCoodinate() + end + + end + + return nil +end + diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 09c6be614..364a29660 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -52,6 +52,7 @@ -- @field #table bombingTargets Table of targets to bomb. -- @field #number nbombtargets Number of bombing targets. -- @field #number nstrafetargets Number of strafing targets. +-- @field #boolean messages Globally enable/disable all messages to players. -- @field #table MenuAddedTo Table for monitoring which players already got an F10 menu. -- @field #table planes Table for administration. -- @field #table strafeStatus Table containing the current strafing target a player as assigned to. @@ -217,6 +218,7 @@ RANGE={ id=nil, rangename=nil, location=nil, + messages=true, rangeradius=5000, rangezone=nil, strafeTargets={}, @@ -286,9 +288,11 @@ RANGE.TargetType={ -- @field #number smokecolor Color of smoke. -- @field #number flarecolor Color of flares. -- @field #boolean messages Display info messages. +-- @field Wrapper.Client#CLIENT client Client object of player. -- @field #string unitname Name of player aircraft unit. -- @field #string playername Name of player. -- @field #string airframe Aircraft type name. +-- @field #boolean inzone If true, player is inside the range zone. --- Bomb target data. -- @type RANGE.BombTarget @@ -336,7 +340,7 @@ RANGE.MenuF10Root=nil --- Range script version. -- @field #string version -RANGE.version="2.0.0" +RANGE.version="2.1.0" --TODO list: --TODO: Verbosity level for messages. @@ -388,6 +392,8 @@ function RANGE:New(rangename) self:AddTransition("Stopped", "Start", "Running") -- Start RANGE script. self:AddTransition("*", "Status", "*") -- Status of RANGE script. self:AddTransition("*", "Impact", "*") -- Impact of bomb/rocket/missile. + self:AddTransition("*", "EnterRange", "*") -- Player enters the range. + self:AddTransition("*", "ExitRange", "*") -- Player leaves the range. self:AddTransition("*", "Save", "*") -- Save player results. self:AddTransition("*", "Load", "*") -- Load player results. @@ -442,6 +448,44 @@ function RANGE:New(rangename) -- @param #string To To state. -- @param #RANGE.BombResult result Data of the bombing run. -- @param #RANGE.Playerdata player Data of player settings etc. + + --- Triggers the FSM event "EnterRange". + -- @function [parent=#RANGE] EnterRange + -- @param #RANGE self + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- Triggers the FSM delayed event "EnterRange". + -- @function [parent=#RANGE] __EnterRange + -- @param #RANGE self + -- @param #number delay Delay in seconds before the function is called. + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- On after "EnterRange" event user function. Called when a player enters the range zone. + -- @function [parent=#RANGE] OnAfterEnterRange + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- Triggers the FSM event "ExitRange". + -- @function [parent=#RANGE] ExitRange + -- @param #RANGE self + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- Triggers the FSM delayed event "ExitRange". + -- @function [parent=#RANGE] __ExitRange + -- @param #RANGE self + -- @param #number delay Delay in seconds before the function is called. + -- @param #RANGE.Playerdata player Data of player settings etc. + + --- On after "ExitRange" event user function. Called when a player leaves the range zone. + -- @function [parent=#RANGE] OnAfterExitRange + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.Playerdata player Data of player settings etc. -- Return object. return self @@ -721,6 +765,23 @@ function RANGE:DebugOFF() return self end +--- Disable ALL messages to players. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetMessagesOFF() + self.messages=false + return self +end + +--- Enable messages to players. This is the default +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetMessagesON() + self.messages=true + return self +end + + --- Enables tracking of all bomb types. Note that this is the default setting. -- @param #RANGE self -- @return #RANGE self @@ -1253,9 +1314,11 @@ function RANGE:OnEventBirth(EventData) self.PlayerSettings[_playername].flarecolor=FLARECOLOR.Red self.PlayerSettings[_playername].delaysmoke=true self.PlayerSettings[_playername].messages=true + self.PlayerSettings[_playername].client=CLIENT:FindByName(_unitName, nil, true) self.PlayerSettings[_playername].unitname=_unitName self.PlayerSettings[_playername].playername=_playername self.PlayerSettings[_playername].airframe=EventData.IniUnit:GetTypeName() + self.PlayerSettings[_playername].inzone=false -- Start check in zone timer. if self.planes[_uid] ~= true then @@ -1580,16 +1643,19 @@ end -- @param #string To To state. function RANGE:onafterStatus(From, Event, To) + -- Check range status. self:I(self.id..string.format("Range status: %s", self:GetState())) - --self:_CheckMissileStatus() + -- Check player status. + self:_CheckPlayers() -- Save results. if self.autosafe then self:Save() end - self:__Status(-60) + -- Check back in ~10 seconds. + self:__Status(-10) end @@ -2245,6 +2311,47 @@ end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Timer Functions +--- Check status of players. +-- @param #RANGE self +-- @param #string _unitName Name of player unit. +function RANGE:_CheckPlayers() + + for playername,_playersettings in pairs(self.PlayerSettings) do + local playersettings=_playersettings --#RANGE.PlayerData + + local unitname=playersettings.unitname + local unit=UNIT:FindByName(unitname) + + if unit and unit:IsAlive() then + + if unit:IsInZone(self.rangezone) then + + ------------------------------ + -- Player INSIDE Range Zone -- + ------------------------------ + + if not playersettings.inzone then + self:EnterRange(playersettings) + playersettings.inzone=true + end + + else + + ------------------------------- + -- Player OUTSIDE Range Zone -- + ------------------------------- + + if playersettings.inzone==true then + self:ExitRange(playersettings) + playersettings.inzone=false + end + + end + end + end + +end + --- Check if player is inside a strafing zone. If he is, we start looking for hits. If he was and left the zone again, the result is stored. -- @param #RANGE self -- @param #string _unitName Name of player unit. @@ -2727,6 +2834,11 @@ function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) _clear=true end + -- Messages globally disabled. + if self.messages==false then + return + end + -- Check if unit is alive. if _unit and _unit:IsAlive() then diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 3537c42d5..84fbb7cd0 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1733,7 +1733,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.9" +WAREHOUSE.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -7669,7 +7669,13 @@ function WAREHOUSE:_PrintQueue(queue, name) end - self:I(self.wid..text) + if #queue==0 then + self:T(self.wid..text) + else + if total~="Empty" then + self:I(self.wid..text) + end + end end --- Display status of warehouse. @@ -8109,28 +8115,28 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) --- Departure/Take-off c[#c+1]=Pdeparture - wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, true, departure, nil, "Departure") + wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb*3.6, true, departure, nil, "Departure") --- Begin of Cruise local Pcruise=Pdeparture:Translate(d_climb, heading) Pcruise.y=FLcruise c[#c+1]=Pcruise - wp[#wp+1]=Pcruise:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "Cruise") + wp[#wp+1]=Pcruise:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise*3.6, true, nil, nil, "Cruise") --- Descent local Pdescent=Pcruise:Translate(d_cruise, heading) Pdescent.y=FLcruise c[#c+1]=Pdescent - wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent, true, nil, nil, "Descent") + wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent*3.6, true, nil, nil, "Descent") --- Holding point Pholding.y=H_holding+h_holding c[#c+1]=Pholding - wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") + wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding*3.6, true, nil, nil, "Holding") --- Final destination. c[#c+1]=Pdestination - wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal, true, destination, nil, "Final Destination") + wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal*3.6, true, destination, nil, "Final Destination") -- Mark points at waypoints for debugging. From e09a02ec148c8faa45a3a2cab51baed7e6cb73e1 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 13 May 2019 11:22:49 +0200 Subject: [PATCH 271/485] Escort menus reworked. It took a while (sorry), was involved in many projects in RL. --- Moose Development/Moose/AI/AI_Escort.lua | 325 +++++++++-------------- 1 file changed, 131 insertions(+), 194 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index d09aedaec..2a78343c6 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -226,6 +226,10 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) self:F( { EscortUnit, EscortGroupSet } ) self.EscortUnit = self.FollowUnit -- Wrapper.Unit#UNIT + + self.PlayerUnit = self.FollowUnit -- Wrapper.Unit#UNIT + self.PlayerGroup = self.FollowUnit:GetGroup() -- Wrapper.Group#GROUP + self.EscortGroupSet = EscortGroupSet self.EscortGroupSet:SetSomeIteratorLimit( 5 ) @@ -250,7 +254,8 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) self.GT1 = 0 - self.FlightMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortName ) + self.MainMenu = MENU_GROUP:New( self.PlayerGroup, EscortName ) + self.FlightMenu = MENU_GROUP:New( self.PlayerGroup, "Flight", self.MainMenu ) EscortGroupSet:ForEachGroup( --- @param Core.Group#GROUP EscortGroup @@ -281,7 +286,7 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) EscortGroupSet:ForEachGroup( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName(), self.MainMenu ) + EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetName(), self.MainMenu ) EscortGroup:WayPointInitialize( 1 ) EscortGroup:OptionROTVertical() @@ -382,11 +387,11 @@ end function AI_ESCORT:MenuFormation( Formation, ... ) if not self.FlightMenuFormation then - self.FlightMenuFormation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Formation", self.MainMenu ) + self.FlightMenuFormation = MENU_GROUP:New( self.PlayerGroup, "Formation", self.MainMenu ) end if not self["FlightMenuFormation"..Formation] then - self["FlightMenuFormation"..Formation] = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), Formation, self.FlightMenuFormation, + self["FlightMenuFormation"..Formation] = MENU_GROUP_COMMAND:New( self.PlayerGroup, Formation, self.FlightMenuFormation, function ( self, Formation, ... ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup @@ -410,27 +415,19 @@ end -- @return #AI_ESCORT function AI_ESCORT:MenuJoinUp() - if not self.FlightMenuReportNavigation then - self.FlightMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", self.FlightMenu ) - end - - if not self.FlightMenuJoinUp then - self.FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Join Up", self.FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) - end + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then - EscortGroup.EscortMenu = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName(), self.MainMenu ) - if not EscortGroup.EscortMenuReportNavigation then - EscortGroup.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", EscortGroup.EscortMenu ) - end - - if not EscortGroup.EscortMenuJoinUpAndFollow then - EscortGroup.EscortMenuJoinUpAndFollow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Join-Up", EscortGroup.EscortMenuReportNavigation, ESCORT._JoinUp, self, EscortGroup ) - end - + + local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuJoinUp = MENU_GROUP:New( self.PlayerGroup, "Join Up", EscortMenuReportNavigation ) + end end ) @@ -623,18 +620,11 @@ function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) end end - self.FlightMenuHold = self.FlightMenuHold or {} - - if not self.FlightMenuHold[MenuText] then - self.FlightMenuHold[MenuText] = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, self.FlightMenu ) - end - - self.FlightMenuHoldPosition = self.FlightMenuHoldPosition or {} - self.FlightMenuHoldPosition[MenuText] = MENU_GROUP_COMMAND + local FlightMenuHoldPosition = MENU_GROUP_COMMAND :New( - self.EscortUnit:GetGroup(), - "Flight", - self.FlightMenuHold[MenuText], + self.PlayerGroup, + MenuText, + self.FlightMenu, AI_ESCORT._FlightHoldPosition, self, nil, @@ -648,13 +638,15 @@ function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) if EscortGroup:IsAir() then local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuHold = MENU_GROUP:New( self.PlayerGroup, MenuText, EscortMenuReportNavigation ) - EscortGroup.EscortMenuHoldPosition = EscortGroup.EscortMenuHoldPosition or {} - EscortGroup.EscortMenuHoldPosition[MenuText] = MENU_GROUP_COMMAND + local EscortMenuHoldPosition = MENU_GROUP_COMMAND :New( - self.EscortUnit:GetGroup(), + self.PlayerGroup, EscortGroupName, - self.FlightMenuHold[MenuText], + EscortMenuHold, AI_ESCORT._HoldPosition, self, EscortGroup, @@ -703,21 +695,14 @@ function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) end end - self.FlightMenuHold = self.FlightMenuHold or {} - - if not self.FlightMenuHold[MenuText] then - self.FlightMenuHold[MenuText] = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, self.FlightMenu ) - end - - self.FlightMenuHoldAtLeaderPosition = self.FlightMenuHoldAtLeaderPosition or {} - self.FlightMenuHoldAtLeaderPosition[MenuText] = MENU_GROUP_COMMAND + local FlightMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND :New( - self.EscortUnit:GetGroup(), + self.PlayerGroup, MenuText, - self.FlightMenuHold[MenuText], + self.FlightMenu, AI_ESCORT._FlightHoldPosition, self, - self.EscortUnit:GetGroup(), + self.PlayerGroup, Height, Speed ) @@ -728,16 +713,18 @@ function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) if EscortGroup:IsAir() then local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuHold = MENU_GROUP:New( self.PlayerGroup, MenuText, EscortMenuReportNavigation ) - EscortGroup.EscortMenuHoldAtLeaderPosition = EscortGroup.EscortMenuHoldAtLeaderPosition or {} - EscortGroup.EscortMenuHoldAtLeaderPosition[MenuText] = MENU_GROUP_COMMAND + local EscortMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND :New( - self.EscortUnit:GetGroup(), + self.PlayerGroup, EscortGroupName, - self.FlightMenuHold[MenuText], + EscortMenuHold, AI_ESCORT._HoldPosition, self, - self.EscortUnit:GetGroup(), + self.PlayerGroup, EscortGroup, Height, Speed @@ -761,7 +748,7 @@ function AI_ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) if self.EscortGroup:IsAir() then if not self.EscortMenuScan then - self.EscortMenuScan = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Scan for targets", self.EscortMenu ) + self.EscortMenuScan = MENU_GROUP:New( self.PlayerGroup, "Scan for targets", self.EscortMenu ) end if not Height then @@ -793,7 +780,7 @@ function AI_ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_GROUP_COMMAND :New( - self.EscortUnit:GetGroup(), + self.PlayerGroup, MenuText, self.EscortMenuScan, AI_ESCORT._ScanTargets, @@ -816,10 +803,6 @@ end function AI_ESCORT:MenuFlare( MenuTextFormat ) self:F() - if not self.FlightMenuReportNavigation then - self.FlightMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", self.FlightMenu ) - end - local MenuText = "" if not MenuTextFormat then MenuText = "Flare" @@ -827,38 +810,27 @@ function AI_ESCORT:MenuFlare( MenuTextFormat ) MenuText = MenuTextFormat end - if not self.FlightMenuFlare then + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuText, FlightMenuReportNavigation ) - self.FlightMenuFlare = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, self.FlightMenuReportNavigation ) - - self.FlightMenuFlareGreen = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release green flare", self.FlightMenuFlare ) - self.FlightMenuFlareRed = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release red flare", self.FlightMenuFlare ) - self.FlightMenuFlareWhite = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release white flare", self.FlightMenuFlare ) - self.FlightMenuFlareYellow = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release yellow flare", self.FlightMenuFlare ) - - self.FlightMenuFlareGreenFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuFlareGreen, AI_ESCORT._FlightFlare, self, FLARECOLOR.Green, "Released a green flare!" ) - self.FlightMenuFlareRedFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuFlareRed, AI_ESCORT._FlightFlare, self, FLARECOLOR.Red, "Released a red flare!" ) - self.FlightMenuFlareWhiteFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuFlareWhite, AI_ESCORT._FlightFlare, self, FLARECOLOR.White, "Released a white flare!" ) - self.FlightMenuFlareYellowFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuFlareYellow, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) - end + local FlightMenuFlareGreenFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Green, "Released a green flare!" ) + local FlightMenuFlareRedFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Red, "Released a red flare!" ) + local FlightMenuFlareWhiteFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.White, "Released a white flare!" ) + local FlightMenuFlareYellowFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release yellow flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - local EscortGroupName = EscortGroup:GetCallsign() - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Flare" - else - MenuText = MenuTextFormat - end - - EscortGroup.EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuFlareGreen, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green, "Released a green flare!" ) - EscortGroup.EscortMenuFlareRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuFlareRed, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Red, "Released a red flare!" ) - EscortGroup.EscortMenuFlareWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuFlareWhite, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.White, "Released a white flare!" ) - EscortGroup.EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuFlareYellow, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Yellow, "Released a yellow flare!" ) + local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuText, EscortMenuReportNavigation ) + + local EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green, "Released a green flare!" ) + local EscortMenuFlareRed = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Red, "Released a red flare!" ) + local EscortMenuFlareWhite = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.White, "Released a white flare!" ) + local EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release yellow flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Yellow, "Released a yellow flare!" ) end ) @@ -875,10 +847,6 @@ end function AI_ESCORT:MenuSmoke( MenuTextFormat ) self:F() - if not self.FlightMenuReportNavigation then - self.FlightMenuReportNavigation = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Navigation", self.FlightMenu ) - end - local MenuText = "" if not MenuTextFormat then MenuText = "Smoke" @@ -886,42 +854,31 @@ function AI_ESCORT:MenuSmoke( MenuTextFormat ) MenuText = MenuTextFormat end - if not self.FlightMenuSmoke then + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuText, FlightMenuReportNavigation ) - self.FlightMenuSmoke = MENU_GROUP:New( self.EscortUnit:GetGroup(), MenuText, self.FlightMenuReportNavigation ) + local FlightMenuSmokeGreenFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) + local FlightMenuSmokeRedFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) + local FlightMenuSmokeWhiteFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) + local FlightMenuSmokeOrangeFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release orange smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + local FlightMenuSmokeBlueFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release blue smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) - self.FlightMenuSmokeGreen = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release green smoke", self.FlightMenuSmoke ) - self.FlightMenuSmokeRed = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release red smoke", self.FlightMenuSmoke ) - self.FlightMenuSmokeWhite = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release white smoke", self.FlightMenuSmoke ) - self.FlightMenuSmokeOrange = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release orange smoke", self.FlightMenuSmoke ) - self.FlightMenuSmokeBlue = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Release blue smoke", self.FlightMenuSmoke ) - - self.FlightMenuSmokeGreenFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuSmokeGreen, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) - self.FlightMenuSmokeRedFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuSmokeRed, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) - self.FlightMenuSmokeWhiteFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuSmokeWhite, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) - self.FlightMenuSmokeOrangeFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuSmokeOrange, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) - self.FlightMenuSmokeBlueFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuSmokeBlue, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) - end self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if not EscortGroup:IsAir() then - local EscortGroupName = EscortGroup:GetCallsign() - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Smoke" - else - MenuText = MenuTextFormat - end - - EscortGroup.EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuSmokeGreen, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green, "Releasing green smoke!" ) - EscortGroup.EscortMenuSmokeRed = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuSmokeRed, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Red, "Releasing red smoke!" ) - EscortGroup.EscortMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuSmokeWhite, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.White, "Releasing white smoke!" ) - EscortGroup.EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuSmokeOrange, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Orange, "Releasing orange smoke!" ) - EscortGroup.EscortMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuSmokeBlue, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Blue, "Releasing blue smoke!" ) + local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuText, EscortMenuReportNavigation ) + + local EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green, "Releasing green smoke!" ) + local EscortMenuSmokeRed = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Red, "Releasing red smoke!" ) + local EscortMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.White, "Releasing white smoke!" ) + local EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release orange smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + local EscortMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release blue smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Blue, "Releasing blue smoke!" ) end end ) @@ -938,10 +895,6 @@ end function AI_ESCORT:MenuReportTargets( Seconds ) self:F( { Seconds } ) - if not self.FlightMenuReportNearbyTargets then - self.FlightMenuReportNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Report targets", self.FlightMenu ) - end - if not Seconds then Seconds = 30 end @@ -950,13 +903,15 @@ function AI_ESCORT:MenuReportTargets( Seconds ) local timer = 1 + local FlightMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", self.FlightMenu ) + -- Report Targets - self.FlightMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets now!", self.FlightMenuReportNearbyTargets, AI_ESCORT._FlightReportNearbyTargetsNow, self ) - self.FlightMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets on", self.FlightMenuReportNearbyTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, true ) - self.FlightMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets off", self.FlightMenuReportNearbyTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, false ) + local FlightMenuReportTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", FlightMenuReportTargets, AI_ESCORT._FlightReportNearbyTargetsNow, self ) + local FlightMenuReportTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, true ) + local FlightMenuReportTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, false ) -- Attack Targets - self.FlightMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Attack targets", self.FlightMenu ) + local FlightMenuAttackNearbyTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", self.FlightMenu ) self.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, 5, Seconds ) @@ -965,19 +920,18 @@ function AI_ESCORT:MenuReportTargets( Seconds ) function( EscortGroup ) if EscortGroup:IsAir() then - --EscortGroup.EscortMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroup:GetName() ) + local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", EscortMenu ) - --if not EscortGroup.EscortMenuReportNearbyTargets then - -- EscortGroup.EscortMenuReportNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Report targets", EscortGroup.EscortMenu ) - --end -- Report Targets - --EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets now!", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) - --EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) - --EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) + --EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) + --EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) + --EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) -- Attack Targets - EscortGroup.EscortMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Attack targets", EscortGroup.EscortMenu ) + local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortMenu ) --EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, timer, Seconds ) timer=timer+1 @@ -1002,7 +956,7 @@ function AI_ESCORT:MenuAssistedAttack() if not EscortGroup:IsAir() then -- Request assistance from other escorts. -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... - self.EscortMenuTargetAssistance = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Request assistance from", EscortGroup.EscortMenu ) + self.EscortMenuTargetAssistance = MENU_GROUP:New( self.PlayerGroup, "Request assistance from", EscortGroup.EscortMenu ) end end ) @@ -1017,26 +971,12 @@ end function AI_ESCORT:MenuROE( MenuTextFormat ) self:F( MenuTextFormat ) - if not self.FlightMenuROE then - self.FlightMenuROE = MENU_GROUP:New( self.EscortUnit:GetGroup(), "ROE", self.FlightMenu ) - end + local FlightMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", self.FlightMenu ) - if not self.FlightMenuROEHoldFire then - self.FlightMenuROEHoldFire = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Hold fire", self.FlightMenuROE ) - self.FlightMenuROEHoldFireFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROEHoldFire, AI_ESCORT._ROE, self, GROUP.OptionROEHoldFire, "Holding weapons!" ) - end - if not self.FlightMenuROEReturnFire then - self.FlightMenuROEReturnFire = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Return fire", self.FlightMenuROE ) - self.FlightMenuROEReturnFireFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROEReturnFire, AI_ESCORT._ROE, self, GROUP.OptionROEReturnFire, "Returning fire!" ) - end - if not self.FlightMenuROEOpenFire then - self.FlightMenuROEOpenFire = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Open fire", self.FlightMenuROE ) - self.FlightMenuROEOpenFireFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROEOpenFire, AI_ESCORT._ROE, self, GROUP.OptionROEOpenFire, "Open fire at designated targets!" ) - end - if not self.FlightMenuROEWeaponFree then - self.FlightMenuROEWeaponFree = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Engage all targets", self.FlightMenuROE ) - self.FlightMenuROEWeaponFreeFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROEWeaponFree, AI_ESCORT._ROE, self, GROUP.OptionROEWeaponFree, "Engaging all targets!" ) - end + local FlightMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", FlightMenuROE, AI_ESCORT._ROE, self, GROUP.OptionROEHoldFire, "Holding weapons!" ) + local FlightMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", FlightMenuROE, AI_ESCORT._ROE, self, GROUP.OptionROEReturnFire, "Returning fire!" ) + local FlightMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", FlightMenuROE, AI_ESCORT._ROE, self, GROUP.OptionROEOpenFire, "Open fire at designated targets!" ) + local FlightMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", FlightMenuROE, AI_ESCORT._ROE, self, GROUP.OptionROEWeaponFree, "Engaging all targets!" ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup @@ -1045,18 +985,20 @@ function AI_ESCORT:MenuROE( MenuTextFormat ) -- Rules of Engagement local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", EscortMenu ) if EscortGroup:OptionROEHoldFirePossible() then - EscortGroup.EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROEHoldFire, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEHoldFire, "Holding weapons!" ) + local EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEHoldFire, "Holding weapons!" ) end if EscortGroup:OptionROEReturnFirePossible() then - EscortGroup.EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROEReturnFire, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEReturnFire, "Returning fire!" ) + local EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEReturnFire, "Returning fire!" ) end if EscortGroup:OptionROEOpenFirePossible() then - EscortGroup.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROEOpenFire, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEOpenFire, "Opening fire on designated targets!!" ) + EscortGroup.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEOpenFire, "Opening fire on designated targets!!" ) end if EscortGroup:OptionROEWeaponFreePossible() then - EscortGroup.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROEWeaponFree, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEWeaponFree, "Opening fire on targets of opportunity!" ) + EscortGroup.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEWeaponFree, "Opening fire on targets of opportunity!" ) end end end @@ -1073,26 +1015,12 @@ end function AI_ESCORT:MenuROT( MenuTextFormat ) self:F( MenuTextFormat ) - if not self.FlightMenuROT then - self.FlightMenuROT = MENU_GROUP:New( self.EscortUnit:GetGroup(), "ROT", self.FlightMenu ) - end + local FlightMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", self.FlightMenu ) - if not self.FlightMenuROTNoReaction then - self.FlightMenuROTNoReaction = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Fight until death", self.FlightMenuROT ) - self.FlightMenuROTNoReactionFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROTNoReaction, AI_ESCORT._ROT, self, GROUP.OptionROTNoReaction, "Fighting until death!" ) - end - if not self.FlightMenuROTPassiveDefense then - self.FlightMenuROTPassiveDefense = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Use flares, chaff and jammers", self.FlightMenuROT ) - self.FlightMenuROTPassiveDefenseFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROTPassiveDefense, AI_ESCORT._ROT, self, GROUP.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) - end - if not self.FlightMenuROTEvadeFire then - self.FlightMenuROTEvadeFire = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Open fire", self.FlightMenuROT ) - self.FlightMenuROTEvadeFireFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Evade enemy fire", self.FlightMenuROTEvadeFire, AI_ESCORT._ROT, self, GROUP.OptionROTEvadeFire, "Evading on enemy fire!" ) - end - if not self.FlightMenuROTVertical then - self.FlightMenuROTVertical = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Avoid radar and evade fire", self.FlightMenuROT ) - self.FlightMenuROTVerticalFlight = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), "Flight", self.FlightMenuROTVertical, AI_ESCORT._ROT, self, GROUP.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) - end + local FlightMenuROTNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", FlightMenuROT, AI_ESCORT._ROT, self, GROUP.OptionROTNoReaction, "Fighting until death!" ) + local FlightMenuROTPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", FlightMenuROT, AI_ESCORT._ROT, self, GROUP.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) + local FlightMenuROTEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", FlightMenuROT, AI_ESCORT._ROT, self, GROUP.OptionROTEvadeFire, "Evading on enemy fire!" ) + local FlightMenuROTVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", FlightMenuROT, AI_ESCORT._ROT, self, GROUP.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup @@ -1100,20 +1028,22 @@ function AI_ESCORT:MenuROT( MenuTextFormat ) if EscortGroup:IsAir() then local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", EscortMenu ) if not EscortGroup.EscortMenuEvasion then -- Reaction to Threats if EscortGroup:OptionROTNoReactionPossible() then - EscortGroup.EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROTNoReaction, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTNoReaction, "Fighting until death!" ) + local EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTNoReaction, "Fighting until death!" ) end if EscortGroup:OptionROTPassiveDefensePossible() then - EscortGroup.EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROTPassiveDefense, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) + local EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) end if EscortGroup:OptionROTEvadeFirePossible() then - EscortGroup.EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROTEvadeFire, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTEvadeFire, "Evading on enemy fire!" ) + local EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTEvadeFire, "Evading on enemy fire!" ) end if EscortGroup:OptionROTVerticalPossible() then - EscortGroup.EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), EscortGroupName, self.FlightMenuROTVertical, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) + local EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) end end end @@ -1132,7 +1062,7 @@ function AI_ESCORT:MenuResumeMission() if not self.EscortMenuResumeMission then -- Mission Resume Menu Root - self.EscortMenuResumeMission = MENU_GROUP:New( self.EscortUnit:GetGroup(), "Resume mission from", self.EscortMenu ) + self.EscortMenuResumeMission = MENU_GROUP:New( self.PlayerGroup, "Resume mission from", self.EscortMenu ) end return self @@ -1410,7 +1340,7 @@ end -- @param Wrapper.Group#GROUP EscortGroup function AI_ESCORT.___Resume( EscortGroup, self ) - local PlayerGroup = self.EscortUnit:GetGroup() + local PlayerGroup = self.PlayerGroup if EscortGroup.EscortMode == AI_ESCORT.MODE.FOLLOW then self:JoinFormation( EscortGroup ) @@ -1535,7 +1465,7 @@ end function AI_ESCORT:_ROE( EscortGroup, EscortROEFunction, EscortROEMessage ) pcall( function() EscortROEFunction() end ) - EscortGroup:MessageTypeToGroup( EscortROEMessage, MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) + EscortGroup:MessageTypeToGroup( EscortROEMessage, MESSAGE.Type.Information, self.PlayerGroup ) end function AI_ESCORT:_FlightROE( EscortROEFunction, EscortROEMessage ) @@ -1550,7 +1480,7 @@ end function AI_ESCORT:_ROT( EscortGroup, EscortROTFunction, EscortROTMessage ) pcall( function() EscortROTFunction() end ) - EscortGroup:MessageTypeToGroup( EscortROTMessage, MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) + EscortGroup:MessageTypeToGroup( EscortROTMessage, MESSAGE.Type.Information, self.PlayerGroup ) end @@ -1625,6 +1555,10 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) local TimeUpdate = timer.getTime() + local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortMenu ) + for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do local DetectedItemReportSummary = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) @@ -1632,9 +1566,10 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) local DetectedMenu = DetectedItemReportSummary:Text("\n") if EscortGroup:IsAir() then - MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), + + MENU_GROUP_COMMAND:New( self.PlayerGroup, DetectedMenu, - EscortGroup.EscortMenuAttackNearbyTargets, + EscortMenuAttackTargets, AI_ESCORT._AttackTarget, self, EscortGroup, @@ -1642,8 +1577,8 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) ):SetTag( "Escort" ):SetTime( TimeUpdate ) else if self.EscortMenuTargetAssistance then - local MenuTargetAssistance = MENU_GROUP:New( self.EscortUnit:GetGroup(), EscortGroupName, EscortGroup.EscortMenuTargetAssistance ) - MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), + local MenuTargetAssistance = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, EscortGroup.EscortMenuTargetAssistance ) + MENU_GROUP_COMMAND:New( self.PlayerGroup, DetectedMenu, MenuTargetAssistance, AI_ESCORT._AssistTarget, @@ -1657,7 +1592,7 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) end - EscortGroup.EscortMenuAttackNearbyTargets:RemoveSubMenus( TimeUpdate, "Esort" ) + EscortMenuAttackTargets:RemoveSubMenus( TimeUpdate, "Esort" ) return true else @@ -1678,9 +1613,9 @@ function AI_ESCORT:_FlightReportTargetsScheduler() local DetectedTargetsReport = REPORT:New( "Reporting detected targets:\n" ) -- A new report to display the detected targets as a message to the player. - if self.EscortUnit:IsAlive() and EscortGroup:IsAlive() then + if EscortGroup and ( self.EscortUnit:IsAlive() and EscortGroup:IsAlive() ) then - local ClientGroup = self.EscortUnit:GetGroup() + local ClientGroup = self.PlayerGroup local TimeUpdate = timer.getTime() @@ -1691,6 +1626,8 @@ function AI_ESCORT:_FlightReportTargetsScheduler() local ClientEscortTargets = self.Detection + local FlightMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", self.FlightMenu ) + for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do self:F("FlightReportTargetScheduler Targets") @@ -1701,9 +1638,9 @@ function AI_ESCORT:_FlightReportTargetsScheduler() local DetectedItemReportMenu = self.Detection:DetectedItemReportMenu( DetectedItem, ClientGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) local ReportMenuText = DetectedItemReportMenu:Text(", ") - MENU_GROUP_COMMAND:New( self.EscortUnit:GetGroup(), + MENU_GROUP_COMMAND:New( self.PlayerGroup, ReportMenuText, - self.FlightMenuAttackNearbyTargets, + FlightMenuAttackTargets, AI_ESCORT._FlightAttackTarget, self, DetectedItem @@ -1714,12 +1651,12 @@ function AI_ESCORT:_FlightReportTargetsScheduler() DetectedTargetsReport:AddIndent( ReportSummary, "-" ) end - self.FlightMenuAttackNearbyTargets:RemoveSubMenus( TimeUpdate, "Flight" ) + FlightMenuAttackTargets:RemoveSubMenus( TimeUpdate, "Flight" ) if DetectedTargets then - EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) + EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.PlayerGroup ) -- else --- EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.EscortUnit:GetGroup() ) +-- EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.PlayerGroup ) end return true From 78a5e89928285b2465e273e449cb728bb5d67731 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 17 May 2019 21:12:02 +0200 Subject: [PATCH 272/485] Synth --- Moose Development/Moose/AI/AI_Escort.lua | 72 ++++++++++++------------ 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 2a78343c6..8d94b39ae 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -409,33 +409,6 @@ function AI_ESCORT:MenuFormation( Formation, ... ) end ---- Defines --- Defines a menu slot to let the escort to join formation. --- This menu will appear under **Formation**. --- @param #AI_ESCORT self --- @return #AI_ESCORT -function AI_ESCORT:MenuJoinUp() - - local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) - local FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) - - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if EscortGroup:IsAir() then - - local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) - local EscortMenuJoinUp = MENU_GROUP:New( self.PlayerGroup, "Join Up", EscortMenuReportNavigation ) - - end - end - ) - - return self -end - - --- Defines a menu slot to let the escort to join in a trail formation. -- This menu will appear under **Formation**. -- @param #AI_ESCORT self @@ -587,6 +560,33 @@ function AI_ESCORT:MenuFormationBox( XStart, XSpace, YStart, YSpace, ZStart, ZSp end +--- Defines --- Defines a menu slot to let the escort to join formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuJoinUp() + + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) + + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", EscortMenuReportNavigation, AI_ESCORT._JoinUp, self ) + + end + end + ) + + return self +end + + --- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. -- This menu will appear under **Hold position**. -- @param #AI_ESCORT self @@ -620,11 +620,13 @@ function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) end end + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuHoldPosition = MENU_GROUP_COMMAND :New( self.PlayerGroup, MenuText, - self.FlightMenu, + FlightMenuReportNavigation, AI_ESCORT._FlightHoldPosition, self, nil, @@ -640,13 +642,12 @@ function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) local EscortGroupName = EscortGroup:GetName() local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) - local EscortMenuHold = MENU_GROUP:New( self.PlayerGroup, MenuText, EscortMenuReportNavigation ) local EscortMenuHoldPosition = MENU_GROUP_COMMAND :New( self.PlayerGroup, - EscortGroupName, - EscortMenuHold, + MenuText, + EscortMenuReportNavigation, AI_ESCORT._HoldPosition, self, EscortGroup, @@ -695,11 +696,13 @@ function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) end end + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND :New( self.PlayerGroup, MenuText, - self.FlightMenu, + FlightMenuReportNavigation, AI_ESCORT._FlightHoldPosition, self, self.PlayerGroup, @@ -715,13 +718,12 @@ function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) local EscortGroupName = EscortGroup:GetName() local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) - local EscortMenuHold = MENU_GROUP:New( self.PlayerGroup, MenuText, EscortMenuReportNavigation ) local EscortMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND :New( self.PlayerGroup, - EscortGroupName, - EscortMenuHold, + MenuText, + EscortMenuReportNavigation, AI_ESCORT._HoldPosition, self, self.PlayerGroup, From 6be56b1b8683382db823c22ec36b2f20643ca3cb Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 23 May 2019 16:33:47 +0300 Subject: [PATCH 273/485] Progress --- Moose Development/Moose/AI/AI_Escort.lua | 80 ++++++++----------- .../Moose/AI/AI_Escort_Request.lua | 17 ++-- Moose Development/Moose/AI/AI_Formation.lua | 74 +++++++++++++---- 3 files changed, 98 insertions(+), 73 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 8d94b39ae..6a7517dd9 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -176,10 +176,6 @@ AI_ESCORT = { EscortUnit = nil, EscortGroup = nil, EscortMode = 1, - MODE = { - FOLLOW = 1, - MISSION = 2, - }, Targets = {}, -- The identified targets FollowScheduler = nil, ReportTargets = true, @@ -192,11 +188,6 @@ AI_ESCORT = { --- @field Functional.Detection#DETECTION_AREAS AI_ESCORT.Detection = nil ---- AI_ESCORT.Mode class --- @type AI_ESCORT.MODE --- @field #number FOLLOW --- @field #number MISSION - --- MENUPARAM type -- @type MENUPARAM -- @field #AI_ESCORT ParamSelf @@ -225,8 +216,6 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) local self = BASE:Inherit( self, AI_FORMATION:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT self:F( { EscortUnit, EscortGroupSet } ) - self.EscortUnit = self.FollowUnit -- Wrapper.Unit#UNIT - self.PlayerUnit = self.FollowUnit -- Wrapper.Unit#UNIT self.PlayerGroup = self.FollowUnit:GetGroup() -- Wrapper.Group#GROUP @@ -261,18 +250,16 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) -- Set EscortGroup known at EscortUnit. - if not self.EscortUnit._EscortGroups then - self.EscortUnit._EscortGroups = {} + if not self.PlayerUnit._EscortGroups then + self.PlayerUnit._EscortGroups = {} end - if not self.EscortUnit._EscortGroups[EscortGroup:GetName()] then - self.EscortUnit._EscortGroups[EscortGroup:GetName()] = {} - self.EscortUnit._EscortGroups[EscortGroup:GetName()].EscortGroup = EscortGroup - self.EscortUnit._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName - self.EscortUnit._EscortGroups[EscortGroup:GetName()].Detection = self.Detection + if not self.PlayerUnit._EscortGroups[EscortGroup:GetName()] then + self.PlayerUnit._EscortGroups[EscortGroup:GetName()] = {} + self.PlayerUnit._EscortGroups[EscortGroup:GetName()].EscortGroup = EscortGroup + self.PlayerUnit._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName + self.PlayerUnit._EscortGroups[EscortGroup:GetName()].Detection = self.Detection end - - EscortGroup.EscortMode = AI_ESCORT.MODE.FOLLOW end ) @@ -297,11 +284,9 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) local LeaderEscort = EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP local Report = REPORT:New( "Escort reporting:" ) - Report:Add( "Current coordinate: " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) - Report:Add( "Configuration: " .. EscortGroupSet:GetUnitTypeNames():Text( ", " ) ) - Report:Add( "Joining Up ..." ) + Report:Add( "Joining Up " .. EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.PlayerUnit ) ) - LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.EscortUnit ) + LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) self.Detection = DETECTION_AREAS:New( EscortGroupSet, 5000 ) @@ -317,7 +302,7 @@ function AI_ESCORT:SetDetection( Detection ) self.Detection = Detection self.EscortGroup.Detection = self.Detection - self.EscortUnit._EscortGroups[self.EscortGroup:GetName()].Detection = self.EscortGroup.Detection + self.PlayerUnit._EscortGroups[self.EscortGroup:GetName()].Detection = self.EscortGroup.Detection Detection:__Start( 1 ) @@ -935,7 +920,7 @@ function AI_ESCORT:MenuReportTargets( Seconds ) -- Attack Targets local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortMenu ) - --EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, timer, Seconds ) + EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, timer, Seconds ) timer=timer+1 end end @@ -1078,7 +1063,7 @@ end -- @param #number OrbitSeconds function AI_ESCORT:_HoldPosition( OrbitGroup, EscortGroup, OrbitHeight, OrbitSeconds ) - local EscortUnit = self.EscortUnit + local EscortUnit = self.PlayerUnit local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT @@ -1121,7 +1106,7 @@ end -- @param #number OrbitSeconds function AI_ESCORT:_FlightHoldPosition( OrbitGroup, OrbitHeight, OrbitSeconds ) - local EscortUnit = self.EscortUnit + local EscortUnit = self.PlayerUnit self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup @@ -1141,10 +1126,11 @@ end function AI_ESCORT:_JoinUp( EscortGroup ) - local EscortUnit = self.EscortUnit + local EscortUnit = self.PlayerUnit self:JoinFormation( EscortGroup ) - EscortGroup.EscortMode = AI_ESCORT.MODE.FOLLOW + + EscortGroup:SetState( self, "Mode", self.__Enum.Mode.Follow ) end @@ -1218,7 +1204,7 @@ end function AI_ESCORT:_Flare( EscortGroup, Color, Message ) - local EscortUnit = self.EscortUnit + local EscortUnit = self.PlayerUnit EscortGroup:GetUnit(1):Flare( Color ) EscortGroup:MessageTypeToGroup( Message, MESSAGE.Type.Information, EscortUnit:GetGroup() ) @@ -1242,7 +1228,7 @@ end function AI_ESCORT:_Smoke( EscortGroup, Color, Message ) - local EscortUnit = self.EscortUnit + local EscortUnit = self.PlayerUnit EscortGroup:GetUnit(1):Smoke( Color ) EscortGroup:MessageTypeToGroup( Message, MESSAGE.Type.Information, EscortUnit:GetGroup() ) @@ -1264,7 +1250,7 @@ end function AI_ESCORT:_ReportNearbyTargetsNow( EscortGroup ) - local EscortUnit = self.EscortUnit + local EscortUnit = self.PlayerUnit self:_ReportTargetsScheduler( EscortGroup ) @@ -1280,7 +1266,7 @@ end function AI_ESCORT:_SwitchReportNearbyTargets( EscortGroup, ReportTargets ) - local EscortUnit = self.EscortUnit + local EscortUnit = self.PlayerUnit self.ReportTargets = ReportTargets @@ -1312,7 +1298,7 @@ end function AI_ESCORT:_ScanTargets( ScanDuration ) local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP - local EscortUnit = self.EscortUnit + local EscortUnit = self.PlayerUnit self.FollowScheduler:Stop( self.FollowSchedule ) @@ -1344,10 +1330,8 @@ function AI_ESCORT.___Resume( EscortGroup, self ) local PlayerGroup = self.PlayerGroup - if EscortGroup.EscortMode == AI_ESCORT.MODE.FOLLOW then - self:JoinFormation( EscortGroup ) - EscortGroup:MessageTypeToClient( "Destroyed all targets. Rejoining.", MESSAGE.Type.Information, PlayerGroup ) - end + self:JoinFormation( EscortGroup ) + EscortGroup:MessageTypeToClient( "Destroyed all targets. Rejoining.", MESSAGE.Type.Information, PlayerGroup ) end @@ -1359,7 +1343,7 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) self:F( EscortGroup ) - local EscortUnit = self.EscortUnit + local EscortUnit = self.PlayerUnit self:ReleaseFormation( EscortGroup ) @@ -1383,7 +1367,7 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) ) Tasks[#Tasks+1] = EscortGroup:TaskCombo( AttackUnitTasks ) - Tasks[#Tasks+1] = EscortGroup:TaskFunction( "AI_ESCORT.___Resume", self, EscortGroup ) + Tasks[#Tasks+1] = EscortGroup:TaskFunction( "AI_ESCORT.___Resume", EscortGroup, self ) EscortGroup:SetTask( EscortGroup:TaskCombo( @@ -1439,7 +1423,7 @@ end -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem function AI_ESCORT:_AssistTarget( EscortGroup, DetectedItem ) - local EscortUnit = self.EscortUnit + local EscortUnit = self.PlayerUnit local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) @@ -1500,7 +1484,7 @@ end function AI_ESCORT:_ResumeMission( WayPoint ) local EscortGroup = self.EscortGroup - local EscortUnit = self.EscortUnit + local EscortUnit = self.PlayerUnit self.FollowScheduler:Stop( self.FollowSchedule ) @@ -1540,7 +1524,7 @@ end function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) self:F( EscortGroup:GetName() ) - if EscortGroup:IsAlive() and self.EscortUnit:IsAlive() then + if EscortGroup:IsAlive() and self.PlayerUnit:IsAlive() then if true then @@ -1563,7 +1547,7 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do - local DetectedItemReportSummary = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) + local DetectedItemReportSummary = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) local DetectedMenu = DetectedItemReportSummary:Text("\n") @@ -1615,7 +1599,7 @@ function AI_ESCORT:_FlightReportTargetsScheduler() local DetectedTargetsReport = REPORT:New( "Reporting detected targets:\n" ) -- A new report to display the detected targets as a message to the player. - if EscortGroup and ( self.EscortUnit:IsAlive() and EscortGroup:IsAlive() ) then + if EscortGroup and ( self.PlayerUnit:IsAlive() and EscortGroup:IsAlive() ) then local ClientGroup = self.PlayerGroup @@ -1637,7 +1621,7 @@ function AI_ESCORT:_FlightReportTargetsScheduler() DetectedTargets = true -- There are detected targets, when the content of the for loop is executed. We use it to display a message. - local DetectedItemReportMenu = self.Detection:DetectedItemReportMenu( DetectedItem, ClientGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) + local DetectedItemReportMenu = self.Detection:DetectedItemReportMenu( DetectedItem, ClientGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) local ReportMenuText = DetectedItemReportMenu:Text(", ") MENU_GROUP_COMMAND:New( self.PlayerGroup, @@ -1648,7 +1632,7 @@ function AI_ESCORT:_FlightReportTargetsScheduler() DetectedItem ):SetTag( "Flight" ):SetTime( TimeUpdate ) - local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, ClientGroup, _DATABASE:GetPlayerSettings( self.EscortUnit:GetPlayerName() ) ) + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, ClientGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) local ReportSummary = DetectedItemReportSummary:Text(", ") DetectedTargetsReport:AddIndent( ReportSummary, "-" ) end diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index 372f09d22..8d17d2657 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -212,7 +212,7 @@ function AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, EscortAirbase, EscortNa local self = BASE:Inherit( self, AI_ESCORT:New( EscortUnit, self.EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT_REQUEST - self.LeaderGroup = self.EscortUnit:GetGroup() + self.LeaderGroup = self.PlayerUnit:GetGroup() self.Detection = DETECTION_AREAS:New( self.EscortGroupSet, 5000 ) self.Detection:__Start( 30 ) @@ -235,15 +235,14 @@ function AI_ESCORT_REQUEST:SpawnEscort() function() local LeaderEscort = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP - local Report = REPORT:New( "Escorts Reporting." ) - Report:Add( "Current coordinate: " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) - Report:Add( "Configuration: " .. self.EscortGroupSet:GetUnitTypeNames():Text( ", " ) ) - Report:Add( "Joining Up ..." ) - - LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.EscortUnit ) + local Report = REPORT:New() + + Report:Add( "Joining Up " .. self.EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) + + LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) self:FormationTrail( 50, 50, 50 ) self:JoinFormation( EscortGroup ) - self:Menus() + self:Menus( self.XStart, self.XSpace, self.YStart, self.YSpace, self.ZStart, self.ZSpace, self.ZLevels ) end ) @@ -254,7 +253,7 @@ end function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) if not self.MenuRequestEscort then - self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request A2G Escort", self.FlightMenu, + self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request A2G Escort", self.MainMenu, function() self:SpawnEscort() end diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 5eae434e2..5eb5d0e90 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -110,10 +110,43 @@ AI_FORMATION = { dtFollow = 0.5, } ---- AI_FORMATION.Mode class --- @type AI_FORMATION.MODE --- @field #number FOLLOW --- @field #number MISSION +AI_FORMATION.__Enum = {} + +--- @type AI_FORMATION.__Enum.Formation +-- @field #number None +-- @field #number Line +-- @field #number Trail +-- @field #number Stack +-- @field #number LeftLine +-- @field #number RightLine +-- @field #number LeftWing +-- @field #number RightWing +-- @field #number Vic +-- @field #number Box +AI_FORMATION.__Enum.Formation = { + None = 0, + Mission = 1, + Line = 2, + Trail = 3, + Stack = 4, + LeftLine = 5, + RightLine = 6, + LeftWing = 7, + RightWing = 8, + Vic = 9, + Box = 10, +} + +--- @type AI_FORMATION.__Enum.Mode +-- @field #number Mission +-- @field #number Formation +AI_FORMATION.__Enum.Mode = { + Mission = 0, + Formation = 1, +} + + + --- MENUPARAM type -- @type MENUPARAM @@ -139,7 +172,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin self.FollowGroupSet:ForEachGroup( function( FollowGroup ) self:E("Following") - FollowGroup.Following = true + FollowGroup:SetState( self, "Mode", self.__Enum.Mode.Formation ) end ) @@ -663,8 +696,8 @@ end -- @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 #AI_FORMATION -function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) --R2.1 - self:F( { FollowGroupSet, From , Event ,To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace } ) +function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, Formation ) --R2.1 + self:F( { FollowGroupSet, From , Event ,To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, Formation } ) FollowGroupSet:Flush( self ) @@ -682,6 +715,9 @@ function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, X local Vec3 = PointVec3:GetVec3() FollowGroup:SetState( self, "FormationVec3", Vec3 ) i = i + 1 + + FollowGroup:SetState( FollowGroup, "Formation", Formation ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) end return self @@ -700,7 +736,7 @@ end -- @return #AI_FORMATION function AI_FORMATION:onafterFormationTrail( FollowGroupSet, From , Event , To, XStart, XSpace, YStart ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,0,0) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,0,0, self.__Enum.Formation.Trail ) return self end @@ -719,7 +755,7 @@ end -- @return #AI_FORMATION function AI_FORMATION:onafterFormationStack( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,0,0) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,0,0, self.__Enum.Formation.Stack ) return self end @@ -740,7 +776,7 @@ end -- @return #AI_FORMATION function AI_FORMATION:onafterFormationLeftLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,-ZStart,-ZSpace) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,-ZStart,-ZSpace, self.__Enum.Formation.LeftLine ) return self end @@ -759,7 +795,7 @@ end -- @return #AI_FORMATION function AI_FORMATION:onafterFormationRightLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,ZStart,ZSpace) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,ZStart,ZSpace,self.__Enum.Formation.RightLine) return self end @@ -778,7 +814,7 @@ end -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. function AI_FORMATION:onafterFormationLeftWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,-ZStart,-ZSpace) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,-ZStart,-ZSpace,self.__Enum.Formation.LeftWing) return self end @@ -798,7 +834,7 @@ end -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. function AI_FORMATION:onafterFormationRightWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,ZStart,ZSpace) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,ZStart,ZSpace,self.__Enum.Formation.RightWing) return self end @@ -836,6 +872,8 @@ function AI_FORMATION:onafterFormationCenterWing( FollowGroupSet, From , Event , local Vec3 = PointVec3:GetVec3() FollowGroup:SetState( self, "FormationVec3", Vec3 ) i = i + 1 + FollowGroup:SetState( FollowGroup, "Formation", self.__Enum.Formation.Vic ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) end return self @@ -895,6 +933,8 @@ function AI_FORMATION:onafterFormationBox( FollowGroupSet, From , Event , To, XS local Vec3 = PointVec3:GetVec3() FollowGroup:SetState( self, "FormationVec3", Vec3 ) i = i + 1 + FollowGroup:SetState( FollowGroup, "Formation", self.__Enum.Formation.Box ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) end return self @@ -919,7 +959,7 @@ end -- @return #AI_FORMATION function AI_FORMATION:ReleaseFormation( FollowGroup ) - FollowGroup.Following = false + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) return self end @@ -931,7 +971,9 @@ end -- @return #AI_FORMATION function AI_FORMATION:JoinFormation( FollowGroup ) - FollowGroup.Following = true + -- If a formation type was defined for the AI_FORMATION object, then we can joinup. + + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) return self end @@ -989,7 +1031,7 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 -- @param Wrapper.Unit#UNIT ClientUnit function( FollowGroup, Formation, ClientUnit, CT1, CV1, CT2, CV2 ) - if FollowGroup.Following == true then + if FollowGroup:GetState( FollowGroup, "Mode" ) == self.__Enum.Mode.Formation then FollowGroup:OptionROTEvadeFire() FollowGroup:OptionROEReturnFire() From 889ce8f64b2e6234a9acd0f0db4a4379a32db26f Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 23 May 2019 21:10:48 +0200 Subject: [PATCH 274/485] Fixed Database OnEventNewZone function --- Moose Development/Moose/Core/Base.lua | 22 ++++++++++++++++++++-- Moose Development/Moose/Core/Database.lua | 13 +------------ Moose Development/Moose/Core/Event.lua | 18 +++++++++++------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index a72fb0e1f..db2f5ddaf 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -595,18 +595,36 @@ do -- Event Handling -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns. - -- initiator : The unit that is doing the shooing. + -- initiator : The unit that is doing the shooting. -- target: The unit that is being targeted. -- @function [parent=#BASE] OnEventShootingStart -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event. - -- initiator : The unit that was doing the shooing. + -- initiator : The unit that was doing the shooting. -- @function [parent=#BASE] OnEventShootingEnd -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. + --- Occurs when a new mark was added. + -- MarkID: ID of the mark. + -- @function [parent=#BASE] OnEventMarkAdded + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mark was removed. + -- MarkID: ID of the mark. + -- @function [parent=#BASE] OnEventMarkRemoved + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mark text was changed. + -- MarkID: ID of the mark. + -- @function [parent=#BASE] OnEventMarkChange + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + end diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 5a07d5f5a..0e87d5f7c 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -304,16 +304,6 @@ do -- Zones self.ZONES[ZoneName] = nil end - - --- Finds an @{Zone} based on the zone name in the DATABASE. - -- @param #DATABASE self - -- @param #string ZoneName - -- @return Core.Zone#ZONE_BASE The found @{Zone}. - function DATABASE:FindZone( ZoneName ) - - local ZoneFound = self.ZONES[ZoneName] - return ZoneFound - end --- Private method that registers new ZONE_BASE derived objects within the DATABASE Object. @@ -348,7 +338,6 @@ do -- Zones end -- zone - do -- cargo --- Adds a Cargo based on the Cargo Name in the DATABASE. @@ -1166,7 +1155,7 @@ function DATABASE:OnEventNewZone( EventData ) self:F2( { EventData } ) if EventData.Zone then - self:AddZone( EventData.Zone ) + self:AddZone( EventData.Zone.ZoneName, EventData.Zone ) end end diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 386169cb0..3c00758eb 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -272,10 +272,16 @@ EVENTS = { -- @field Wrapper.Airbase#AIRBASE Place The MOOSE airbase object. -- @field #string PlaceName The name of the airbase. -- --- @field weapon The weapon used during the event. --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit +-- @field #table weapon The weapon used during the event. +-- @field #table Weapon +-- @field #string WeaponName Name of the weapon. +-- @field DCS#Unit WeaponTgtDCSUnit Target DCS unit of the weapon. +-- +-- @field Cargo.Cargo#CARGO Cargo The cargo object. +-- @field #string CargoName The name of the cargo object. +-- +-- @field Core.ZONE#ZONE Zone The zone object. +-- @field #string ZoneName The name of the zone. @@ -959,8 +965,7 @@ function EVENT:onEvent( Event ) Event.PlaceName=Event.Place:GetName() end --- @FC: something like this should be added. ---[[ + -- Mark points. if Event.idx then Event.MarkID=Event.idx Event.MarkVec3=Event.pos @@ -969,7 +974,6 @@ function EVENT:onEvent( Event ) Event.MarkCoalition=Event.coalition Event.MarkGroupID = Event.groupID end -]] if Event.cargo then Event.Cargo = Event.cargo From e1418235562c4fe44a1f00a86c1dfff2daccb909 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 26 May 2019 12:15:21 +0200 Subject: [PATCH 275/485] FOX Missile Trainer FOX v0.5.0 - Added new missile trainer. RANGE v2.1.1 - Fixed bugs. WAREHOUSE v0.9.0 - Added events AssetSpawned and AssetLowFuel - Improved Arrived function. --- Moose Development/Moose/Core/SpawnStatic.lua | 2 +- Moose Development/Moose/Functional/Fox.lua | 1591 +++++++++++++++++ Moose Development/Moose/Functional/Range.lua | 1046 +++++------ .../Moose/Functional/Warehouse.lua | 359 ++-- Moose Development/Moose/Modules.lua | 1 + Moose Development/Moose/Ops/Airboss.lua | 1 - Moose Development/Moose/Wrapper/Airbase.lua | 8 + Moose Development/Moose/Wrapper/Group.lua | 4 +- .../Moose/Wrapper/Positionable.lua | 21 +- Moose Setup/Moose.files | 1 + 10 files changed, 2398 insertions(+), 636 deletions(-) create mode 100644 Moose Development/Moose/Functional/Fox.lua diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index e5a7b4e59..66c3581d0 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -291,7 +291,7 @@ end -- @param #SPAWNSTATIC self -- @param Core.Zone#ZONE_BASE Zone The Zone where to spawn the static. -- @param #number Heading The heading of the static, which is a number in degrees from 0 to 360. --- @param #string (optional) The name of the new static. +-- @param #string NewName (optional) The name of the new static. -- @return #SPAWNSTATIC function SPAWNSTATIC:SpawnFromZone( Zone, Heading, NewName ) --R2.1 self:F( { Zone, Heading, NewName } ) diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua new file mode 100644 index 000000000..e6fb352f4 --- /dev/null +++ b/Moose Development/Moose/Functional/Fox.lua @@ -0,0 +1,1591 @@ +--- **Functional** - (R2.5) - Yet Another Missile Trainer. +-- +-- +-- Practice to evade missiles without being destroyed. +-- +-- +-- ## Main Features: +-- +-- * Handles air-to-air and surface-to-air missiles. +-- * Define your own training zones on the map. Players in this zone will be protected. +-- * Define launch zones. Only missiles launched in these zones are tracked. +-- * Define protected AI groups. +-- * F10 radio menu to adjust settings for each player. +-- * Alert on missile launch (optional). +-- * Marker of missile launch position (optional). +-- * Adaptive update of missile-to-player distance. +-- * Finite State Machine (FSM) implementation. +-- * Easy to use. See examples below. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Functional.FOX +-- @image Functional_FOX.png + + +--- FOX class. +-- @type FOX +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table menuadded Table of groups the menu was added for. +-- @field #boolean menudisabled If true, F10 menu for players is disabled. +-- @field #boolean destroy Default player setting for destroying missiles. +-- @field #boolean launchalert Default player setting for launch alerts. +-- @field #boolean marklaunch Default player setting for mark launch coordinates. +-- @field #table players Table of players. +-- @field #table missiles Table of tracked missiles. +-- @field #table safezones Table of practice zones. +-- @field #table launchzones Table of launch zones. +-- @field Core.Set#SET_GROUP protectedset Set of protected groups. +-- @field #number explosionpower Power of explostion when destroying the missile in kg TNT. Default 5 kg TNT. +-- @field #number explosiondist Missile player distance in meters for destroying the missile. Default 100 m. +-- @field #number dt50 Time step [sec] for missile position updates if distance to target > 50 km. Default 5 sec. +-- @field #number dt10 Time step [sec] for missile position updates if distance to target > 10 km and < 50 km. Default 1 sec. +-- @field #number dt05 Time step [sec] for missile position updates if distance to target > 5 km and < 10 km. Default 0.5 sec. +-- @field #number dt01 Time step [sec] for missile position updates if distance to target > 1 km and < 5 km. Default 0.1 sec. +-- @field #number dt00 Time step [sec] for missile position updates if distance to target < 1 km. Default 0.01 sec. +-- @field #boolean +-- @extends Core.Fsm#FSM + +--- Fox 3! +-- +-- === +-- +-- ![Banner Image](..\Presentations\FOX\FOX_Main.png) +-- +-- # The FOX Concept +-- +-- As you probably know [Fox](https://en.wikipedia.org/wiki/Fox_(code_word)) is a NATO brevity code for launching air-to-air munition. Therefore, the class name is not 100% accurate as this +-- script handles air-to-air but also surface-to-air missiles. +-- +-- # Basic Script +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Training Zones +-- +-- Players are only protected if they are inside one of the training zones. +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Add training zones. +-- fox:AddSafeZone(ZONE:New("Training Zone Alpha") +-- fox:AddSafeZone(ZONE:New("Training Zone Bravo") +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Launch Zones +-- +-- Missile launches are only monitored if the shooter is inside the defined launch zone. +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Add training zones. +-- fox:AddLaunchZone(ZONE:New("Launch Zone SA-10 Krim") +-- fox:AddLaunchZone(ZONE:New("Training Zone Bravo") +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Protected AI Groups +-- +-- Define AI protected groups. These groups cannot be harmed by missiles. +-- +-- ## Add Individual Groups +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Add single protected group(s). +-- fox:AddProtectedGroup(GROUP:FindByName("A-10 Protected")) +-- fox:AddProtectedGroup(GROUP:FindByName("Yak-40")) +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Fine Tuning +-- +-- Todo! +-- +-- # Special Events +-- +-- Todo! +-- +-- +-- @field #FOX +FOX = { + ClassName = "FOX", + Debug = false, + lid = nil, + menuadded = {}, + menudisabled = nil, + destroy = nil, + launchalert = nil, + marklaunch = nil, + missiles = {}, + players = {}, + safezones = {}, + launchzones = {}, + protectedset = nil, + explosionpower = 5, + explosiondist = 100, + destroy = nil, + dt50 = 5, + dt10 = 1, + dt05 = 0.5, + dt01 = 0.1, + dt00 = 0.01, +} + + +--- Player data table holding all important parameters of each player. +-- @type FOX.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string unitname Name of the unit. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string callsign Callsign of player. +-- @field Wrapper.Group#GROUP group Aircraft group of player. +-- @field #string groupname Name of the the player aircraft group. +-- @field #string name Player name. +-- @field #number coalition Coalition number of player. +-- @field #boolean destroy Destroy missile. +-- @field #boolean launchalert Alert player on detected missile launch. +-- @field #boolean marklaunch Mark position of launched missile on F10 map. +-- @field #number defeated Number of missiles defeated. +-- @field #number dead Number of missiles not defeated. +-- @field #boolean inzone Player is inside a protected zone. + +--- Missile data table. +-- @type FOX.MissileData +-- @field Wrapper.Unit#UNIT weapon Missile weapon unit. +-- @field #boolean active If true the missile is active. +-- @field #string missileType Type of missile. +-- @field #number missileRange Range of missile in meters. +-- @field Wrapper.Unit#UNIT shooterUnit Unit that shot the missile. +-- @field Wrapper.Group#GROUP shooterGroup Group that shot the missile. +-- @field #number shooterCoalition Coalition side of the shooter. +-- @field #string shooterName Name of the shooter unit. +-- @field #number shotTime Abs mission time in seconds the missile was fired. +-- @field Core.Point#COORDINATE shotCoord Coordinate where the missile was fired. +-- @field Wrapper.Unit#UNIT targetUnit Unit that was targeted. +-- @field #FOX.PlayerData targetPlayer Player that was targeted or nil. + +--- Main radio menu on group level. +-- @field #table MenuF10 Root menu table on group level. +FOX.MenuF10={} + +--- Main radio menu on mission level. +-- @field #table MenuF10Root Root menu on mission level. +FOX.MenuF10Root=nil + +--- FOX class version. +-- @field #string version +FOX.version="0.5.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO list: +-- DONE: safe zones +-- DONE: mark shooter on F10 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FOX class object. +-- @param #FOX self +-- @return #FOX self. +function FOX:New() + + self.lid="FOX | " + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #FOX + + -- Defaults: + self:SetDefaultMissileDestruction(true) + self:SetDefaultLaunchAlerts(true) + self:SetDefaultLaunchMarks(true) + self:SetExplosionPower() + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FOX script. + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "MissileLaunch", "*") -- Missile was launched. + self:AddTransition("*", "MissileDestroyed", "*") -- Missile was destroyed before impact. + self:AddTransition("*", "EnterSafeZone", "*") -- Player enters a safe zone. + self:AddTransition("*", "ExitSafeZone", "*") -- Player exists a safe zone. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the FOX. Initializes parameters and starts event handlers. + -- @function [parent=#FOX] Start + -- @param #FOX self + + --- Triggers the FSM event "Start" after a delay. Starts the FOX. Initializes parameters and starts event handlers. + -- @function [parent=#FOX] __Start + -- @param #FOX self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the FOX and all its event handlers. + -- @param #FOX self + + --- Triggers the FSM event "Stop" after a delay. Stops the FOX and all its event handlers. + -- @function [parent=#FOX] __Stop + -- @param #FOX self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#FOX] Status + -- @param #FOX self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#FOX] __Status + -- @param #FOX self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "MissileLaunch". + -- @function [parent=#FOX] MissileLaunch + -- @param #FOX self + -- @param #FOX.MissileData missile Data of the fired missile. + + --- Triggers the FSM delayed event "MissileLaunch". + -- @function [parent=#FOX] __MissileLaunch + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.MissileData missile Data of the fired missile. + + --- On after "MissileLaunch" event user function. Called when a missile was launched. + -- @function [parent=#FOX] OnAfterMissileLaunch + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.MissileData missile Data of the fired missile. + + --- Triggers the FSM event "MissileDestroyed". + -- @function [parent=#FOX] MissileDestroyed + -- @param #FOX self + -- @param #FOX.MissileData missile Data of the destroyed missile. + + --- Triggers the FSM delayed event "MissileDestroyed". + -- @function [parent=#FOX] __MissileDestroyed + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.MissileData missile Data of the destroyed missile. + + --- On after "MissileDestroyed" event user function. Called when a missile was destroyed. + -- @function [parent=#FOX] OnAfterMissileDestroyed + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.MissileData missile Data of the destroyed missile. + + + --- Triggers the FSM event "EnterSafeZone". + -- @function [parent=#FOX] EnterSafeZone + -- @param #FOX self + -- @param #FOX.PlayerData player Player data. + + --- Triggers the FSM delayed event "EnterSafeZone". + -- @function [parent=#FOX] __EnterSafeZone + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.PlayerData player Player data. + + --- On after "EnterSafeZone" event user function. Called when a player enters a safe zone. + -- @function [parent=#FOX] OnAfterEnterSafeZone + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.PlayerData player Player data. + + + --- Triggers the FSM event "ExitSafeZone". + -- @function [parent=#FOX] ExitSafeZone + -- @param #FOX self + -- @param #FOX.PlayerData player Player data. + + --- Triggers the FSM delayed event "ExitSafeZone". + -- @function [parent=#FOX] __ExitSafeZone + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.PlayerData player Player data. + + --- On after "ExitSafeZone" event user function. Called when a player exists a safe zone. + -- @function [parent=#FOX] OnAfterExitSafeZone + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.PlayerData player Player data. + + + return self +end + +--- On after Start event. Starts the missile trainer and adds event handlers. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting FOX Missile Trainer %s", FOX.version) + env.info(text) + + -- Handle events: + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Shot) + + if self.Debug then + self:TraceClass(self.ClassName) + self:TraceLevel(2) + end + + self:__Status(-10) +end + +--- On after Stop event. Stops the missile trainer and unhandles events. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX:onafterStop(From, Event, To) + + -- Short info. + local text=string.format("Stopping FOX Missile Trainer %s", FOX.version) + env.info(text) + + -- Handle events: + self:UnhandleEvent(EVENTS.Birth) + self:UnhandleEvent(EVENTS.Shot) + +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a training zone. Players in the zone are safe. +-- @param #FOX self +-- @param Core.Zone#ZONE zone Training zone. +-- @return #FOX self +function FOX:AddSafeZone(zone) + + table.insert(self.safezones, zone) + + return self +end + +--- Add a launch zone. Only missiles launched within these zones will be tracked. +-- @param #FOX self +-- @param Core.Zone#ZONE zone Training zone. +-- @return #FOX self +function FOX:AddLaunchZone(zone) + + table.insert(self.launchzones, zone) + + return self +end + +--- Add a protected set of groups. +-- @param #FOX self +-- @param Core.Set#SET_GROUP groupset The set of groups. +-- @return #FOX self +function FOX:SetProtectedGroupSet(groupset) + self.protectedset=groupset + return self +end + +--- Add a group to the protected set. +-- @param #FOX self +-- @param Wrapper.Group#GROUP group Protected group. +-- @return #FOX self +function FOX:AddProtectedGroup(group) + + if not self.protectedset then + self.protectedset=SET_GROUP:New() + end + + self.protectedset:AddGroup(group) + + return self +end + +--- Set explosion power. +-- @param #FOX self +-- @param #number power Explosion power in kg TNT. Default 5. +-- @return #FOX self +function FOX:SetExplosionPower(power) + + self.explosionpower=power or 5 + + return self +end + + +--- Disable F10 menu for all players. +-- @param #FOX self +-- @param #boolean switch If true debug mode on. If false/nil debug mode off +-- @return #FOX self +function FOX:SetDisableF10Menu() + + self.menudisabled=true + + return self +end + +--- Set default player setting for missile destruction. +-- @param #FOX self +-- @param #boolean switch If true missiles are destroyed. If false/nil missiles are not destroyed. +-- @return #FOX self +function FOX:SetDefaultMissileDestruction(switch) + + if switch==nil then + self.destroy=false + else + self.destroy=switch + end + + return self +end + +--- Set default player setting for launch alerts. +-- @param #FOX self +-- @param #boolean switch If true launch alerts to players are active. If false/nil no launch alerts are given. +-- @return #FOX self +function FOX:SetDefaultLaunchAlerts(switch) + + if switch==nil then + self.launchalert=false + else + self.launchalert=switch + end + + return self +end + +--- Set default player setting for marking missile launch coordinates +-- @param #FOX self +-- @param #boolean switch If true missile launches are marked. If false/nil marks are disabled. +-- @return #FOX self +function FOX:SetDefaultLaunchMarks(switch) + + if switch==nil then + self.marklaunch=false + else + self.marklaunch=switch + end + + return self +end + + +--- Set debug mode on/off. +-- @param #FOX self +-- @param #boolean switch If true debug mode on. If false/nil debug mode off. +-- @return #FOX self +function FOX:SetDebugOnOff(switch) + + if switch==nil then + self.Debug=false + else + self.Debug=switch + end + + return self +end + +--- Set debug mode on. +-- @param #FOX self +-- @return #FOX self +function FOX:SetDebugOn() + self:SetDebugOnOff(true) + return self +end + +--- Set debug mode off. +-- @param #FOX self +-- @return #FOX self +function FOX:SetDebugOff() + self:SetDebugOff(false) + return self +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check spawn queue and spawn aircraft if necessary. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX:onafterStatus(From, Event, To) + + -- Get FSM state. + local fsmstate=self:GetState() + + -- Status. + self:I(self.lid..string.format("Missile trainer status: %s", fsmstate)) + + -- Check missile status. + self:_CheckMissileStatus() + + -- Check player status. + self:_CheckPlayers() + + if fsmstate=="Running" then + self:__Status(-10) + end +end + +--- Check status of players. +-- @param #FOX self +function FOX:_CheckPlayers() + + for playername,_playersettings in pairs(self.players) do + local playersettings=_playersettings --#FOX.PlayerData + + local unitname=playersettings.unitname + local unit=UNIT:FindByName(unitname) + + if unit and unit:IsAlive() then + + local coord=unit:GetCoordinate() + + local issafe=self:_CheckCoordSafe(coord) + + + if issafe then + + ----------------------------- + -- Player INSIDE Safe Zone -- + ----------------------------- + + if not playersettings.inzone then + self:EnterSafeZone(playersettings) + playersettings.inzone=true + end + + else + + ------------------------------ + -- Player OUTSIDE Safe Zone -- + ------------------------------ + + if playersettings.inzone==true then + self:ExitSafeZone(playersettings) + playersettings.inzone=false + end + + end + end + end + +end + +--- Remove missile. +-- @param #FOX self +-- @param #FOX.MissileData missile Missile data. +function FOX:_RemoveMissile(missile) + + if missile then + for i,_missile in pairs(self.missiles) do + local m=_missile --#FOX.MissileData + if missile.missileName==m.missileName then + table.remove(self.missiles, i) + return + end + end + end + +end + +--- Missile status. +-- @param #FOX self +function FOX:_CheckMissileStatus() + + local text="Missiles:" + local inactive={} + for i,_missile in pairs(self.missiles) do + local missile=_missile --#FOX.MissileData + + local targetname="unkown" + if missile.targetUnit then + targetname=missile.targetUnit:GetName() + end + local playername="none" + if missile.targetPlayer then + playername=missile.targetPlayer.name + end + local active=tostring(missile.active) + local mtype=missile.missileType + local dtype=missile.missileType + local range=UTILS.MetersToNM(missile.missileRange) + + if not active then + table.insert(inactive,i) + end + local heading=self:_GetWeapongHeading(missile.weapon) + + text=text..string.format("\n[%d] %s: active=%s, range=%.1f NM, heading=%03d, target=%s, player=%s, missilename=%s", i, mtype, active, range, heading, targetname, playername, missile.missileName) + + end + self:I(self.lid..text) + + -- Remove inactive missiles. + for i=#self.missiles,1,-1 do + local missile=self.missiles[i] --#FOX.MissileData + if missile and not missile.active then + table.remove(self.missiles, i) + end + end + +end + +--- Check if missile target is protected. +-- @param #FOX self +-- @param Wrapper.Unit#UNIT targetunit Target unit. +-- @return #boolean If true, unit is protected. +function FOX:_IsProtected(targetunit) + + if not self.protectedset then + return false + end + + if targetunit and targetunit:IsAlive() then + + -- Get Group. + local targetgroup=targetunit:GetGroup() + + if targetgroup then + local targetname=targetgroup:GetName() + + for _,_group in pairs(self.protectedset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + if group then + local groupname=group:GetName() + + -- Target belongs to a protected set. + if targetname==groupname then + return true + end + end + + end + end + end + + return false +end + +--- Missle launch event. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FOX.MissileData missile Fired missile +function FOX:onafterMissileLaunch(From, Event, To, missile) + + -- Tracking info and init of last bomb position. + self:I(FOX.lid..string.format("FOX: Tracking %s - %s.", missile.missileType, missile.missileName)) + + -- Loop over players. + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + -- Player position. + local playerUnit=player.unit + + -- Check that player is alive and of the opposite coalition. + if playerUnit and playerUnit:IsAlive() and player.coalition~=missile.shooterCoalition then + + -- Player missile distance. + local distance=playerUnit:GetCoordinate():Get3DDistance(missile.shotCoord) + + -- Player bearing to missile. + local bearing=playerUnit:GetCoordinate():HeadingTo(missile.shotCoord) + + -- Alert that missile has been launched. + if player.launchalert then + + -- Alert directly targeted players or players that are within missile max range. + if (missile.targetPlayer and player.unitname==missile.targetPlayer.unitname) or (distance destroy missile if in safe zone. + if distance<=self.explosiondist and self:_CheckCoordSafe(targetCoord)then + + -- Destroy missile. + self:T(self.lid..string.format("Destroying missile at distance %.1f m", distance)) + _ordnance:destroy() + + -- Missile is not active any more. + missile.active=false + + -- Create event. + self:MissileDestroyed(missile) + + -- Little explosion for the visual effect. + if self.explosionpower>0 then + missileCoord:Explosion(self.explosionpower) + end + + local text=string.format("Destroying missile. %s", self:_DeadText()) + MESSAGE:New(text, 10):ToGroup(target:GetGroup()) + + -- Increase dead counter. + if missile.targetPlayer then + missile.targetPlayer.dead=missile.targetPlayer.dead+1 + end + + -- Terminate timer. + return nil + else + + -- Time step. + local dt=1.0 + if distance>50000 then + -- > 50 km + dt=self.dt50 --=5.0 + elseif distance>10000 then + -- 10-50 km + dt=self.dt10 --=1.0 + elseif distance>5000 then + -- 5-10 km + dt=self.dt05 --0.5 + elseif distance>1000 then + -- 1-5 km + dt=self.dt01 --0.1 + else + -- < 1 km + dt=self.dt00 --0.01 + end + + -- Check again in dt seconds. + return timer.getTime()+dt + end + else + + -- No target ==> terminate timer. + return nil + end + + else + + ------------------------------------- + -- Missile does not exist any more -- + ------------------------------------- + + if target then + + -- Get human player. + local player=self:_GetPlayerFromUnit(target) + + -- Check for player and distance < 10 km. + if player and player.unit:IsAlive() then -- and missileCoord and player.unit:GetCoordinate():Get3DDistance(missileCoord)<10*1000 then + local text=string.format("Missile defeated. Well done, %s!", player.name) + MESSAGE:New(text, 10):ToClient(player.client) + + -- Increase defeated counter. + player.defeated=player.defeated+1 + end + + end + + -- Missile is not active any more. + missile.active=false + + --Terminate the timer. + self:T(FOX.lid..string.format("Terminating missile track timer.")) + return nil + + end -- _status check + + end -- end function trackBomb + + -- Weapon is not yet "alife" just yet. Start timer with a little delay. + self:T(FOX.lid..string.format("Tracking of missile starts in 0.0001 seconds.")) + timer.scheduleFunction(trackMissile, missile.weapon, timer.getTime()+0.0001) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- FOX event handler for event birth. +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventBirth(EventData) + self:F3({eventbirth = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") + self:E(EventData) + return + end + + -- Player unit and name. + local _unitName=EventData.IniUnitName + local playerunit, playername=self:_GetPlayerUnitAndName(_unitName) + + -- Debug info. + self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T(self.lid.."BIRTH: player = "..tostring(playername)) + + -- Check if player entered. + if playerunit and playername then + + local _uid=playerunit:GetID() + local _group=playerunit:GetGroup() + local _callsign=playerunit:GetCallsign() + + -- Debug output. + 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) + + -- Add F10 radio menu for player. + if not self.menudisabled then + SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) + end + + -- Player data. + local playerData={} --#FOX.PlayerData + + -- Player unit, client and callsign. + playerData.unit = playerunit + playerData.unitname = _unitName + playerData.group = _group + playerData.groupname = _group:GetName() + playerData.name = playername + playerData.callsign = playerData.unit:GetCallsign() + playerData.client = CLIENT:FindByName(_unitName, nil, true) + playerData.coalition = _group:GetCoalition() + + playerData.destroy=playerData.destroy or self.destroy + playerData.launchalert=playerData.launchalert or self.launchalert + playerData.marklaunch=playerData.marklaunch or self.marklaunch + + playerData.defeated=playerData.defeated or 0 + playerData.dead=playerData.dead or 0 + + -- Init player data. + self.players[playername]=playerData + + end +end + +--- FOX event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventShot(EventData) + self:I({eventshot = EventData}) + + if EventData.Weapon==nil then + return + end + if EventData.IniDCSUnit==nil then + return + end + + -- Weapon data. + local _weapon = EventData.WeaponName + local _target = EventData.Weapon:getTarget() + local _targetName = "unknown" + local _targetUnit = nil --Wrapper.Unit#UNIT + + -- Weapon descriptor. + local desc=EventData.Weapon:getDesc() + self:E({desc=desc}) + + -- Weapon category: 0=Shell, 1=Missile, 2=Rocket, 3=BOMB + local weaponcategory=desc.category + + -- Missile category: 1=AAM, 2=SAM, 6=OTHER + local missilecategory=desc.missileCategory + + local missilerange=nil + if missilecategory then + missilerange=desc.rangeMaxAltMax + end + + -- Debug info. + self:E(FOX.lid.."EVENT SHOT: FOX") + self:E(FOX.lid..string.format("EVENT SHOT: Ini unit = %s", tostring(EventData.IniUnitName))) + self:E(FOX.lid..string.format("EVENT SHOT: Ini group = %s", tostring(EventData.IniGroupName))) + self:E(FOX.lid..string.format("EVENT SHOT: Weapon type = %s", tostring(_weapon))) + self:E(FOX.lid..string.format("EVENT SHOT: Weapon categ = %s", tostring(weaponcategory))) + self:E(FOX.lid..string.format("EVENT SHOT: Missil categ = %s", tostring(missilecategory))) + self:E(FOX.lid..string.format("EVENT SHOT: Missil range = %s", tostring(missilerange))) + + + -- Check if fired in launch zone. + if not self:_CheckCoordLaunch(EventData.IniUnit:GetCoordinate()) then + self:T(self.lid.."Missile was not fired in launch zone. No tracking!") + return + end + + -- Get the target unit. Note if if _target is not nil, the unit can sometimes not be found! + if _target then + self:E({target=_target}) + --_targetName=Unit.getName(_target) + --_targetUnit=UNIT:FindByName(_targetName) + _targetUnit=UNIT:Find(_target) + end + self:E(FOX.lid..string.format("EVENT SHOT: Target name = %s", tostring(_targetName))) + + -- Track missiles of type AAM=1, SAM=2 or OTHER=6 + local _track = weaponcategory==1 and missilecategory and (missilecategory==1 or missilecategory==2 or missilecategory==6) + + -- Only track missiles + if _track then + + local missile={} --#FOX.MissileData + + missile.active=true + missile.weapon=EventData.weapon + missile.missileType=_weapon + missile.missileRange=missilerange + missile.missileName=EventData.weapon:getName() + missile.shooterUnit=EventData.IniUnit + missile.shooterGroup=EventData.IniGroup + missile.shooterCoalition=EventData.IniUnit:GetCoalition() + missile.shooterName=EventData.IniUnitName + missile.shotTime=timer.getAbsTime() + missile.shotCoord=EventData.IniUnit:GetCoordinate() + missile.targetUnit=_targetUnit + missile.targetPlayer=self:_GetPlayerFromUnit(missile.targetUnit) + + -- Only track if target was a player or target is protected. + if missile.targetPlayer or self:_IsProtected(missile.targetUnit) then + + -- Add missile table. + table.insert(self.missiles, missile) + + -- Trigger MissileLaunch event. + self:__MissileLaunch(0.1, missile) + + end + + end --if _track + +end + + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MENU Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add menu commands for player. +-- @param #FOX self +-- @param #string _unitName Name of player unit. +function FOX:_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 + + -- Set menu root path. + local _rootPath=nil + if FOX.MenuF10Root then + ------------------------ + -- MISSON LEVEL MENUE -- + ------------------------ + + -- F10/FOX/... + _rootPath=FOX.MenuF10Root + + else + ------------------------ + -- GROUP LEVEL MENUES -- + ------------------------ + + -- Main F10 menu: F10/FOX/ + if FOX.MenuF10[gid]==nil then + FOX.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "FOX") + end + + -- F10/FOX/... + _rootPath=FOX.MenuF10[gid] + + end + + + -------------------------------- + -- F10/F FOX/F1 Help + -------------------------------- + --local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) + -- F10/FOX/F1 Help/ + --missionCommands.addCommandForGroup(gid, "Subtitles On/Off", _helpPath, self._SubtitlesOnOff, self, _unitName) -- F7 + --missionCommands.addCommandForGroup(gid, "Trapsheet On/Off", _helpPath, self._TrapsheetOnOff, self, _unitName) -- F8 + + ------------------------- + -- F10/F FOX/ + ------------------------- + + missionCommands.addCommandForGroup(gid, "Destroy Missiles On/Off", _rootPath, self._ToggleDestroyMissiles, self, _unitName) -- F1 + missionCommands.addCommandForGroup(gid, "Launch Alerts On/Off", _rootPath, self._ToggleLaunchAlert, self, _unitName) -- F2 + missionCommands.addCommandForGroup(gid, "Mark Launch On/Off", _rootPath, self._ToggleLaunchMark, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "My Status", _rootPath, self._MyStatus, self, _unitName) -- F4 + + end + else + self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) + end + else + self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName)) + end + +end + + +--- Turn player's launch alert on/off. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_MyStatus(_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] --#FOX.PlayerData + + if playerData then + + local m,mtext=self:_GetTargetMissiles(playerData.name) + + local text=string.format("Status of player %s:\n", playerData.name) + local safe=self:_CheckCoordSafe(playerData.unit:GetCoordinate()) + + text=text..string.format("Destroy missiles? %s\n", tostring(playerData.destroy)) + text=text..string.format("Launch alert? %s\n", tostring(playerData.launchalert)) + text=text..string.format("Launch marks? %s\n", tostring(playerData.marklaunch)) + text=text..string.format("Am I safe? %s\n", tostring(safe)) + text=text..string.format("Missiles defeated: %d\n", playerData.defeated) + text=text..string.format("Missiles destroyed: %d\n", playerData.dead) + text=text..string.format("Me target: %d\n%s", m, mtext) + + MESSAGE:New(text, 10, nil, true):ToClient(playerData.client) + + end + end +end + +--- Turn player's launch alert on/off. +-- @param #FOX self +-- @param #string playername Name of the player. +-- @return #number Number of missiles targeting the player. +-- @return #string Missile info. +function FOX:_GetTargetMissiles(playername) + + local text="" + local n=0 + for _,_missile in pairs(self.missiles) do + local missile=_missile --#FOX.MissileData + + if missile.targetPlayer and missile.targetPlayer.name==playername then + n=n+1 + text=text..string.format("Type %s: active %s\n", missile.missileType, tostring(missile.active)) + end + + end + + return n,text +end + +--- Turn player's launch alert on/off. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_ToggleLaunchAlert(_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] --#FOX.PlayerData + + if playerData then + + -- Invert state. + playerData.launchalert=not playerData.launchalert + + -- Inform player. + local text="" + if playerData.launchalert==true then + text=string.format("%s, missile launch alerts are now ENABLED.", playerData.name) + else + text=string.format("%s, missile launch alerts are now DISABLED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + +--- Turn player's launch marks on/off. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_ToggleLaunchMark(_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] --#FOX.PlayerData + + if playerData then + + -- Invert state. + playerData.marklaunch=not playerData.marklaunch + + -- Inform player. + local text="" + if playerData.marklaunch==true then + text=string.format("%s, missile launch marks are now ENABLED.", playerData.name) + else + text=string.format("%s, missile launch marks are now DISABLED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + + +--- Turn destruction of missiles on/off for player. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_ToggleDestroyMissiles(_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] --#FOX.PlayerData + + if playerData then + + -- Invert state. + playerData.destroy=not playerData.destroy + + -- Inform player. + local text="" + if playerData.destroy==true then + text=string.format("%s, incoming missiles will be DESTROYED.", playerData.name) + else + text=string.format("%s, incoming missiles will NOT be DESTROYED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get a random text message in case you die. +-- @param #FOX self +-- @return #string Text in case you die. +function FOX:_DeadText() + + local texts={} + texts[1]="You're dead!" + texts[2]="Meet your maker!" + texts[3]="Time to meet your maker!" + texts[4]="Well, I guess that was it!" + texts[5]="Bye, bye!" + texts[6]="Cheers buddy, was nice knowing you!" + + local r=math.random(#texts) + + return texts[r] +end + + +--- Check if a coordinate lies within a safe training zone. +-- @param #FOX self +-- @param Core.Point#COORDINATE coord Coordinate to check. +-- @return #boolean True if safe. +function FOX:_CheckCoordSafe(coord) + + -- No safe zones defined ==> Everything is safe. + if #self.safezones==0 then + return true + end + + -- Loop over all zones. + for _,_zone in pairs(self.safezones) do + local zone=_zone --Core.Zone#ZONE + local inzone=zone:IsCoordinateInZone(coord) + if inzone then + return true + end + end + + return false +end + +--- Check if a coordinate lies within a launch zone. +-- @param #FOX self +-- @param Core.Point#COORDINATE coord Coordinate to check. +-- @return #boolean True if in launch zone. +function FOX:_CheckCoordLaunch(coord) + + -- No safe zones defined ==> Everything is safe. + if #self.launchzones==0 then + return true + end + + -- Loop over all zones. + for _,_zone in pairs(self.launchzones) do + local zone=_zone --Core.Zone#ZONE + local inzone=zone:IsCoordinateInZone(coord) + if inzone then + return true + end + end + + return false +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX self +-- @param DCS#Weapon weapon The weapon. +-- @return #number Heading of weapon in degrees or -1. +function FOX:_GetWeapongHeading(weapon) + + if weapon and weapon:isExist() then + + local wp=weapon:getPosition() + + local wph = math.atan2(wp.x.z, wp.x.x) + + if wph < 0 then + wph=wph+2*math.pi + end + + wph=math.deg(wph) + + return wph + end + + return -1 +end + +--- Tell player notching headings. +-- @param #FOX self +-- @param #FOX.PlayerData playerData Player data. +-- @param DCS#Weapon weapon The weapon. +function FOX:_SayNotchingHeadings(playerData, weapon) + + if playerData and playerData.unit and playerData.unit:IsAlive() then + + local nr, nl=self:_GetNotchingHeadings(weapon) + + if nr and nl then + local text=string.format("Notching heading %03d° or %03d°", nr, nl) + MESSAGE:New(text, 5, "FOX"):ToClient(playerData.client) + end + + end + +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX self +-- @param DCS#Weapon weapon The weapon. +-- @return #number Notching heading right, i.e. missile heading +90� +-- @return #number Notching heading left, i.e. missile heading -90�. +function FOX:_GetNotchingHeadings(weapon) + + if weapon then + + local hdg=self:_GetWeapongHeading(weapon) + + local hdg1=hdg+90 + if hdg1>360 then + hdg1=hdg1-360 + end + + local hdg2=hdg-90 + if hdg2<0 then + hdg2=hdg2+360 + end + + return hdg1, hdg2 + end + + return nil, nil +end + +--- Returns the player data from a unit name. +-- @param #FOX self +-- @param #string unitName Name of the unit. +-- @return #FOX.PlayerData Player data. +function FOX:_GetPlayerFromUnitname(unitName) + + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + if player.unitname==unitName then + return player + end + end + + return nil +end + +--- Retruns the player data from a unit. +-- @param #FOX self +-- @param Wrapper.Unit#UNIT unit +-- @return #FOX.PlayerData Player data. +function FOX:_GetPlayerFromUnit(unit) + + if unit and unit:IsAlive() then + + -- Name of the unit + local unitname=unit:GetName() + + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + if player.unitname==unitname then + return player + end + 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 #FOX 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 FOX:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + -- Get player name if any. + local playername=DCSunit:getPlayerName() + + -- Unit object. + local unit=UNIT:Find(DCSunit) + + -- Debug. + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + + -- Check if enverything is there. + if DCSunit and unit and playername then + self:T(self.lid..string.format("Found DCS unit %s with player %s.", tostring(_unitName), tostring(playername))) + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 364a29660..eb3009ee4 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 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. +-- * 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 @@ -47,7 +47,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. @@ -64,11 +64,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. @@ -83,26 +83,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" 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. -- 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 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. --- +-- 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 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. --- --- * 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. @@ -110,107 +110,107 @@ -- wrong/opposite direction. -- * 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, -- 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": Temperature, wind and QFE pressure information is provided. --- +-- * "Weather Report": Temperature, 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", @@ -324,7 +324,7 @@ RANGE.TargetType={ -- @field #string quality Hit quality. -- @field #string player Player name. -- @field #string airframe Aircraft type of player. --- @field #number time Time via timer.getAbsTime() in seconds of impact. +-- @field #number time Time via timer.getAbsTime() in seconds of impact. --- Global list of all defined range names. -- @field #table Names @@ -340,7 +340,7 @@ RANGE.MenuF10Root=nil --- Range script version. -- @field #string version -RANGE.version="2.1.0" +RANGE.version="2.1.1" --TODO list: --TODO: Verbosity level for messages. @@ -367,22 +367,22 @@ function RANGE:New(rangename) -- Inherit BASE. local self=BASE:Inherit(self, FSM: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" - + -- Log id. - self.id=string.format("RANGE %s | ", self.rangename) - + self.id=string.format("RANGE %s | ", self.rangename) + -- Debug info. local text=string.format("Script version %s - creating new RANGE object %s.", RANGE.version, self.rangename) self:I(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) - + -- Defaults self:SetDefaultPlayerSmokeBomb() - + -- Start State. self:SetStartState("Stopped") @@ -393,10 +393,10 @@ function RANGE:New(rangename) self:AddTransition("*", "Status", "*") -- Status of RANGE script. self:AddTransition("*", "Impact", "*") -- Impact of bomb/rocket/missile. self:AddTransition("*", "EnterRange", "*") -- Player enters the range. - self:AddTransition("*", "ExitRange", "*") -- Player leaves the range. + self:AddTransition("*", "ExitRange", "*") -- Player leaves the range. self:AddTransition("*", "Save", "*") -- Save player results. self:AddTransition("*", "Load", "*") -- Load player results. - + ------------------------ --- Pseudo Functions --- ------------------------ @@ -486,7 +486,7 @@ function RANGE:New(rangename) -- @param #string Event Event. -- @param #string To To state. -- @param #RANGE.Playerdata player Data of player settings etc. - + -- Return object. return self end @@ -500,12 +500,12 @@ function RANGE:onafterStart() -- 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=self:_GetBombTargetCoordinate(_target) @@ -517,7 +517,7 @@ function RANGE:onafterStart() _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() @@ -525,28 +525,28 @@ function RANGE:onafterStart() 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) + local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.nstrafetargets, self.nbombtargets) self:E(self.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(self.id..text) MESSAGE:New(text,10):ToAllIf(self.Debug) - + -- Event handling. if self.eventmoose then -- Events are handled my MOOSE. @@ -559,21 +559,21 @@ function RANGE:onafterStart() self:T(self.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()) local _static=_target.type==RANGE.TargetType.STATIC - + 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() @@ -582,7 +582,7 @@ function RANGE:onafterStart() self:_SmokeStrafeTargetBoxes() self.rangezone:SmokeZone(SMOKECOLOR.White) end - + self:__Status(-60) end @@ -685,7 +685,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. -- @return #RANGE self @@ -846,44 +846,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(self.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(self.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(self.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 @@ -892,24 +892,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(self.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 @@ -922,37 +922,37 @@ 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() - + local st={} --#RANGE.StrafeTarget st.name=_name st.polygon=_polygon @@ -962,15 +962,15 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe st.foulline=foulline st.smokepoints=p st.heading=heading - + -- Add zone to table. table.insert(self.strafeTargets, st) - + -- 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(self.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - + return self end @@ -992,25 +992,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 return self @@ -1029,15 +1029,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(self.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange, false)) @@ -1049,9 +1049,9 @@ function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) else self:E(self.id..string.format("ERROR! Could not find bombing target %s.", name)) end - + end - + return self end @@ -1063,21 +1063,21 @@ end -- @return #RANGE self 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(self.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) @@ -1086,16 +1086,16 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) else self:E(self.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 - + local target={} --#RANGE.BombTarget target.name=name - target.target=target + target.target=unit target.goodhitrange=goodhitrange target.move=randommove target.speed=speed @@ -1105,15 +1105,15 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) else target.type=RANGE.TargetType.UNIT end - + -- Insert target to table. table.insert(self.bombingTargets, target) - + return self end ---- Add a coordinate of a bombing target. This +--- Add a coordinate of a bombing target. This -- @param #RANGE self -- @param Core.Point#COORDINATE coord The coordinate. -- @param #string name Name of target. @@ -1129,9 +1129,9 @@ function RANGE:AddBombingTargetCoordinate(coord, name, goodhitrange) target.speed=0 target.coordinate=coord target.type=RANGE.TargetType.COORD - + -- Insert target to table. - table.insert(self.bombingTargets, target) + table.insert(self.bombingTargets, target) return self end @@ -1144,18 +1144,18 @@ end -- @return #RANGE self 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 - + return self end @@ -1167,10 +1167,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 @@ -1180,7 +1180,7 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) else self:E(self.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 @@ -1190,7 +1190,7 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) else self:E(self.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 @@ -1229,26 +1229,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(self.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) self:T3(self.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) @@ -1256,22 +1256,22 @@ function RANGE:onEvent(Event) self:T3(self.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) self:T3(self.id..string.format("EVENT: Tgt unit = %s" , tostring(EventData.TgtUnitName))) self:T3(self.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 @@ -1280,32 +1280,32 @@ 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(self.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) self:T3(self.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) - self:T3(self.id.."BIRTH: player = "..tostring(_playername)) - + self:T3(self.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(self.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - + -- Reset current strafe status. self.strafeStatus[_uid] = nil - + -- Add Menu commands after a delay of 0.1 seconds. SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) - + -- By default, some bomb impact points and do not flare each hit on target. self.PlayerSettings[_playername]={} --#RANGE.PlayerData self.PlayerSettings[_playername].smokebombimpact=self.defaultsmokebomb @@ -1319,14 +1319,14 @@ function RANGE:OnEventBirth(EventData) self.PlayerSettings[_playername].playername=_playername self.PlayerSettings[_playername].airframe=EventData.IniUnit:GetTypeName() self.PlayerSettings[_playername].inzone=false - + -- 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. @@ -1334,7 +1334,7 @@ end -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventHit(EventData) self:F({eventhit = EventData}) - + -- Debug info. self:T3(self.id.."HIT: Ini unit = "..tostring(EventData.IniUnitName)) self:T3(self.id.."HIT: Ini group = "..tostring(EventData.IniGroupName)) @@ -1346,43 +1346,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) @@ -1390,92 +1390,92 @@ 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}) - + -- Nil checks. if EventData.Weapon==nil then return end if EventData.IniDCSUnit==nil then return - end - + end + -- Weapon data. local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName local _weaponStrArray = UTILS.Split(_weapon,"%.") local _weaponName = _weaponStrArray[#_weaponStrArray] - + -- Weapon descriptor. local desc=EventData.Weapon:getDesc() - + -- Weapon category: 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X) local weaponcategory=desc.category - + -- Debug info. self:T(self.id.."EVENT SHOT: Range "..self.rangename) self:T(self.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) self:T(self.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) self:T(self.id.."EVENT SHOT: Weapon type = ".._weapon) self:T(self.id.."EVENT SHOT: Weapon name = ".._weaponName) - self:T(self.id.."EVENT SHOT: Weapon cate = "..weaponcategory) - + self:T(self.id.."EVENT SHOT: Weapon cate = "..weaponcategory) + -- 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 = weaponcategory==Weapon.Category.BOMB --string.match(_weapon, "weapons.bombs") - local _rockets = weaponcategory==Weapon.Category.ROCKET --string.match(_weapon, "weapons.nurs") + local _bombs = weaponcategory==Weapon.Category.BOMB --string.match(_weapon, "weapons.bombs") + local _rockets = weaponcategory==Weapon.Category.ROCKET --string.match(_weapon, "weapons.nurs") local _missiles = weaponcategory==Weapon.Category.MISSILE --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(self.id..string.format("Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR/1000)) @@ -1483,16 +1483,16 @@ function RANGE:OnEventShot(EventData) -- Only track if distance player to range is < 25 km. Also check that a player shot. No need to track AI weapons. if _track and dPR<=self.BombtrackThreshold and _unit and _playername then - + -- Player data. local playerData=self.PlayerSettings[_playername] --#RANGE.PlayerData -- Tracking info and init of last bomb position. self:T(self.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) @@ -1504,43 +1504,43 @@ function RANGE:OnEventShot(EventData) self:T2(self.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status))) if _status then - + ---------------------------- -- Weapon is still in air -- ---------------------------- - + -- Remember this position. _lastBombPos = {x = _bombPos.x, y = _bombPos.y, z= _bombPos.z } -- Check again in ~0.005 seconds ==> 200 checks per second. return timer.getTime() + self.dtBombtrack - + else - + ----------------------------- -- Bomb did hit the ground -- ----------------------------- - + -- Get closet target to last position. local _closetTarget=nil --#RANGE.BombTarget local _distance=nil local _closeCoord=nil local _hitquality="POOR" - + -- Get callsign. local _callsign=self:_myname(_unitName) - + -- Coordinate of impact point. local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) - + -- Check if impact happened in range zone. local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) - + -- Impact point of bomb. if self.Debug then impactcoord:MarkToAll("Bomb impact point") end - + -- Smoke impact point of bomb. if playerData.smokebombimpact and insidezone then if playerData.delaysmoke then @@ -1549,18 +1549,18 @@ function RANGE:OnEventShot(EventData) impactcoord:Smoke(playerData.smokecolor) end end - + -- Loop over defined bombing targets. for _,_bombtarget in pairs(self.bombingTargets) do - -- Get target coordinate. + -- Get target coordinate. local targetcoord=self:_GetBombTargetCoordinate(_bombtarget) - + if targetcoord then - + -- Distance between bomb and target. local _temp = impactcoord:Get2DDistance(targetcoord) - + -- Find closest target to last known position of the bomb. if _distance == nil or _temp < _distance then _distance = _temp @@ -1575,7 +1575,7 @@ function RANGE:OnEventShot(EventData) else _hitquality = "POOR" end - + end end end @@ -1589,7 +1589,7 @@ function RANGE:OnEventShot(EventData) -- Local results. local _results=self.bombPlayerResults[_playername] - + local result={} --#RANGE.BombResult result.name=_closetTarget.name or "unknown" result.distance=_distance @@ -1599,37 +1599,37 @@ function RANGE:OnEventShot(EventData) result.player=playerData.playername result.time=timer.getAbsTime() result.airframe=playerData.airframe - + -- Add to table. table.insert(_results, result) - + -- Call impact. self:Impact(result, playerData) 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, false) - + else self:T(self.id.."Weapon impacted outside range zone.") end - + --Terminate the timer self:T(self.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(self.id..string.format("Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername)) timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime()+0.1) - + end --if _track (string.match) and player-range distance < threshold. - + end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1645,10 +1645,10 @@ function RANGE:onafterStatus(From, Event, To) -- Check range status. self:I(self.id..string.format("Range status: %s", self:GetState())) - + -- Check player status. self:_CheckPlayers() - + -- Save results. if self.autosafe then self:Save() @@ -1682,14 +1682,14 @@ function RANGE:onafterImpact(From, Event, To, result, player) text=text.."." end text=text..string.format(" %s hit.", result.quality) - + -- Unit. local unit=UNIT:FindByName(player.unitname) - + -- Send message. self:_DisplayMessageToGroup(unit, text, nil, true) self:T(self.id..text) - + end --- Function called before save event. Checks that io and lfs are desanitized. @@ -1723,7 +1723,7 @@ function RANGE:onafterSave(From, Event, To) self:E(self.id..string.format("ERROR: Could not save results to file %s", tostring(filename))) end end - + -- Path. local path=lfs.writedir() @@ -1732,10 +1732,10 @@ function RANGE:onafterSave(From, Event, To) -- Header line. local scores="Name,Pass,Target,Distance,Radial,Quality,Weapon,Airframe,Mission Time" - + -- Loop over all players. for playername,results in pairs(self.bombPlayerResults) do - + -- Loop over player grades table. for i,_result in pairs(results) do local result=_result --#RANGE.BombResult @@ -1749,7 +1749,7 @@ function RANGE:onafterSave(From, Event, To) scores=scores..string.format("\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time) end end - + _savefile(filename, scores) end @@ -1787,10 +1787,10 @@ function RANGE:onafterLoad(From, Event, To) return nil end end - + -- Path. - local path=lfs.writedir() - + local path=lfs.writedir() + -- Set file name. local filename=path..string.format("\\RANGE-%s_BombingResults.csv", self.rangename) @@ -1800,45 +1800,45 @@ function RANGE:onafterLoad(From, Event, To) -- Load asset data from file. local data=_loadfile(filename) - + if data then -- Split by line break. local results=UTILS.Split(data,"\n") - + -- Remove first header line. table.remove(results, 1) - + -- Init player scores table. self.bombPlayerResults={} - + -- Loop over all lines. for _,_result in pairs(results) do - + -- Parameters are separated by commata. local resultdata=UTILS.Split(_result, ",") - + -- Grade table local result={} --#RANGE.BombResult - + -- Player name. local playername=resultdata[1] result.player=playername - + -- Results data. result.name=tostring(resultdata[3]) - result.distance=tonumber(resultdata[4]) + result.distance=tonumber(resultdata[4]) result.radial=tonumber(resultdata[5]) result.quality=tostring(resultdata[6]) result.weapon=tostring(resultdata[7]) result.airframe=tostring(resultdata[8]) result.time=UTILS.ClockToSeconds(resultdata[9] or "00:00:00") - + -- Create player array if necessary. self.bombPlayerResults[playername]=self.bombPlayerResults[playername] or {} - + -- Add result to table. - table.insert(self.bombPlayerResults[playername], result) + table.insert(self.bombPlayerResults[playername], result) end end end @@ -1858,57 +1858,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, true) end end @@ -1918,52 +1918,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, true) end @@ -1975,50 +1975,50 @@ 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 = "" for i,_result in pairs(_results) do local result=_result --#RANGE.BombResult - + -- Message with name, weapon and distance. _message = _message.."\n"..string.format("[%d] %d m %03d° - %s - %s - %s hit", i, result.distance, result.radial, result.name, result.weapon, result.quality) - + -- Store best/first result. if _bestMsg == "" then _bestMsg = string.format("%d m %03d° - %s - %s - %s hit", result.distance, result.radial, result.name, result.weapon, result.quality) end - + -- Best 10 runs only. if i==self.ndisplayresult then break end - + end - + -- Message. _message = _message .."\n\nBEST: ".._bestMsg end - + -- Send message. self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end @@ -2029,22 +2029,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 @@ -2052,29 +2052,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, true) end @@ -2088,20 +2088,20 @@ 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 - - local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS - + + local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS + -- Direction vector from current position (coord) to target (position). local position=self.location --Core.Point#COORDINATE local bulls=position:ToStringBULLS(unit:GetCoalition(), settings) @@ -2110,10 +2110,10 @@ function RANGE:_DisplayRangeInfo(_unitname) 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)) @@ -2132,8 +2132,8 @@ function RANGE:_DisplayRangeInfo(_unitname) else textdelay=string.format("Smoke bomb delay: OFF") end - - -- Player unit settings. + + -- Player unit settings. local trange=string.format("%.1f km", range/1000) local trangealt=string.format("%d m", rangealt) local tstrafemaxalt=string.format("%d m", self.strafemaxalt) @@ -2142,7 +2142,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") @@ -2156,10 +2156,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, true) - + -- Debug output. self:T2(self.id..text) end @@ -2174,29 +2174,29 @@ 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 - + -- Coordinate of bombtarget. local coord=self:_GetBombTargetCoordinate(_bombtarget) - + if coord then - + local mycoord=coord:ToStringA2G(_unit, _settings) _text=_text..string.format("\n- %s: %s",_bombtarget.name or "unknown", mycoord) end end - + self:_DisplayMessageToGroup(_unit,_text, nil, true, true) end end @@ -2209,23 +2209,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 @@ -2236,7 +2236,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, true) end end @@ -2250,33 +2250,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) @@ -2284,10 +2284,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") @@ -2297,15 +2297,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, true) - + -- Debug output. self:T2(self.id..text) else self:T(self.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2318,34 +2318,34 @@ function RANGE:_CheckPlayers() for playername,_playersettings in pairs(self.PlayerSettings) do local playersettings=_playersettings --#RANGE.PlayerData - + local unitname=playersettings.unitname local unit=UNIT:FindByName(unitname) - + if unit and unit:IsAlive() then - + if unit:IsInZone(self.rangezone) then - + ------------------------------ -- Player INSIDE Range Zone -- ------------------------------ - + if not playersettings.inzone then self:EnterRange(playersettings) playersettings.inzone=true end - + else - + ------------------------------- -- Player OUTSIDE Range Zone -- - ------------------------------- - + ------------------------------- + if playersettings.inzone==true then self:ExitRange(playersettings) playersettings.inzone=false end - + end end end @@ -2370,56 +2370,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 still in zone. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) self:T2(self.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 @@ -2427,81 +2427,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(self.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 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2513,25 +2513,25 @@ 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 - - -- Range root menu path. + + -- Range root menu path. local _rangePath=nil if RANGE.MenuF10Root then @@ -2539,24 +2539,24 @@ function RANGE:_AddF10Commands(_unitName) ------------------- -- MISSION LEVEL -- ------------------- - + _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10Root) - + else - + ----------------- -- GROUP LEVEL -- ----------------- - - -- Main F10 menu: F10/On the Range// + + -- Main F10 menu: F10/On the Range// if RANGE.MenuF10[_gid] == nil then RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") - end + end _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid]) - - end - - + + end + + local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Statistics", _rangePath) local _markPath = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath) local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) @@ -2567,8 +2567,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/ @@ -2593,7 +2593,7 @@ function RANGE:_AddF10Commands(_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, "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) @@ -2608,7 +2608,7 @@ function RANGE:_AddF10Commands(_unitName) end end - + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Helper Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2620,9 +2620,9 @@ end function RANGE:_GetBombTargetCoordinate(target) local coord=nil --Core.Point#COORDINATE - + if target.type==RANGE.TargetType.UNIT then - + if not target.move then -- Target should not move. coord=target.coordinate @@ -2632,21 +2632,21 @@ function RANGE:_GetBombTargetCoordinate(target) coord=target.target:GetCoordinate() end end - + elseif target.type==RANGE.TargetType.STATIC then - -- Static targets dont move. + -- Static targets dont move. coord=target.coordinate - + elseif target.type==RANGE.TargetType.COORD then - + -- Coordinates dont move. coord=target.coordinate - + else self:E(self.id.."ERROR: Unknown target type.") end - + return coord end @@ -2657,35 +2657,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(self.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(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) @@ -2697,7 +2697,7 @@ function RANGE:_GetAmmo(unitname) end end end - + return ammo end @@ -2712,7 +2712,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 @@ -2725,7 +2725,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 @@ -2740,13 +2740,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. @@ -2765,16 +2765,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 @@ -2784,14 +2784,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) @@ -2805,10 +2805,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) @@ -2825,7 +2825,7 @@ end -- @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 or _clear==false then @@ -2833,36 +2833,36 @@ function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) else _clear=true end - + -- Messages globally disabled. if self.messages==false then return end - + -- Check if unit is alive. if _unit and _unit:IsAlive() then - + -- Group ID. local _gid=_unit:GetGroup():GetID() - + -- 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 trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) - end + end end end - + end --- Toggle status of smoking bomb impact points. @@ -2870,7 +2870,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 @@ -2883,7 +2883,7 @@ function RANGE:_SmokeBombImpactOnOff(unitname) end self:_DisplayMessageToGroup(unit, text, 5, false, true) end - + end --- Toggle status of time delay for smoking bomb impact points @@ -2891,7 +2891,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 @@ -2904,7 +2904,7 @@ function RANGE:_SmokeBombDelayOnOff(unitname) end self:_DisplayMessageToGroup(unit, text, 5, false, true) end - + end --- Toggle display messages to player. @@ -2912,7 +2912,7 @@ end -- @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 @@ -2924,7 +2924,7 @@ function RANGE:_MessagesToPlayerOnOff(unitname) self:_DisplayMessageToGroup(unit, text, 5, false, true) self.PlayerSettings[playername].messages=not self.PlayerSettings[playername].messages end - + end --- Toggle status of flaring direct hits of range targets. @@ -2932,7 +2932,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 @@ -2945,7 +2945,7 @@ function RANGE:_FlareDirectHitsOnOff(unitname) end self:_DisplayMessageToGroup(unit, text, 5, false, true) end - + end --- Mark bombing targets with smoke. @@ -2953,7 +2953,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 local coord=self:_GetBombTargetCoordinate(_bombtarget) @@ -2961,13 +2961,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. @@ -2975,17 +2975,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. @@ -2993,7 +2993,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) @@ -3001,13 +3001,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. @@ -3016,14 +3016,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. @@ -3032,14 +3032,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". @@ -3048,7 +3048,7 @@ end -- @return #string Color text. function RANGE:_smokecolor2text(color) self:F(color) - + local txt="" if color==SMOKECOLOR.Blue then txt="blue" @@ -3063,7 +3063,7 @@ function RANGE:_smokecolor2text(color) else txt=string.format("unknown color (%s)", tostring(color)) end - + return txt end @@ -3073,7 +3073,7 @@ end -- @return #string Color text. function RANGE:_flarecolor2text(color) self:F(color) - + local txt="" if color==FLARECOLOR.Green then txt="green" @@ -3086,7 +3086,7 @@ function RANGE:_flarecolor2text(color) else txt=string.format("unknown color (%s)", tostring(color)) end - + return txt end @@ -3099,23 +3099,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(self.id..string.format("Adding DCS static to MOOSE database. Name = %s.", name)) _DATABASE:AddStatic(name) end - + return true else self:T3(self.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 @@ -3136,18 +3136,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. @@ -3157,38 +3157,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 diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 84fbb7cd0..e2c8c7b6d 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -17,6 +17,13 @@ -- -- === -- +-- ## Youtube Videos: +-- +-- * [Warehouse Trailer](https://www.youtube.com/watch?v=e98jzLi5fGk) +-- * [DCS Warehouse Airbase Resources Proof Of Concept](https://www.youtube.com/watch?v=YeuGL0duEgY) +-- +-- === +-- -- ## Missions: -- -- === @@ -25,8 +32,6 @@ -- in order to meet requirements of a potential conflict. In particular, this class is concerned with maintaining army supply lines while disrupting those of the enemy, since an armed -- force without resources and transportation is defenseless. -- --- Please note that his class is work in progress and in an **alpha** stage. --- -- === -- -- ### Author: **funkyfranky** @@ -71,6 +76,7 @@ -- @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. +-- @field #number lowfuelthresh Low fuel threshold. Triggers the event AssetLowFuel if for any unit fuel goes below this number. -- @extends Core.Fsm#FSM --- Have your assets at the right place at the right time - or not! @@ -83,7 +89,7 @@ -- to another in a realistic and highly automatic fashion. In contrast to a "DCS warehouse" these assets have a physical representation in game. In particular, -- 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 +-- This comes along with some additional interesting strategic aspects since capturing/defending and destroying/protecting an enemy or your -- 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 @@ -109,12 +115,12 @@ -- -- * Ground vehicles will use the road infrastructure. So a good road connection for both warehouses is important but also off road connections can be added if necessary. -- * Airborne units get a flightplan from the airbase of the sending warehouse to the airbase of the receiving warehouse. This already implies that for airborne --- assets both warehouses need an airbase. If either one of the warehouses does not have an associated airbase, direct transportation of airborne assest is not possible. +-- assets both warehouses need an airbase. If either one of the warehouses does not have an associated airbase, direct transportation of airborne assets is not possible. -- * Naval units can be exchanged between warehouses which possess a port, which can be defined by the user. Also shipping lanes must be specified manually but the user since DCS does not provide these. -- * Trains (would) use the available railroad infrastructure and both warehouses must have a connection to the railroad. Unfortunately, however, trains are not yet implemented to -- a reasonable degree in DCS at the moment and hence cannot be used yet. -- --- Furthermore, ground assets can be transferred between warehouses by transport units. These are APCs, helicopters and airplanes. The transportation process is modelled +-- Furthermore, ground assets can be transferred between warehouses by transport units. These are APCs, helicopters and airplanes. The transportation process is modeled -- in a realistic way by using the corresponding cargo dispatcher classes, i.e. -- -- * @{AI.AI_Cargo_Dispatcher_APC#AI_DISPATCHER_APC} @@ -239,13 +245,13 @@ -- -- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, "China UN") -- --- would spawn the asset with a chinese UN livery. +-- would spawn the asset with a Chinese UN livery. -- -- Or -- -- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, {"China UN", "German"}) -- --- would spawn the asset with either a chinese UN or German livery. Mind the curly brackets **{}** when you want to specify multiple liveries. +-- would spawn the asset with either a Chinese UN or German livery. Mind the curly brackets **{}** when you want to specify multiple liveries. -- -- Four each unit type, the livery names can be found in the DCS root folder under Bazar\Liveries. You have to use the name of the livery subdirectory. The names of the liveries -- as displayed in the mission editor might be different and won't work in general. @@ -524,7 +530,7 @@ -- -- # Why is my request not processed? -- --- For each request, the warehouse class logic does a lot of consistancy and validation checks under the hood. +-- For each request, the warehouse class logic does a lot of consistency and validation checks under the hood. -- This helps to circumvent a lot of DCS issues and shortcomings. For example, it is checked that enough free -- parking spots at an airport are available *before* the assets are spawned. -- However, this also means that sometimes a request is deemed to be *invalid* in which case they are deleted @@ -535,7 +541,7 @@ -- Invalid request are requests which can **never** be processes because there is some logical or physical argument against it. -- (Or simply because that feature was not implemented (yet).) -- --- * All airborne assets need an associated airbase of any kind on the sending *and* receiving warhouse. +-- * All airborne assets need an associated airbase of any kind on the sending *and* receiving warehouse. -- * Airplanes need an airdrome at the sending and receiving warehouses. -- * Not enough parking spots of the right terminal type at the sending warehouse. This avoids planes spawning on runways or on top of each other. -- * No parking spots of the right terminal type at the receiving warehouse. This avoids DCS despawning planes on landing if they have no valid parking spot. @@ -552,7 +558,7 @@ -- -- ## Temporarily Unprocessable Requests -- --- Temporarily unprocessable requests are possible in priciple, but cannot be processed at the given time the warehouse checks its queue. +-- Temporarily unprocessable requests are possible in principle, but cannot be processed at the given time the warehouse checks its queue. -- -- * No enough parking spaces are available for all requested assets but the airbase has enough parking spots in total so that this request is possible once other aircraft have taken off. -- * The requesting warehouse is not in state "Running" (could be paused, not yet started or under attack). @@ -564,11 +570,11 @@ -- -- ## Cargo Bay and Weight Limitations -- --- The transporation of cargo is handled by the AI\_Dispatcher classes. These take the cargo bay of a carrier and the weight of +-- The transportation of cargo is handled by the AI\_Dispatcher classes. These take the cargo bay of a carrier and the weight of -- the cargo into account so that a carrier can only load a realistic amount of cargo. -- -- However, if troops are supposed to be transported between warehouses, there is one important limitations one has to keep in mind. --- This is that **cargo asset groups cannot be split** and devided into separate carrier units! +-- This is that **cargo asset groups cannot be split** and divided into separate carrier units! -- -- For example, a TPz Fuchs has a cargo bay large enough to carry up to 10 soldiers at once, which is a realistic number. -- If a group consisting of more than ten soldiers needs to be transported, it cannot be loaded into the APC. @@ -601,7 +607,7 @@ -- # Strategic Considerations -- -- Due to the fact that a warehouse holds (or can hold) a lot of valuable assets, it makes a (potentially) juicy target for enemy attacks. --- There are several interesting situations, which can occurr. +-- There are several interesting situations, which can occur. -- -- ## Capturing a Warehouses Airbase -- @@ -621,7 +627,7 @@ -- -- A warehouse can be captured by the enemy coalition. If enemy ground troops enter the warehouse zone the event **Attacked** is triggered which can be captured by the -- @{#WAREHOUSE.OnAfterAttacked} event. By default the warehouse zone circular zone with a radius of 500 meters located at the center of the physical warehouse. --- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a cirular zone. +-- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a circular zone. -- -- 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}() @@ -632,8 +638,8 @@ -- 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. -- --- The warehouse turns to the capturing coalition, i.e. its physical representation, and all assets as well. In paticular, all requests to the warehouse will --- spawn assets beloning to the new owner. +-- The warehouse turns to the capturing coalition, i.e. its physical representation, and all assets as well. In particular, all requests to the warehouse will +-- spawn assets belonging to the new owner. -- -- If the enemy troops could be defeated, i.e. no more troops of the opposite coalition are in the warehouse zone, the event **Defeated** is triggered and -- the @{#WAREHOUSE.OnAfterDefeated} function can be used to adapt to the new situation. For example putting back all spawned defender troops back into @@ -641,11 +647,11 @@ -- -- ## Destroying a Warehouse -- --- If an enemy destroy the physical warehouse structure, the warehouse will of course stop all its services. In priciple, all assets contained in the warehouse are +-- If an enemy destroy the physical warehouse structure, the warehouse will of course stop all its services. In principle, all assets contained in the warehouse are -- gone as well. So a warehouse should be properly defended. -- -- Upon destruction of the warehouse, the event **Destroyed** is triggered, which can be captured by the @{#WAREHOUSE.OnAfterDestroyed} function. --- So the mission designer can intervene at this point and for example choose to spawn all or paricular types of assets before the warehouse is gone for good. +-- So the mission designer can intervene at this point and for example choose to spawn all or particular types of assets before the warehouse is gone for good. -- -- === -- @@ -670,7 +676,9 @@ -- * "Attacked" --> "Captured" --> "Running" (warehouse was captured by the enemy) -- * "*" --> "AirbaseCaptured" --> "*" (airbase belonging to the warehouse was captured by the enemy) -- * "*" --> "AirbaseRecaptured" --> "*" (airbase was re-captured) --- * "*" --> "AssetDead" --> "*" (a whole asset group is dead) +-- * "*" --> "AssetSpawned" --> "*" (an asset has been spawned into the world) +-- * "*" --> "AssetLowFuel" --> "*" (an asset is running low on fuel) +-- * "*" --> "AssetDead" --> "*" (a whole asset, i.e. all its units/groups, is dead) -- * "*" --> "Destroyed" --> "Destroyed" (warehouse was destroyed) -- * "Running" --> "Pause" --> "Paused" (warehouse is paused) -- * "Paused" --> "Unpause" --> "Running" (warehouse is unpaused) @@ -822,7 +830,7 @@ -- ## Example 2: Self propelled Ground Troops -- -- Warehouse Berlin, which is a FARP near Batumi, requests infantry and troop transports from the warehouse at Batumi. --- The groups are spawned at Batumi and move by themselfs from Batumi to Berlin using the roads. +-- The groups are spawned at Batumi and move by themselves from Batumi to Berlin using the roads. -- Once the troops have arrived at Berlin, the troops are automatically added to the warehouse stock of Berlin. -- While on the road, Batumi has requested back two APCs from Berlin. Since Berlin does not have the assets in stock, -- the request is queued. After the troops have arrived, Berlin is sending back the APCs to Batumi. @@ -943,7 +951,7 @@ -- If the red coalition manages to capture our warehouse, all assets go into their possession. Now red tries to steal three F/A-18 flights and send them to -- Sukhumi. These aircraft will be spawned and begin to taxi. However, ... -- --- A blue Bradley is in the area and will attemt to recapture the warehouse. It might also catch the red F/A-18s before they take off. +-- A blue Bradley is in the area and will attempt to recapture the warehouse. It might also catch the red F/A-18s before they take off. -- -- -- Start warehouses. -- warehouse.Senaki:Start() @@ -1110,7 +1118,7 @@ -- After 30 and 45 seconds requests for five groups of armed speedboats are made. These will be spawned in the port zone right behind the carrier. -- The first five groups will go port of the carrier an form a left wing formation. The seconds groups will to the analogue on the starboard side. -- **Note** that in order to spawn naval assets a warehouse needs a port (zone). Since the carrier and hence the warehouse is mobile, we define a moving --- zone as @{Core.Zone#ZONE_UNIT} with the carrier as reference unit. The "port" of the Stennis at its stern so all naval assets are spawned behing the carrier. +-- zone as @{Core.Zone#ZONE_UNIT} with the carrier as reference unit. The "port" of the Stennis at its stern so all naval assets are spawned behind the carrier. -- -- -- Start warehouse on USS Stennis. -- warehouse.Stennis:Start() @@ -1317,7 +1325,7 @@ -- -- ## Example 14: Strategic Bombing -- --- This example shows how to employ stategic bombers in a mission. Three B-52s are lauched at Kobuleti with the assignment to wipe out the enemy warehouse at Sukhumi. +-- This example shows how to employ strategic bombers in a mission. Three B-52s are launched at Kobuleti with the assignment to wipe out the enemy warehouse at Sukhumi. -- The bombers will get a flight path and make their approach from the South at an altitude of 5000 m ASL. After their bombing run, they will return to Kobuleti and -- added back to stock. -- @@ -1560,6 +1568,7 @@ WAREHOUSE = { autosavefile = nil, saveparking = false, isunit = false, + lowfuelthresh = 0.15, } --- Item of the warehouse stock table. @@ -1582,7 +1591,7 @@ WAREHOUSE = { -- @field #number loadradius Distance when cargo is loaded into the carrier. -- @field DCS#AI.Skill skill Skill of AI unit. -- @field #string livery Livery of the asset. --- @field #string assignment Assignment of the asset. This could, e.g., be used in the @{#WAREHOUSE.OnAfterNewAsset) funktion. +-- @field #string assignment Assignment of the asset. This could, e.g., be used in the @{#WAREHOUSE.OnAfterNewAsset) function. --- Item of the warehouse queue table. -- @type WAREHOUSE.Queueitem @@ -1616,6 +1625,7 @@ WAREHOUSE = { -- @field Core.Set#SET_CARGO transportcargoset Set of cargo objects. -- @field #table carriercargo Table holding the cargo groups of each carrier unit. -- @field #number ntransporthome Number of transports back home. +-- @field #boolean lowfuel If true, at least one asset group is low on fuel. -- @extends #WAREHOUSE.Queueitem --- Descriptors enumerator describing the type of the asset. @@ -1733,7 +1743,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.7.0" +WAREHOUSE.version="0.9.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1747,7 +1757,7 @@ WAREHOUSE.version="0.7.0" -- 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! +-- DONE: Add save/load capability of warehouse <==> persistance 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. @@ -1876,6 +1886,8 @@ function WAREHOUSE:New(warehouse, alias) self:AddTransition("Running", "Request", "*") -- Process a request. Only in running mode. self:AddTransition("Attacked", "Request", "*") -- Process a request. Only in running mode. self:AddTransition("*", "Unloaded", "*") -- Cargo has been unloaded from the carrier (unused ==> unnecessary?). + self:AddTransition("*", "AssetSpawned", "*") -- Asset has been spawned into the world. + self:AddTransition("*", "AssetLowFuel", "*") -- Asset is low on fuel. self:AddTransition("*", "Arrived", "*") -- Cargo or transport group has arrived. self:AddTransition("*", "Delivered", "*") -- All cargo groups of a request have been delivered to the requesting warehouse. self:AddTransition("Running", "SelfRequest", "*") -- Request to warehouse itself. Requested assets are only spawned but not delivered anywhere. @@ -2301,6 +2313,52 @@ function WAREHOUSE:New(warehouse, alias) -- @param #string To To state. + --- Triggers the FSM event "AssetSpawned" when the warehouse has spawned an asset. + -- @function [parent=#WAREHOUSE] AssetSpawned + -- @param #WAREHOUSE self + -- @param Wrapper.Group#GROUP group the group that was spawned. + -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + + --- Triggers the FSM event "AssetSpawned" with a delay when the warehouse has spawned an asset. + -- @function [parent=#WAREHOUSE] __AssetSpawned + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Group#GROUP group the group that was spawned. + -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + + --- On after "AssetSpawned" event user function. Called when the warehouse has spawned an asset. + -- @function [parent=#WAREHOUSE] OnAfterAssetSpawned + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP group the group that was spawned. + -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + + + --- Triggers the FSM event "AssetLowFuel" when an asset runs low on fuel + -- @function [parent=#WAREHOUSE] AssetLowFuel + -- @param #WAREHOUSE self + -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel. + -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel. + + --- Triggers the FSM event "AssetLowFuel" with a delay when an asset runs low on fuel. + -- @function [parent=#WAREHOUSE] __AssetLowFuel + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel. + -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel. + + --- On after "AssetLowFuel" event user function. Called when the an asset is low on fuel. + -- @function [parent=#WAREHOUSE] OnAfterAssetLowFuel + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel. + -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel. + + --- Triggers the FSM event "Save" when the warehouse assets are saved to file on disk. -- @function [parent=#WAREHOUSE] Save -- @param #WAREHOUSE self @@ -2403,6 +2461,14 @@ function WAREHOUSE:SetSafeParkingOff() return self end +--- Set low fuel threshold. If one unit of an asset has less fuel than this number, the event AssetLowFuel will be fired. +-- @param #WAREHOUSE self +-- @param #number threshold Relative low fuel threshold, i.e. a number in [0,1]. Default 0.15 (15%). +-- @return #WAREHOUSE self +function WAREHOUSE:SetLowFuelThreshold(threshold) + self.lowfuelthresh=threshold or 0.15 + return self +end --- Set interval of status updates. Note that normally only one request can be processed per time interval. -- @param #WAREHOUSE self @@ -3212,7 +3278,7 @@ end function WAREHOUSE:onafterStatus(From, Event, To) self:I(self.wid..string.format("Checking status of warehouse %s. Current FSM state %s. Global warehouse assets = %d.", self.alias, self:GetState(), #_WAREHOUSEDB.Assets)) - -- Check if any pending jobs are done and can be deleted from the + -- Check if any pending jobs are done and can be deleted from the queue. self:_JobDone() -- Print status. @@ -3241,6 +3307,9 @@ function WAREHOUSE:onafterStatus(From, Event, To) self:_PrintQueue(self.queue, "Queue waiting") self:_PrintQueue(self.pending, "Queue pending") + -- Check fuel for all assets. + self:_CheckFuel() + -- Update warhouse marker on F10 map. self:_UpdateWarehouseMarkText() @@ -3675,7 +3744,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, local SpeedMax=group:GetSpeedMax() local RangeMin=group:GetRange() local smax,sx,sy,sz=_GetObjectSize(Descriptors) - + --self:E(Descriptors) -- Get weight and cargo bay size in kg. @@ -4259,6 +4328,9 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Add transport assets. table.insert(_transportassets,_assetitem) + + -- Asset spawned FSM function. + self:__AssetSpawned(1, spawngroup, _assetitem) end end @@ -5175,7 +5247,7 @@ 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 @@ -5195,6 +5267,9 @@ function WAREHOUSE:_SpawnAssetRequest(Request) if _group then _groupset:AddGroup(_group) table.insert(_assets, _assetitem) + + -- Call FSM function. + self:__AssetSpawned(1,_group,_assetitem) else self:E(self.wid.."ERROR: Cargo asset could not be spawned!") end @@ -5235,7 +5310,7 @@ 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 @@ -5483,7 +5558,6 @@ end -- @param #WAREHOUSE self -- @param Wrapper.Group#GROUP group The ground group to be routed -- @param #WAREHOUSE.Queueitem request The request for this group. --- @param #number Speed Speed in km/h to drive to the destination coordinate. Default is 60% of max possible speed the unit can go. function WAREHOUSE:_RouteGround(group, request) if group and group:IsAlive() then @@ -5526,18 +5600,25 @@ function WAREHOUSE:_RouteGround(group, request) local FromWP=group:GetCoordinate():WaypointGround(_speed, "Off Road") table.insert(Waypoints, 1, FromWP) - -- Final coordinate. - local ToWP=request.warehouse.spawnzone:GetRandomCoordinate():WaypointGround(_speed, "Off Road") - table.insert(Waypoints, #Waypoints+1, ToWP) + -- Final coordinate. Note, this can lead to errors if the final WP is too close the the point on the road. The vehicle will stop driving and not reach the final WP! + --local ToCO=request.warehouse.spawnzone:GetRandomCoordinate() + --local ToWP=ToCO:WaypointGround(_speed, "Off Road") + --table.insert(Waypoints, #Waypoints+1, ToWP) end + for n,wp in ipairs(Waypoints) do + env.info(n) + local tf=self:_SimpleTaskFunctionWP("warehouse:_PassingWaypoint",group, n, #Waypoints) + group:SetTaskWaypoint(wp, tf) + end + -- Task function triggering the arrived event at the last waypoint. - local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) + --local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) -- Put task function on last waypoint. - local Waypoint = Waypoints[#Waypoints] - group:SetTaskWaypoint(Waypoint, TaskFunction) + --local Waypoint = Waypoints[#Waypoints] + --group:SetTaskWaypoint(Waypoint, TaskFunction) -- Route group to destination. group:Route(Waypoints, 1) @@ -5662,9 +5743,30 @@ end -- @param Wrapper.Group#GROUP group The group that arrived. function WAREHOUSE:_Arrived(group) self:_DebugMessage(string.format("Group %s arrived!", tostring(group:GetName()))) + --self:E(string.format("Group %s arrived!", tostring(group:GetName()))) if group then --Trigger "Arrived event. + --group:SmokeBlue() + self:__Arrived(1, group) + end + +end + +--- Task function for when passing a waypoint. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group that arrived. +-- @param #number n Waypoint passed. +-- @param #number N Final waypoint. +function WAREHOUSE:_PassingWaypoint(group,n,N) + self:T(string.format("Group %s passing waypoint %d of %d!", tostring(group:GetName()), n, N)) + + if group then + --group:SmokeGreen() + end + + if n==N then + --group:SmokeBlue() self:__Arrived(1, group) end @@ -5888,40 +5990,27 @@ end -- @param #WAREHOUSE.Pendingitem request Request that needs to be updated. function WAREHOUSE:_UnitDead(deadunit, request) - -- Flare unit - deadunit:FlareRed() + -- Flare unit. + if self.Debug then + deadunit:FlareRed() + end -- Group the dead unit belongs to. local group=deadunit:GetGroup() - -- Check if this was the last unit of the group ==> whole group dead. + -- Number of alive units in group. + local nalive=group:CountAliveUnits() + + -- Whole group is dead? local groupdead=true - local nunits=0 - local nunits0=0 - if group then - -- Get current size of group and substract the unit that just died because it is not counted yet! - nunits=group:GetSize()-1 - nunits0=group:GetInitialSize() - - if nunits > 0 then - groupdead=false - end + if nalive>0 then + groupdead=false end - -- Here I need to get rid of the #CARGO at the end to obtain the original name again! local unitname=self:_GetNameWithOut(deadunit) local groupname=self:_GetNameWithOut(group) - -- Debug message. - local text=string.format("Unit %s died! #units=%d/%d ==> Group dead=%s (IsAlive=%s).", unitname, nunits, nunits0, tostring(groupdead), tostring(group:IsAlive())) - self:T2(self.wid..text) - - -- Check if this really works as expected! - if nunits<0 then - self:E(self.wid.."ERROR: Number of units negative! This should not happen.") - end - -- Group is dead! if groupdead then self:T(self.wid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request)))) @@ -5943,7 +6032,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Easy case: Group can simply be removed from the cargogroupset. --- - -- Remove dead group from carg group set. + -- Remove dead group from cargo group set. if groupdead==true then request.cargogroupset:Remove(groupname, NoTriggerEvent) self:T(self.wid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) @@ -6953,15 +7042,46 @@ 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 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. + 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. + 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) + 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))) + + return DCSTask +end + +--- Simple task function. Can be used to call a function which has the warehouse and the executing group as parameters. +-- @param #WAREHOUSE self +-- @param #string Function The name of the function to call passed as string. +-- @param Wrapper.Group#GROUP group The group which is meant. +-- @param #number n Waypoint passed. +-- @param #number N Final waypoint number. +function WAREHOUSE:_SimpleTaskFunctionWP(Function, group, n, N) + self:F2({Function}) + + -- Name of the warehouse (static) object. + local warehouse=self.warehouse:GetName() + local groupname=group:GetName() + + -- Task script. + local DCSScript = {} + + 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, %d, %d)', Function, n ,N) -- Call the function, e.g. myfunction.(warehouse,mygroup) -- Create task. local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) @@ -7053,6 +7173,17 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) end + -- Check all clients. + --[[ + for _,_unit in pairs(_units) do + local unit=_unit --Wrapper.Unit#UNIT + local _coord=unit:GetCoordinate() + local _size=self:_GetObjectSize(unit:GetDCSObject()) + local _name=unit:GetName() + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="client"}) + end + ]] + -- Check all statics. for _,static in pairs(_statics) do local _vec3=static:getPoint() @@ -7610,6 +7741,61 @@ function WAREHOUSE:_SortQueue() table.sort(self.queue, _sort) end +--- Checks fuel on all pening assets. +-- @param #WAREHOUSE self +function WAREHOUSE:_CheckFuel() + + for i,qitem in ipairs(self.pending) do + local qitem=qitem --#WAREHOUSE.Pendingitem + + if qitem.transportgroupset then + for _,_group in pairs(qitem.transportgroupset:GetSet()) do + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + local fuel=group:GetFuelMin() + + self:T2(self.wid..string.format("Transport group %s min fuel state = %.2f %%", group:GetName(), fuel)) + + if fuel Date: Sun, 26 May 2019 16:49:56 +0200 Subject: [PATCH 276/485] fix issue #948 in Scoring.lua Changed ScoreGoals to PenaltyGoals in function CORING:ReportScoreAllSummary( PlayerGroup ) --- Moose Development/Moose/Functional/Scoring.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index af50957e6..b5c8da81b 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -1635,7 +1635,7 @@ function SCORING:ReportScoreGroupSummary( PlayerGroup ) self:F( { ReportMissions, ScoreMissions, PenaltyMissions } ) local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions PlayerMessage = string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", From 256ef2c7b48d3b14ba1f38675f848165b81e03e0 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 26 May 2019 21:25:11 +0200 Subject: [PATCH 277/485] Update Warehouse.lua --- .../Moose/Functional/Warehouse.lua | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index e2c8c7b6d..49558c58d 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -3096,7 +3096,7 @@ function WAREHOUSE:FindAssetInDB(group) if aid~=nil then local asset=_WAREHOUSEDB.Assets[aid] - self:E({asset=asset}) + self:T2({asset=asset}) if asset==nil then self:_ErrorMessage(string.format("ERROR: Asset for group %s not found in the data base!", group:GetName()), 0) end @@ -7753,14 +7753,21 @@ function WAREHOUSE:_CheckFuel() local group=_group --Wrapper.Group#GROUP if group and group:IsAlive() then - + + -- Get min fuel of group. local fuel=group:GetFuelMin() - self:T2(self.wid..string.format("Transport group %s min fuel state = %.2f %%", group:GetName(), fuel)) + -- Debug info. + self:T2(self.wid..string.format("Transport group %s min fuel state = %.2f", group:GetName(), fuel)) + -- Check if fuel is below threshold for first time. if fuel Date: Mon, 27 May 2019 14:15:17 +0200 Subject: [PATCH 278/485] RANGE 2.1.2 fixes --- Moose Development/Moose/Core/Radio.lua | 2 +- Moose Development/Moose/Functional/Fox.lua | 35 ++++++++---- Moose Development/Moose/Functional/Range.lua | 58 ++++++++++---------- 3 files changed, 53 insertions(+), 42 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index ffd33255d..412e18430 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -1175,7 +1175,7 @@ function RADIOQUEUE:_CheckRadioQueue() else - if time-Tlast>=transmission.interval then + if Tlast==nil or time-Tlast>=transmission.interval then next=transmission else diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index e6fb352f4..69afa5761 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -76,8 +76,8 @@ -- fox=FOX:New() -- -- -- Add training zones. --- fox:AddSafeZone(ZONE:New("Training Zone Alpha") --- fox:AddSafeZone(ZONE:New("Training Zone Bravo") +-- fox:AddSafeZone(ZONE:New("Training Zone Alpha")) +-- fox:AddSafeZone(ZONE:New("Training Zone Bravo")) -- -- -- Start missile trainer. -- fox:Start() @@ -90,8 +90,8 @@ -- fox=FOX:New() -- -- -- Add training zones. --- fox:AddLaunchZone(ZONE:New("Launch Zone SA-10 Krim") --- fox:AddLaunchZone(ZONE:New("Training Zone Bravo") +-- fox:AddLaunchZone(ZONE:New("Launch Zone SA-10 Krim")) +-- fox:AddLaunchZone(ZONE:New("Training Zone Bravo")) -- -- -- Start missile trainer. -- fox:Start() @@ -189,7 +189,7 @@ FOX.MenuF10Root=nil --- FOX class version. -- @field #string version -FOX.version="0.5.0" +FOX.version="0.5.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -444,6 +444,17 @@ function FOX:SetExplosionPower(power) return self end +--- Set missile-player distance when missile is destroyed. +-- @param #FOX self +-- @param #number distance Distance in meters. Default 100 m. +-- @return #FOX self +function FOX:SetExplosionDistance(distance) + + self.explosiondist=distance or 100 + + return self +end + --- Disable F10 menu for all players. -- @param #FOX self @@ -1065,13 +1076,13 @@ function FOX:OnEventShot(EventData) end -- Debug info. - self:E(FOX.lid.."EVENT SHOT: FOX") - self:E(FOX.lid..string.format("EVENT SHOT: Ini unit = %s", tostring(EventData.IniUnitName))) - self:E(FOX.lid..string.format("EVENT SHOT: Ini group = %s", tostring(EventData.IniGroupName))) - self:E(FOX.lid..string.format("EVENT SHOT: Weapon type = %s", tostring(_weapon))) - self:E(FOX.lid..string.format("EVENT SHOT: Weapon categ = %s", tostring(weaponcategory))) - self:E(FOX.lid..string.format("EVENT SHOT: Missil categ = %s", tostring(missilecategory))) - self:E(FOX.lid..string.format("EVENT SHOT: Missil range = %s", tostring(missilerange))) + self:T2(FOX.lid.."EVENT SHOT: FOX") + self:T2(FOX.lid..string.format("EVENT SHOT: Ini unit = %s", tostring(EventData.IniUnitName))) + self:T2(FOX.lid..string.format("EVENT SHOT: Ini group = %s", tostring(EventData.IniGroupName))) + self:T2(FOX.lid..string.format("EVENT SHOT: Weapon type = %s", tostring(_weapon))) + self:T2(FOX.lid..string.format("EVENT SHOT: Weapon categ = %s", tostring(weaponcategory))) + self:T2(FOX.lid..string.format("EVENT SHOT: Missil categ = %s", tostring(missilecategory))) + self:T2(FOX.lid..string.format("EVENT SHOT: Missil range = %s", tostring(missilerange))) -- Check if fired in launch zone. diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index eb3009ee4..a1d716058 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -78,7 +78,7 @@ -- @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. --- @field #boolean autosafe If true, automatically save results every X seconds. +-- @field #boolean autosave If true, automatically save results every X seconds. -- @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. @@ -340,7 +340,7 @@ RANGE.MenuF10Root=nil --- Range script version. -- @field #string version -RANGE.version="2.1.1" +RANGE.version="2.1.2" --TODO list: --TODO: Verbosity level for messages. @@ -617,19 +617,19 @@ function RANGE:SetMessageTimeDuration(time) return self end ---- Automatically safe player results to disc. +--- Automatically save player results to disc. -- @param #RANGE self -- @return #RANGE self -function RANGE:SetAutosafeOn() - self.autosafe=true +function RANGE:SetAutosaveOn() + self.autosave=true return self end ---- Switch off auto safe player results. +--- Switch off auto save player results. -- @param #RANGE self -- @return #RANGE self -function RANGE:SetAutosafeOff() - self.autosafe=false +function RANGE:SetAutosaveOff() + self.autosave=false return self end @@ -662,7 +662,7 @@ function RANGE:SetRangeRadius(radius) return self end ---- Set player setting whether bomb impact points are smoked or not +--- Set player setting whether bomb impact points are smoked or not. -- @param #RANGE self -- @param #boolean switch If true nor nil default is to smoke impact points of bombs. -- @return #RANGE self @@ -1650,7 +1650,7 @@ function RANGE:onafterStatus(From, Event, To) self:_CheckPlayers() -- Save results. - if self.autosafe then + if self.autosave then self:Save() end @@ -1725,10 +1725,10 @@ function RANGE:onafterSave(From, Event, To) end -- Path. - local path=lfs.writedir() + local path=lfs.writedir()..[[Logs\]] -- Set file name. - local filename=path..string.format("\\RANGE-%s_BombingResults.csv", self.rangename) + local filename=path..string.format("RANGE-%s_BombingResults.csv", self.rangename) -- Header line. local scores="Name,Pass,Target,Distance,Radial,Quality,Weapon,Airframe,Mission Time" @@ -1788,11 +1788,11 @@ function RANGE:onafterLoad(From, Event, To) end end - -- Path. - local path=lfs.writedir() + -- Path in DCS log file. + local path=lfs.writedir()..[[Logs\]] -- Set file name. - local filename=path..string.format("\\RANGE-%s_BombingResults.csv", self.rangename) + local filename=path..string.format("RANGE-%s_BombingResults.csv", self.rangename) -- Info message. local text=string.format("Loading player bomb results from file %s", filename) @@ -2185,15 +2185,16 @@ function RANGE:_DisplayBombTargets(_unitname) local _text="Bomb Target Locations:" for _,_bombtarget in pairs(self.bombingTargets) do - local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + local bombtarget=_bombtarget --#RANGE.BombTarget -- Coordinate of bombtarget. - local coord=self:_GetBombTargetCoordinate(_bombtarget) + local coord=self:_GetBombTargetCoordinate(bombtarget) if coord then - local mycoord=coord:ToStringA2G(_unit, _settings) - _text=_text..string.format("\n- %s: %s",_bombtarget.name or "unknown", mycoord) + local ca2g=coord:ToStringA2G(_unit, _settings) + local lldms=coord:ToStringLLDMS() + _text=_text..string.format("\n- %s: %s %s", bombtarget.name or "unknown", ca2g, lldms) end end @@ -2708,21 +2709,19 @@ function RANGE:_MarkTargetsOnMap(_unitName) self:F(_unitName) -- Get group. - local group=nil + local group=nil --Wrapper.Group#GROUP 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 - if _target and _target:IsAlive() then - local coord=_target:GetCoordinate() --Core.Point#COORDINATE - if group then - coord:MarkToGroup("Bomb target ".._bombtarget.name, group) - else - coord:MarkToAll("Bomb target ".._bombtarget.name) - end + local bombtarget=_bombtarget --#RANGE.BombTarget + local coord=self:_GetBombTargetCoordinate(_bombtarget) + if group then + coord:MarkToGroup(string.format("Bomb target %s:\n%s\n%s", bombtarget.name, coord:ToStringLLDMS(), coord:ToStringBULLS(group:GetCoalition())), group) + else + coord:MarkToAll(string.format("Bomb target %s", bombtarget.name)) end end @@ -2733,7 +2732,8 @@ function RANGE:_MarkTargetsOnMap(_unitName) if _target and _target:IsAlive() then local coord=_target:GetCoordinate() --Core.Point#COORDINATE if group then - coord:MarkToGroup("Strafe target ".._target:GetName(), group) + --coord:MarkToGroup("Strafe target ".._target:GetName(), group) + coord:MarkToGroup(string.format("Strafe target %s:\n%s\n%s", _target:GetName(), coord:ToStringLLDMS(), coord:ToStringBULLS(group:GetCoalition())), group) else coord:MarkToAll("Strafe target ".._target:GetName()) end From d3ecbac40abf802fd6d3ce65465151204889f85a Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 27 May 2019 17:10:19 +0200 Subject: [PATCH 279/485] Updates for mission mode. --- Moose Development/Moose/AI/AI_Escort.lua | 78 ++++++++++++------- .../Moose/AI/AI_Escort_Request.lua | 20 +++-- 2 files changed, 65 insertions(+), 33 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 6a7517dd9..0cf5b5800 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -274,7 +274,7 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetName(), self.MainMenu ) - EscortGroup:WayPointInitialize( 1 ) + EscortGroup:WayPointInitialize() EscortGroup:OptionROTVertical() EscortGroup:OptionROEOpenFire() @@ -361,7 +361,7 @@ function AI_ESCORT:Menus( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevel self:MenuROE() self:MenuROT() --- self:MenuResumeMission() + self:MenuResumeMission() return self @@ -1047,10 +1047,16 @@ end function AI_ESCORT:MenuResumeMission() self:F() - if not self.EscortMenuResumeMission then - -- Mission Resume Menu Root - self.EscortMenuResumeMission = MENU_GROUP:New( self.PlayerGroup, "Resume mission from", self.EscortMenu ) - end + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + EscortGroup.EscortMenuResumeMission = MENU_GROUP:New( self.PlayerGroup, "Resume from", EscortMenu ) + end + end + ) return self end @@ -1324,10 +1330,12 @@ function AI_ESCORT:_ScanTargets( ScanDuration ) end ---- @param #AI_ESCORT self --- @param Wrapper.Group#GROUP EscortGroup +--- @param Wrapper.Group#GROUP EscortGroup +-- @param #AI_ESCORT self function AI_ESCORT.___Resume( EscortGroup, self ) + self:F( { self=self } ) + local PlayerGroup = self.PlayerGroup self:JoinFormation( EscortGroup ) @@ -1336,6 +1344,26 @@ function AI_ESCORT.___Resume( EscortGroup, self ) end +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +-- @param #number WayPoint +function AI_ESCORT._ResumeMission( EscortGroup, WayPoint ) + + --self.FollowScheduler:Stop( self.FollowSchedule ) + + local WayPoints = EscortGroup:GetTaskRoute() + self:T( WayPoint, WayPoints ) + + for WayPointIgnore = 1, WayPoint do + table.remove( WayPoints, 1 ) + end + + EscortGroup:SetTask( EscortGroup:TaskRoute( WayPoints ), 1 ) + + EscortGroup:MessageTypeToClient( "Resuming mission from waypoint ", MESSAGE.Type.Information, self.PlayerGroup ) +end + + --- @param #AI_ESCORT self -- @param Wrapper.Group#GROUP EscortGroup The escort group that will attack the detected item. -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem @@ -1481,26 +1509,6 @@ end -function AI_ESCORT:_ResumeMission( WayPoint ) - - local EscortGroup = self.EscortGroup - local EscortUnit = self.PlayerUnit - - self.FollowScheduler:Stop( self.FollowSchedule ) - - local WayPoints = EscortGroup:GetTaskRoute() - self:T( WayPoint, WayPoints ) - - for WayPointIgnore = 1, WayPoint do - table.remove( WayPoints, 1 ) - end - - SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) - - EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortUnit ) -end - - --- Registers the waypoints -- @param #AI_ESCORT self -- @return #table @@ -1580,6 +1588,20 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) EscortMenuAttackTargets:RemoveSubMenus( TimeUpdate, "Esort" ) + if EscortGroup.EscortMenuResumeMission then + EscortGroup.EscortMenuResumeMission:RemoveSubMenus() + + local TaskPoints = EscortGroup:GetTaskRoute() + + for WayPointID, WayPoint in pairs( TaskPoints ) do + local EscortVec3 = EscortGroup:GetVec3() + local Distance = ( ( WayPoint.x - EscortVec3.x )^2 + + ( WayPoint.y - EscortVec3.z )^2 + ) ^ 0.5 / 1000 + MENU_GROUP_COMMAND:New( self.PlayerGroup, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", EscortGroup.EscortMenuResumeMission, AI_ESCORT._ResumeMission, self, EscortGroup, WayPointID ) + end + end + return true else end diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index 8d17d2657..e7d2b6d78 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -149,7 +149,7 @@ --- @type AI_ESCORT_REQUEST --- @extends AI.AI_Formation#AI_FORMATION +-- @extends AI.AI_Escort#AI_ESCORT --- AI_ESCORT_REQUEST class -- @@ -216,7 +216,8 @@ function AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, EscortAirbase, EscortNa self.Detection = DETECTION_AREAS:New( self.EscortGroupSet, 5000 ) self.Detection:__Start( 30 ) - + + self.SpawnMode = self.__Enum.Mode.Mission return self end @@ -240,8 +241,10 @@ function AI_ESCORT_REQUEST:SpawnEscort() Report:Add( "Joining Up " .. self.EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) - self:FormationTrail( 50, 50, 50 ) - self:JoinFormation( EscortGroup ) + if self.SpawnMode == self.__Enum.Mode.Formation then + self:FormationTrail( 50, 50, 50 ) + self:JoinFormation( EscortGroup ) + end self:Menus( self.XStart, self.XSpace, self.YStart, self.YSpace, self.ZStart, self.ZSpace, self.ZLevels ) end ) @@ -253,7 +256,7 @@ end function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) if not self.MenuRequestEscort then - self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request A2G Escort", self.MainMenu, + self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request new escort ", self.MainMenu, function() self:SpawnEscort() end @@ -262,3 +265,10 @@ function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) end +--- Set the spawn mode to be mission execution. +-- @param #AI_ESCORT_REQUEST self +function AI_ESCORT_REQUEST:SetEscortSpawnMission() + + self.SpawnMode = self.__Enum.Mode.Mission + +end From a53bb1aac6a216cdd85c93ac46a0e27b15c6edfd Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 28 May 2019 00:24:47 +0200 Subject: [PATCH 280/485] WH v0.9.1 - Added request as parameter in AssetSpawned event. --- Moose Development/Moose/Functional/Warehouse.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 49558c58d..16ad67f5e 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1743,7 +1743,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.9.0" +WAREHOUSE.version="0.9.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -2318,6 +2318,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @param Wrapper.Group#GROUP group the group that was spawned. -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + -- @param #WAREHOUSE.Pendingitem request The request of the spawned asset. --- Triggers the FSM event "AssetSpawned" with a delay when the warehouse has spawned an asset. -- @function [parent=#WAREHOUSE] __AssetSpawned @@ -2325,6 +2326,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #number delay Delay in seconds. -- @param Wrapper.Group#GROUP group the group that was spawned. -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + -- @param #WAREHOUSE.Pendingitem request The request of the spawned asset. --- On after "AssetSpawned" event user function. Called when the warehouse has spawned an asset. -- @function [parent=#WAREHOUSE] OnAfterAssetSpawned @@ -2334,6 +2336,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #string To To state. -- @param Wrapper.Group#GROUP group the group that was spawned. -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + -- @param #WAREHOUSE.Pendingitem request The request of the spawned asset. --- Triggers the FSM event "AssetLowFuel" when an asset runs low on fuel @@ -4330,7 +4333,7 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) table.insert(_transportassets,_assetitem) -- Asset spawned FSM function. - self:__AssetSpawned(1, spawngroup, _assetitem) + self:__AssetSpawned(1, spawngroup, _assetitem, Request) end end @@ -5269,7 +5272,7 @@ function WAREHOUSE:_SpawnAssetRequest(Request) table.insert(_assets, _assetitem) -- Call FSM function. - self:__AssetSpawned(1,_group,_assetitem) + self:__AssetSpawned(1,_group,_assetitem, Request) else self:E(self.wid.."ERROR: Cargo asset could not be spawned!") end From c5236d337e77c05370bd59dbfe50a85cc5ee23ca Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 28 May 2019 20:44:51 +0300 Subject: [PATCH 281/485] Updates --- Moose Development/Moose/AI/AI_Escort.lua | 142 ++-- .../Moose/AI/AI_Escort_Request.lua | 25 +- Moose Development/Moose/AI/AI_Formation.lua | 5 +- Moose Development/Moose/Core/Spawn.lua | 651 +++++++++--------- Moose Development/Moose/Wrapper/Airbase.lua | 7 +- 5 files changed, 451 insertions(+), 379 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 0cf5b5800..b8e169a5e 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -225,6 +225,7 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) self.EscortBriefing = EscortBriefing + self.Menu = {} -- if not EscortBriefing then -- EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. @@ -545,26 +546,36 @@ function AI_ESCORT:MenuFormationBox( XStart, XSpace, YStart, YSpace, ZStart, ZSp end +--- Sets a menu slot to join formation for an escort. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:EscortMenuJoinUp( EscortGroup ) + + if self.Menu.JoinUp == true then + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", EscortMenuReportNavigation, AI_ESCORT._JoinUp, self, EscortGroup ) + end + end +end + + --- Defines --- Defines a menu slot to let the escort to join formation. --- This menu will appear under **Formation**. -- @param #AI_ESCORT self -- @return #AI_ESCORT function AI_ESCORT:MenuJoinUp() + self.Menu.JoinUp = true + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) local FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - if EscortGroup:IsAir() then - - local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) - local EscortMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", EscortMenuReportNavigation, AI_ESCORT._JoinUp, self ) - - end + self:EscortSetMenuJoinUp( EscortGroup ) end ) @@ -572,6 +583,32 @@ function AI_ESCORT:MenuJoinUp() end +function AI_ESCORT:EscortMenuHoldAtEscortPosition( EscortGroup ) + + for _, HoldAtEscortPosition in pairs( self.Menu.HoldAtEscortPosition ) do + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuHoldPosition = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + HoldAtEscortPosition.MenuText, + EscortMenuReportNavigation, + AI_ESCORT._HoldPosition, + self, + EscortGroup, + EscortGroup, + HoldAtEscortPosition.Height, + HoldAtEscortPosition.Speed + ) + end + end + + return self +end + + --- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. -- This menu will appear under **Hold position**. -- @param #AI_ESCORT self @@ -619,28 +656,16 @@ function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) Speed ) + self.Menu.HoldAtEscortPosition = self.Menu.HoldAtEscortPosition or {} + self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition+1] = {} + self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].Height = Height + self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].Speed = Speed + self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].MenuText = MenuText + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - if EscortGroup:IsAir() then - - local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) - - local EscortMenuHoldPosition = MENU_GROUP_COMMAND - :New( - self.PlayerGroup, - MenuText, - EscortMenuReportNavigation, - AI_ESCORT._HoldPosition, - self, - EscortGroup, - EscortGroup, - Height, - Speed - ) - end + self:EscortMenuHoldAtEscortPosition( EscortGroup ) end ) @@ -648,6 +673,33 @@ function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) end +function AI_ESCORT:EscortMenuHoldAtLeaderPosition( EscortGroup ) + + for _, HoldAtLeaderPosition in pairs( self.Menu.HoldAtLeaderPosition ) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + + local EscortMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + HoldAtLeaderPosition.MenuText, + EscortMenuReportNavigation, + AI_ESCORT._HoldPosition, + self, + self.PlayerGroup, + EscortGroup, + HoldAtLeaderPosition.Height, + HoldAtLeaderPosition.Speed + ) + end + end + + return self +end + --- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. -- This menu will appear under **Navigation**. -- @param #AI_ESCORT self @@ -695,28 +747,16 @@ function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) Speed ) + self.Menu.HoldAtLeaderPosition = self.Menu.HoldAtLeaderPosition or {} + self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition+1] = {} + self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].Height = Height + self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].Speed = Speed + self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].MenuText = MenuText + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - if EscortGroup:IsAir() then - - local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) - - local EscortMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND - :New( - self.PlayerGroup, - MenuText, - EscortMenuReportNavigation, - AI_ESCORT._HoldPosition, - self, - self.PlayerGroup, - EscortGroup, - Height, - Speed - ) - end + self:EscortMenuHoldAtLeaderPosition( EscortGroup ) end ) @@ -1140,7 +1180,7 @@ function AI_ESCORT:_JoinUp( EscortGroup ) end -function AI_ESCORT:_FlightJoinUp( EscortGroup ) +function AI_ESCORT:_FlightJoinUp() self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup @@ -1339,7 +1379,7 @@ function AI_ESCORT.___Resume( EscortGroup, self ) local PlayerGroup = self.PlayerGroup self:JoinFormation( EscortGroup ) - EscortGroup:MessageTypeToClient( "Destroyed all targets. Rejoining.", MESSAGE.Type.Information, PlayerGroup ) + EscortGroup:MessageTypeToGroup( "Destroyed all targets. Rejoining.", MESSAGE.Type.Information, PlayerGroup ) end @@ -1347,7 +1387,7 @@ end --- @param #AI_ESCORT self -- @param Wrapper.Group#GROUP EscortGroup -- @param #number WayPoint -function AI_ESCORT._ResumeMission( EscortGroup, WayPoint ) +function AI_ESCORT:_ResumeMission( EscortGroup, WayPoint ) --self.FollowScheduler:Stop( self.FollowSchedule ) @@ -1360,7 +1400,7 @@ function AI_ESCORT._ResumeMission( EscortGroup, WayPoint ) EscortGroup:SetTask( EscortGroup:TaskRoute( WayPoints ), 1 ) - EscortGroup:MessageTypeToClient( "Resuming mission from waypoint ", MESSAGE.Type.Information, self.PlayerGroup ) + EscortGroup:MessageTypeToGroup( "Resuming mission from waypoint ", MESSAGE.Type.Information, self.PlayerGroup ) end diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index e7d2b6d78..4b95b4b90 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -193,7 +193,7 @@ AI_ESCORT_REQUEST = { -- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where escorts will be spawned once requested. -- @param #string EscortName Name of the escort. -- @param #string EscortBriefing A text showing the AI_ESCORT_REQUEST briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. --- @return #AI_ESCORT_REQUEST self +-- @return #AI_ESCORT_REQUEST -- @usage -- EscortSpawn = SPAWN:NewWithAlias( "Red A2G Escort Template", "Red A2G Escort AI" ):InitLimit( 10, 10 ) -- EscortSpawn:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Sochi_Adler ), AIRBASE.TerminalType.OpenBig ) @@ -223,10 +223,9 @@ function AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, EscortAirbase, EscortNa end --- @param #AI_ESCORT_REQUEST self --- @param Core.Set#SET_GROUP EscortGroupSet function AI_ESCORT_REQUEST:SpawnEscort() - local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Cold ) + local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Hot ) self.EscortGroupSet:AddGroup( EscortGroup ) EscortGroup:OptionROTVertical() @@ -241,11 +240,27 @@ function AI_ESCORT_REQUEST:SpawnEscort() Report:Add( "Joining Up " .. self.EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) + self:FormationTrail( 50, 50, 50 ) if self.SpawnMode == self.__Enum.Mode.Formation then - self:FormationTrail( 50, 50, 50 ) self:JoinFormation( EscortGroup ) end - self:Menus( self.XStart, self.XSpace, self.YStart, self.YSpace, self.ZStart, self.ZSpace, self.ZLevels ) + + --self:Menus( self.XStart, self.XSpace, self.YStart, self.YSpace, self.ZStart, self.ZSpace, self.ZLevels ) + + self:EscortMenuJoinUp( EscortGroup ) + + self:EscortMenuHoldAtEscortPosition( EscortGroup ) + self:EscortMenuHoldAtLeaderPosition( EscortGroup ) + + self:MenuFlare() + self:MenuSmoke() + + self:MenuReportTargets( 60 ) + self:MenuAssistedAttack() + self:MenuROE() + self:MenuROT() + + self:MenuResumeMission() end ) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 5eb5d0e90..f4ad1902f 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -717,7 +717,6 @@ function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, X i = i + 1 FollowGroup:SetState( FollowGroup, "Formation", Formation ) - FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) end return self @@ -873,7 +872,6 @@ function AI_FORMATION:onafterFormationCenterWing( FollowGroupSet, From , Event , FollowGroup:SetState( self, "FormationVec3", Vec3 ) i = i + 1 FollowGroup:SetState( FollowGroup, "Formation", self.__Enum.Formation.Vic ) - FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) end return self @@ -934,7 +932,6 @@ function AI_FORMATION:onafterFormationBox( FollowGroupSet, From , Event , To, XS FollowGroup:SetState( self, "FormationVec3", Vec3 ) i = i + 1 FollowGroup:SetState( FollowGroup, "Formation", self.__Enum.Formation.Box ) - FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) end return self @@ -1026,7 +1023,7 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 ClientUnit:SetState( self, "CV1", CV2 ) end - FollowGroupSet:ForEachGroup( + FollowGroupSet:ForEachGroupAlive( --- @param Wrapper.Group#GROUP FollowGroup -- @param Wrapper.Unit#UNIT ClientUnit function( FollowGroup, Formation, ClientUnit, CT1, CV1, CT2, CV2 ) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index d042f49e6..3495907ad 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1835,6 +1835,336 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT end +--- Will park a group at an @{Wrapper.Airbase}. +-- +-- @param #SPAWN self +-- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group. +-- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}. +-- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactily these spots! +-- @return #nil Nothing is returned! +function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) + + self:F( { SpawnIndex = SpawnIndex, SpawnMaxGroups = self.SpawnMaxGroups } ) + + -- Get position of airbase. + local PointVec3 = SpawnAirbase:GetCoordinate() + self:T2(PointVec3) + + -- Set take off type. Default is hot. + local Takeoff = SPAWN.Takeoff.Cold + + -- Get group template. + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + + if SpawnTemplate then + + -- Check if the aircraft with the specified SpawnIndex is already spawned. + -- If yes, ensure that the aircraft is spawned at the same aircraft spot. + + local GroupAlive = self:GetGroupFromIndex( SpawnIndex ) + + -- Debug output + self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) + + -- Template group, unit and its attributes. + local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) + local TemplateUnit=TemplateGroup:GetUnit(1) + local ishelo=TemplateUnit:HasAttribute("Helicopters") + local isbomber=TemplateUnit:HasAttribute("Bombers") + local istransport=TemplateUnit:HasAttribute("Transports") + local isfighter=TemplateUnit:HasAttribute("Battleplanes") + + -- Number of units in the group. With grouping this can actually differ from the template group size! + local nunits=#SpawnTemplate.units + + -- First waypoint of the group. + local SpawnPoint = SpawnTemplate.route.points[1] + + -- These are only for ships and FARPS. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Get airbase ID and category. + local AirbaseID = SpawnAirbase:GetID() + local AirbaseCategory = SpawnAirbase:GetDesc().category + self:F( { AirbaseCategory = AirbaseCategory } ) + + -- Set airdromeId. + if AirbaseCategory == Airbase.Category.SHIP then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.HELIPAD then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.AIRDROME then + SpawnPoint.airdromeId = AirbaseID + end + + -- Set waypoint type/action. + SpawnPoint.alt = 0 + SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + + -- Check if we spawn on ground. + local spawnonground=not (Takeoff==SPAWN.Takeoff.Air) + self:T({spawnonground=spawnonground, TOtype=Takeoff, TOair=Takeoff==SPAWN.Takeoff.Air}) + + -- Check where we actually spawn if we spawn on ground. + local spawnonship=false + local spawnonfarp=false + local spawnonrunway=false + local spawnonairport=false + if spawnonground then + if AirbaseCategory == Airbase.Category.SHIP then + spawnonship=true + elseif AirbaseCategory == Airbase.Category.HELIPAD then + spawnonfarp=true + elseif AirbaseCategory == Airbase.Category.AIRDROME then + spawnonairport=true + end + spawnonrunway=Takeoff==SPAWN.Takeoff.Runway + end + + -- Array with parking spots coordinates. + local parkingspots={} + local parkingindex={} + local spots + + -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship. + if spawnonground and not SpawnTemplate.parked then + + + -- Number of free parking spots. + local nfree=0 + + -- Set terminal type. + local termtype=TerminalType + + -- Scan options. Might make that input somehow. + local scanradius=50 + local scanunits=true + local scanstatics=true + local scanscenery=false + local verysafe=false + + -- Number of free parking spots at the airbase. + if spawnonship or spawnonfarp or spawnonrunway then + -- These places work procedural and have some kind of build in queue ==> Less effort. + self:T(string.format("Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype, true) + spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype, true) + elseif Parkingdata~=nil then + -- Parking data explicitly set by user as input parameter. + nfree=#Parkingdata + spots=Parkingdata + else + if ishelo then + if termtype==nil then + -- Helo is spawned. Try exclusive helo spots first. + self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly)) + spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) + nfree=#spots + if nfree=1 then + + -- All units get the same spot. DCS takes care of the rest. + for i=1,nunits do + table.insert(parkingspots, spots[1].Coordinate) + table.insert(parkingindex, spots[1].TerminalID) + end + -- This is actually used... + PointVec3=spots[1].Coordinate + + else + -- If there is absolutely no spot ==> air start! + _notenough=true + end + + elseif spawnonairport then + + if nfree>=nunits then + + for i=1,nunits do + table.insert(parkingspots, spots[i].Coordinate) + table.insert(parkingindex, spots[i].TerminalID) + end + + else + -- Not enough spots for the whole group ==> air start! + _notenough=true + end + end + + -- Not enough spots ==> Prepare airstart. + if _notenough then + + if not self.SpawnUnControlled then + else + self:E(string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + return nil + end + end + + else + + end + + if not SpawnTemplate.parked then + -- Translate the position of the Group Template to the Vec3. + + SpawnTemplate.parked = true + + for UnitID = 1, nunits do + self:F('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + + -- Template of the current unit. + local UnitTemplate = SpawnTemplate.units[UnitID] + + -- Tranlate position and preserve the relative position/formation of all aircraft. + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = PointVec3.x + (SX-BX) + local TY = PointVec3.z + (SY-BY) + + if spawnonground then + + -- Ships and FARPS seem to have a build in queue. + if spawnonship or spawnonfarp or spawnonrunway then + + self:T(string.format("Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn on ship. We take only the position of the ship. + SpawnTemplate.units[UnitID].x = PointVec3.x --TX + SpawnTemplate.units[UnitID].y = PointVec3.z --TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + else + + self:T(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + + -- Get coordinates of parking spot. + SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x + SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z + SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y + + --parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + end + + else + + self:T(string.format("Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + end + + -- Parking spot id. + UnitTemplate.parking = nil + UnitTemplate.parking_id = nil + if parkingindex[UnitID] then + UnitTemplate.parking = parkingindex[UnitID] + end + + -- Debug output. + self:T2(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) + self:T2(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) + self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + end + end + + -- Set gereral spawnpoint position. + SpawnPoint.x = PointVec3.x + SpawnPoint.y = PointVec3.z + SpawnPoint.alt = PointVec3.y + + SpawnTemplate.x = PointVec3.x + SpawnTemplate.y = PointVec3.z + + SpawnTemplate.uncontrolled = true + + -- Spawn group. + local GroupSpawned = self:SpawnWithIndex( SpawnIndex, true ) + + -- 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 ) + end + end + + -- Check if we accidentally spawned on the runway. Needs to be schedules, because group is not immidiately alive. + if Takeoff~=SPAWN.Takeoff.Runway and Takeoff~=SPAWN.Takeoff.Air and spawnonairport then + SCHEDULER:New(nil, AIRBASE.CheckOnRunWay, {SpawnAirbase, GroupSpawned, 75, true} , 1.0) + end + + end + +end + --- Will park a group at an @{Wrapper.Airbase}. -- This method is mostly advisable to be used if you want to simulate parking units at an airbase and be visible. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. @@ -1876,325 +2206,10 @@ end function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, R2.4, R2.5 self:F( { self.SpawnTemplatePrefix, SpawnAirbase, TerminalType } ) - -- Get position of airbase. - local PointVec3 = SpawnAirbase:GetCoordinate() + self:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, 1 ) - -- Set take off type. Default is hot. - local Takeoff = SPAWN.Takeoff.Cold - - for SpawnIndex = 1, self.SpawnMaxGroups do - - self:F( { SpawnIndex = SpawnIndex, SpawnMaxGroups = self.SpawnMaxGroups } ) - - -- Get group template. - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - -- Check if the aircraft with the specified SpawnIndex is already spawned. - -- If yes, ensure that the aircraft is spawned at the same aircraft spot. - - local GroupAlive = self:GetGroupFromIndex( SpawnIndex ) - - -- Debug output - self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) - - -- Template group, unit and its attributes. - local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) - local TemplateUnit=TemplateGroup:GetUnit(1) - local ishelo=TemplateUnit:HasAttribute("Helicopters") - local isbomber=TemplateUnit:HasAttribute("Bombers") - local istransport=TemplateUnit:HasAttribute("Transports") - local isfighter=TemplateUnit:HasAttribute("Battleplanes") - - -- Number of units in the group. With grouping this can actually differ from the template group size! - local nunits=#SpawnTemplate.units - - -- First waypoint of the group. - local SpawnPoint = SpawnTemplate.route.points[1] - - -- These are only for ships and FARPS. - SpawnPoint.linkUnit = nil - SpawnPoint.helipadId = nil - SpawnPoint.airdromeId = nil - - -- Get airbase ID and category. - local AirbaseID = SpawnAirbase:GetID() - local AirbaseCategory = SpawnAirbase:GetDesc().category - self:F( { AirbaseCategory = AirbaseCategory } ) - - -- Set airdromeId. - if AirbaseCategory == Airbase.Category.SHIP then - SpawnPoint.linkUnit = AirbaseID - SpawnPoint.helipadId = AirbaseID - elseif AirbaseCategory == Airbase.Category.HELIPAD then - SpawnPoint.linkUnit = AirbaseID - SpawnPoint.helipadId = AirbaseID - elseif AirbaseCategory == Airbase.Category.AIRDROME then - SpawnPoint.airdromeId = AirbaseID - end - - -- Set waypoint type/action. - SpawnPoint.alt = 0 - SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type - SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action - - -- Check if we spawn on ground. - local spawnonground=not (Takeoff==SPAWN.Takeoff.Air) - self:T({spawnonground=spawnonground, TOtype=Takeoff, TOair=Takeoff==SPAWN.Takeoff.Air}) - - -- Check where we actually spawn if we spawn on ground. - local spawnonship=false - local spawnonfarp=false - local spawnonrunway=false - local spawnonairport=false - if spawnonground then - if AirbaseCategory == Airbase.Category.SHIP then - spawnonship=true - elseif AirbaseCategory == Airbase.Category.HELIPAD then - spawnonfarp=true - elseif AirbaseCategory == Airbase.Category.AIRDROME then - spawnonairport=true - end - spawnonrunway=Takeoff==SPAWN.Takeoff.Runway - end - - -- Array with parking spots coordinates. - local parkingspots={} - local parkingindex={} - local spots - - -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship. - if spawnonground and not SpawnTemplate.parked then - - - -- Number of free parking spots. - local nfree=0 - - -- Set terminal type. - local termtype=TerminalType - - -- Scan options. Might make that input somehow. - local scanradius=50 - local scanunits=true - local scanstatics=true - local scanscenery=false - local verysafe=false - - -- Number of free parking spots at the airbase. - if spawnonship or spawnonfarp or spawnonrunway then - -- These places work procedural and have some kind of build in queue ==> Less effort. - self:T(string.format("Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) - nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype, true) - spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype, true) - elseif Parkingdata~=nil then - -- Parking data explicitly set by user as input parameter. - nfree=#Parkingdata - spots=Parkingdata - else - if ishelo then - if termtype==nil then - -- Helo is spawned. Try exclusive helo spots first. - self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly)) - spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) - nfree=#spots - if nfree=1 then - - -- All units get the same spot. DCS takes care of the rest. - for i=1,nunits do - table.insert(parkingspots, spots[1].Coordinate) - table.insert(parkingindex, spots[1].TerminalID) - end - -- This is actually used... - PointVec3=spots[1].Coordinate - - else - -- If there is absolutely no spot ==> air start! - _notenough=true - end - - elseif spawnonairport then - - if nfree>=nunits then - - for i=1,nunits do - table.insert(parkingspots, spots[i].Coordinate) - table.insert(parkingindex, spots[i].TerminalID) - end - - else - -- Not enough spots for the whole group ==> air start! - _notenough=true - end - end - - -- Not enough spots ==> Prepare airstart. - if _notenough then - - if not self.SpawnUnControlled then - else - self:E(string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) - return nil - end - end - - else - - end - - if not SpawnTemplate.parked then - -- Translate the position of the Group Template to the Vec3. - - SpawnTemplate.parked = true - - for UnitID = 1, nunits do - self:F('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) - - -- Template of the current unit. - local UnitTemplate = SpawnTemplate.units[UnitID] - - -- Tranlate position and preserve the relative position/formation of all aircraft. - local SX = UnitTemplate.x - local SY = UnitTemplate.y - local BX = SpawnTemplate.route.points[1].x - local BY = SpawnTemplate.route.points[1].y - local TX = PointVec3.x + (SX-BX) - local TY = PointVec3.z + (SY-BY) - - if spawnonground then - - -- Ships and FARPS seem to have a build in queue. - if spawnonship or spawnonfarp or spawnonrunway then - - self:T(string.format("Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) - - -- Spawn on ship. We take only the position of the ship. - SpawnTemplate.units[UnitID].x = PointVec3.x --TX - SpawnTemplate.units[UnitID].y = PointVec3.z --TY - SpawnTemplate.units[UnitID].alt = PointVec3.y - - else - - self:T(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) - - -- Get coordinates of parking spot. - SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x - SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z - SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y - - --parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) - end - - else - - self:T(string.format("Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) - - -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. - SpawnTemplate.units[UnitID].x = TX - SpawnTemplate.units[UnitID].y = TY - SpawnTemplate.units[UnitID].alt = PointVec3.y - - end - - -- Parking spot id. - UnitTemplate.parking = nil - UnitTemplate.parking_id = nil - if parkingindex[UnitID] then - UnitTemplate.parking = parkingindex[UnitID] - end - - -- Debug output. - self:T2(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) - self:T2(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) - self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) - end - end - - -- Set gereral spawnpoint position. - SpawnPoint.x = PointVec3.x - SpawnPoint.y = PointVec3.z - SpawnPoint.alt = PointVec3.y - - SpawnTemplate.x = PointVec3.x - SpawnTemplate.y = PointVec3.z - - SpawnTemplate.uncontrolled = true - - -- Spawn group. - local GroupSpawned = self:SpawnWithIndex( SpawnIndex, true ) - - -- 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 ) - end - end - - -- Check if we accidentally spawned on the runway. Needs to be schedules, because group is not immidiately alive. - if Takeoff~=SPAWN.Takeoff.Runway and Takeoff~=SPAWN.Takeoff.Air and spawnonairport then - SCHEDULER:New(nil, AIRBASE.CheckOnRunWay, {SpawnAirbase, GroupSpawned, 75, true} , 1.0) - end - - end + for SpawnIndex = 2, self.SpawnMaxGroups do + self:ScheduleOnce( SpawnIndex * 0.1, SPAWN.ParkAircraft, self, SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) end self:SetSpawnIndex() diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 953ba2457..1960b67a0 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -549,12 +549,15 @@ function AIRBASE:GetParkingSpotsTable(termtype) local spots={} for _,_spot in pairs(parkingdata) do if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then + self:I({_spot=_spot}) local _free=_isfree(_spot) local _coord=COORDINATE:NewFromVec3(_spot.vTerminalPos) table.insert(spots, {Coordinate=_coord, TerminalID=_spot.Term_Index, TerminalType=_spot.Term_Type, TOAC=_spot.TO_AC, Free=_free, TerminalID0=_spot.Term_Index_0, DistToRwy=_spot.fDistToRW}) end end + self:I({ spots = spots } ) + return spots end @@ -704,6 +707,8 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE local _termid=parkingspot.TerminalID + self:I({_termid=_termid}) + if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) then -- Very safe uses the DCS getParking() info to check if a spot is free. Unfortunately, the function returns free=false until the aircraft has actually taken-off. @@ -783,7 +788,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, --_spot:MarkToAll(string.format("Parking spot %d free=%s", parkingspot.TerminalID, tostring(not occupied))) if occupied then - self:T(string.format("%s: Parking spot id %d occupied.", airport, _termid)) + self:I(string.format("%s: Parking spot id %d occupied.", airport, _termid)) else self:I(string.format("%s: Parking spot id %d free.", airport, _termid)) if nvalid<_nspots then From 271217cf04fcdcdf67c1f3bbb7dfcbbe43eb6815 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 28 May 2019 23:58:14 +0200 Subject: [PATCH 282/485] Update Warehouse.lua --- Moose Development/Moose/Functional/Warehouse.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 16ad67f5e..1f6b3d441 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -3152,7 +3152,7 @@ function WAREHOUSE:onafterStart(From, Event, To) end -- Mark point at road connection. if self.road then - self.road:MarkToAll(string.format("%s road connection.", self.alias), true) + self.markroad=self.road:MarkToCoalition(string.format("%s road connection.",self.alias), self:GetCoalition(), true) end -- Get the closest point on railroad wrt spawnzone of ground assets. @@ -3166,7 +3166,7 @@ function WAREHOUSE:onafterStart(From, Event, To) end -- Mark point at rail connection. if self.rail then - self.rail:MarkToAll(string.format("%s rail connection.", self.alias), true) + self.markrail=self.rail:MarkToCoalition(string.format("%s rail connection.", self.alias), self:GetCoalition(), true) end -- Handle events: From 7b5b57c08734153b4ededc04feec1ab3d90c14be Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 31 May 2019 16:35:35 +0200 Subject: [PATCH 283/485] updates --- Moose Development/Moose/AI/AI_Escort.lua | 39 ++++++++----------- .../Moose/AI/AI_Escort_Request.lua | 26 ++++++++++--- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index b8e169a5e..734b84a8f 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -293,6 +293,9 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) self.Detection:__Start( 30 ) + self:HandleEvent( EVENTS.Dead, OnEventDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, OnEventDeadOrCrash ) + end --- Set a Detection method for the EscortUnit to be reported upon. @@ -554,8 +557,7 @@ function AI_ESCORT:EscortMenuJoinUp( EscortGroup ) if self.Menu.JoinUp == true then if EscortGroup:IsAir() then local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) local EscortMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", EscortMenuReportNavigation, AI_ESCORT._JoinUp, self, EscortGroup ) end end @@ -588,8 +590,7 @@ function AI_ESCORT:EscortMenuHoldAtEscortPosition( EscortGroup ) for _, HoldAtEscortPosition in pairs( self.Menu.HoldAtEscortPosition ) do if EscortGroup:IsAir() then local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) local EscortMenuHoldPosition = MENU_GROUP_COMMAND :New( self.PlayerGroup, @@ -679,8 +680,7 @@ function AI_ESCORT:EscortMenuHoldAtLeaderPosition( EscortGroup ) if EscortGroup:IsAir() then local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) local EscortMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND :New( @@ -850,8 +850,7 @@ function AI_ESCORT:MenuFlare( MenuTextFormat ) function( EscortGroup ) local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) local EscortMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuText, EscortMenuReportNavigation ) local EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green, "Released a green flare!" ) @@ -897,8 +896,7 @@ function AI_ESCORT:MenuSmoke( MenuTextFormat ) if not EscortGroup:IsAir() then local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortMenu ) + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) local EscortMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuText, EscortMenuReportNavigation ) local EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green, "Releasing green smoke!" ) @@ -948,8 +946,7 @@ function AI_ESCORT:MenuReportTargets( Seconds ) if EscortGroup:IsAir() then local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", EscortMenu ) + local EscortMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu ) -- Report Targets @@ -958,7 +955,7 @@ function AI_ESCORT:MenuReportTargets( Seconds ) --EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) -- Attack Targets - local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortMenu ) + local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, timer, Seconds ) timer=timer+1 @@ -1012,8 +1009,7 @@ function AI_ESCORT:MenuROE( MenuTextFormat ) -- Rules of Engagement local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", EscortMenu ) + local EscortMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", EscortGroup.EscortMenu ) if EscortGroup:OptionROEHoldFirePossible() then local EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEHoldFire, "Holding weapons!" ) @@ -1055,8 +1051,7 @@ function AI_ESCORT:MenuROT( MenuTextFormat ) if EscortGroup:IsAir() then local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", EscortMenu ) + local EscortMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", EscortGroup.EscortMenu ) if not EscortGroup.EscortMenuEvasion then -- Reaction to Threats @@ -1092,8 +1087,7 @@ function AI_ESCORT:MenuResumeMission() function( EscortGroup ) if EscortGroup:IsAir() then local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - EscortGroup.EscortMenuResumeMission = MENU_GROUP:New( self.PlayerGroup, "Resume from", EscortMenu ) + EscortGroup.EscortMenuResumeMission = MENU_GROUP:New( self.PlayerGroup, "Resume from", EscortGroup.EscortMenu ) end end ) @@ -1435,7 +1429,7 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) ) Tasks[#Tasks+1] = EscortGroup:TaskCombo( AttackUnitTasks ) - Tasks[#Tasks+1] = EscortGroup:TaskFunction( "AI_ESCORT.___Resume", EscortGroup, self ) + Tasks[#Tasks+1] = EscortGroup:TaskFunction( "AI_ESCORT.___Resume", self ) EscortGroup:SetTask( EscortGroup:TaskCombo( @@ -1590,8 +1584,7 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) local TimeUpdate = timer.getTime() local EscortGroupName = EscortGroup:GetName() - local EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, self.MainMenu ) - local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortMenu ) + local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do @@ -1712,3 +1705,5 @@ function AI_ESCORT:_FlightReportTargetsScheduler() return false end + + diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index 4b95b4b90..6bbbe8576 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -231,22 +231,24 @@ function AI_ESCORT_REQUEST:SpawnEscort() EscortGroup:OptionROTVertical() EscortGroup:OptionROEHoldFire() - self:ScheduleOnce( 5, - function() + self:ScheduleOnce( 0.1, + function( EscortGroup ) local LeaderEscort = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP local Report = REPORT:New() Report:Add( "Joining Up " .. self.EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) - LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) + self:FormationTrail( 50, 50, 50 ) if self.SpawnMode == self.__Enum.Mode.Formation then self:JoinFormation( EscortGroup ) end --self:Menus( self.XStart, self.XSpace, self.YStart, self.YSpace, self.ZStart, self.ZSpace, self.ZLevels ) - + + EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetName(), self.MainMenu ) + self:EscortMenuJoinUp( EscortGroup ) self:EscortMenuHoldAtEscortPosition( EscortGroup ) @@ -261,7 +263,18 @@ function AI_ESCORT_REQUEST:SpawnEscort() self:MenuROT() self:MenuResumeMission() - end + + --- @param #AI_ESCORT self + -- @param Core.Event#EVENTDATA EventData + function EscortGroup:OnEventDeadOrCrash( EventData ) + self:F( { "EventDead", EventData } ) + self.EscortMenu:Remove() + end + + EscortGroup:HandleEvent( EVENTS.Dead, EscortGroup.OnEventDeadOrCrash ) + EscortGroup:HandleEvent( EVENTS.Crash, EscortGroup.OnEventDeadOrCrash ) + + end, EscortGroup ) end @@ -277,6 +290,9 @@ function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) end ) end + + self:HandleEvent( EVENTS.Dead, self.OnEventDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self.OnEventDeadOrCrash ) end From 7bae22248a4502666fc4cfbff998e1155625597d Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 1 Jun 2019 08:06:18 +0200 Subject: [PATCH 284/485] Optimizations ROE and ROT and flight modes. --- Moose Development/Moose/AI/AI_Escort.lua | 112 +++++++++++++----- .../Moose/AI/AI_Escort_Request.lua | 2 +- Moose Development/Moose/AI/AI_Formation.lua | 35 ++++-- 3 files changed, 111 insertions(+), 38 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 734b84a8f..ea5dccfe8 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -250,6 +250,8 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) EscortGroupSet:ForEachGroup( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) + EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetName(), self.MainMenu ) + -- Set EscortGroup known at EscortUnit. if not self.PlayerUnit._EscortGroups then self.PlayerUnit._EscortGroups = {} @@ -577,7 +579,7 @@ function AI_ESCORT:MenuJoinUp() self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - self:EscortSetMenuJoinUp( EscortGroup ) + self:EscortMenuJoinUp( EscortGroup ) end ) @@ -997,10 +999,10 @@ function AI_ESCORT:MenuROE( MenuTextFormat ) local FlightMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", self.FlightMenu ) - local FlightMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", FlightMenuROE, AI_ESCORT._ROE, self, GROUP.OptionROEHoldFire, "Holding weapons!" ) - local FlightMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", FlightMenuROE, AI_ESCORT._ROE, self, GROUP.OptionROEReturnFire, "Returning fire!" ) - local FlightMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", FlightMenuROE, AI_ESCORT._ROE, self, GROUP.OptionROEOpenFire, "Open fire at designated targets!" ) - local FlightMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", FlightMenuROE, AI_ESCORT._ROE, self, GROUP.OptionROEWeaponFree, "Engaging all targets!" ) + local FlightMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", FlightMenuROE, AI_ESCORT._FlightROEHoldFire, self, "Holding weapons!" ) + local FlightMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", FlightMenuROE, AI_ESCORT._FlightROEReturnFire, self, "Returning fire!" ) + local FlightMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", FlightMenuROE, AI_ESCORT._FlightROEOpenFire, self, "Open fire at designated targets!" ) + local FlightMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", FlightMenuROE, AI_ESCORT._FlightROEWeaponFree, self, "Engaging all targets!" ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup @@ -1012,16 +1014,16 @@ function AI_ESCORT:MenuROE( MenuTextFormat ) local EscortMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", EscortGroup.EscortMenu ) if EscortGroup:OptionROEHoldFirePossible() then - local EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEHoldFire, "Holding weapons!" ) + local EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEHoldFire, "Holding weapons!" ) end if EscortGroup:OptionROEReturnFirePossible() then - local EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEReturnFire, "Returning fire!" ) + local EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEReturnFire, "Returning fire!" ) end if EscortGroup:OptionROEOpenFirePossible() then - EscortGroup.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEOpenFire, "Opening fire on designated targets!!" ) + EscortGroup.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEOpenFire, "Opening fire on designated targets!!" ) end if EscortGroup:OptionROEWeaponFreePossible() then - EscortGroup.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, GROUP.OptionROEWeaponFree, "Opening fire on targets of opportunity!" ) + EscortGroup.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEWeaponFree, "Opening fire on targets of opportunity!" ) end end end @@ -1040,10 +1042,10 @@ function AI_ESCORT:MenuROT( MenuTextFormat ) local FlightMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", self.FlightMenu ) - local FlightMenuROTNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", FlightMenuROT, AI_ESCORT._ROT, self, GROUP.OptionROTNoReaction, "Fighting until death!" ) - local FlightMenuROTPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", FlightMenuROT, AI_ESCORT._ROT, self, GROUP.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) - local FlightMenuROTEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", FlightMenuROT, AI_ESCORT._ROT, self, GROUP.OptionROTEvadeFire, "Evading on enemy fire!" ) - local FlightMenuROTVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", FlightMenuROT, AI_ESCORT._ROT, self, GROUP.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) + local FlightMenuROTNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", FlightMenuROT, AI_ESCORT._FlightROTNoReaction, self, "Fighting until death!" ) + local FlightMenuROTPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", FlightMenuROT, AI_ESCORT._FlightROTPassiveDefense, self, "Defending using jammers, chaff and flares!" ) + local FlightMenuROTEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", FlightMenuROT, AI_ESCORT._FlightROTEvadeFire, self, "Evading on enemy fire!" ) + local FlightMenuROTVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", FlightMenuROT, AI_ESCORT._FlightROTVertical, self, "Evading on enemy fire with vertical manoeuvres!" ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup @@ -1056,16 +1058,16 @@ function AI_ESCORT:MenuROT( MenuTextFormat ) if not EscortGroup.EscortMenuEvasion then -- Reaction to Threats if EscortGroup:OptionROTNoReactionPossible() then - local EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTNoReaction, "Fighting until death!" ) + local EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTNoReaction, "Fighting until death!" ) end if EscortGroup:OptionROTPassiveDefensePossible() then - local EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) + local EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) end if EscortGroup:OptionROTEvadeFirePossible() then - local EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTEvadeFire, "Evading on enemy fire!" ) + local EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTEvadeFire, "Evading on enemy fire!" ) end if EscortGroup:OptionROTVerticalPossible() then - local EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, GROUP.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) + local EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) end end end @@ -1107,7 +1109,7 @@ function AI_ESCORT:_HoldPosition( OrbitGroup, EscortGroup, OrbitHeight, OrbitSec local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT - self:ReleaseFormation( EscortGroup ) + self:ModeMission( EscortGroup ) local PointFrom = {} local GroupVec3 = EscortGroup:GetUnit(1):GetVec3() @@ -1168,7 +1170,7 @@ function AI_ESCORT:_JoinUp( EscortGroup ) local EscortUnit = self.PlayerUnit - self:JoinFormation( EscortGroup ) + self:ModeFormation( EscortGroup ) EscortGroup:SetState( self, "Mode", self.__Enum.Mode.Follow ) end @@ -1372,7 +1374,6 @@ function AI_ESCORT.___Resume( EscortGroup, self ) local PlayerGroup = self.PlayerGroup - self:JoinFormation( EscortGroup ) EscortGroup:MessageTypeToGroup( "Destroyed all targets. Rejoining.", MESSAGE.Type.Information, PlayerGroup ) end @@ -1407,7 +1408,7 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) local EscortUnit = self.PlayerUnit - self:ReleaseFormation( EscortGroup ) + self:ModeMission( EscortGroup ) if EscortGroup:IsAir() then EscortGroup:OptionROEOpenFire() @@ -1512,36 +1513,89 @@ function AI_ESCORT:_AssistTarget( EscortGroup, DetectedItem ) end function AI_ESCORT:_ROE( EscortGroup, EscortROEFunction, EscortROEMessage ) - pcall( function() EscortROEFunction() end ) + pcall( function() EscortROEFunction( EscortGroup ) end ) EscortGroup:MessageTypeToGroup( EscortROEMessage, MESSAGE.Type.Information, self.PlayerGroup ) end -function AI_ESCORT:_FlightROE( EscortROEFunction, EscortROEMessage ) - self.EscortGroupSet:ForSomeGroupAlive( + +function AI_ESCORT:_FlightROEHoldFire( EscortROEMessage ) + self.EscortGroupSet:ForEachGroupAlive( --- @param Wrapper.Group#GROUP EscortGroup function( EscortGroup ) - self:_ROE( EscortGroup, EscortROEFunction, EscortROEMessage ) + self:_ROE( EscortGroup, EscortGroup.OptionROEHoldFire, EscortROEMessage ) + end + ) +end + +function AI_ESCORT:_FlightROEOpenFire( EscortROEMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROE( EscortGroup, EscortGroup.OptionROEOpenFire, EscortROEMessage ) + end + ) +end + +function AI_ESCORT:_FlightROEReturnFire( EscortROEMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROE( EscortGroup, EscortGroup.OptionROEReturnFire, EscortROEMessage ) + end + ) +end + +function AI_ESCORT:_FlightROEWeaponFree( EscortROEMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROE( EscortGroup, EscortGroup.OptionROEWeaponFree, EscortROEMessage ) end ) end function AI_ESCORT:_ROT( EscortGroup, EscortROTFunction, EscortROTMessage ) - pcall( function() EscortROTFunction() end ) + pcall( function() EscortROTFunction( EscortGroup ) end ) EscortGroup:MessageTypeToGroup( EscortROTMessage, MESSAGE.Type.Information, self.PlayerGroup ) end -function AI_ESCORT:_FlightROT( EscortROTFunction, EscortROTMessage ) - self.EscortGroupSet:ForSomeGroupAlive( +function AI_ESCORT:_FlightROTNoReaction( EscortROTMessage ) + self.EscortGroupSet:ForEachGroupAlive( --- @param Wrapper.Group#GROUP EscortGroup function( EscortGroup ) - self:_ROT( EscortGroup, EscortROTFunction, EscortROTMessage ) + self:_ROT( EscortGroup, EscortGroup.OptionROTNoReaction, EscortROTMessage ) end ) end +function AI_ESCORT:_FlightROTPassiveDefense( EscortROTMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROT( EscortGroup, EscortGroup.OptionROTPassiveDefense, EscortROTMessage ) + end + ) +end +function AI_ESCORT:_FlightROTEvadeFire( EscortROTMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROT( EscortGroup, EscortGroup.OptionROTEvadeFire, EscortROTMessage ) + end + ) +end + +function AI_ESCORT:_FlightROTVertical( EscortROTMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROT( EscortGroup, EscortGroup.OptionROTVertical, EscortROTMessage ) + end + ) +end --- Registers the waypoints -- @param #AI_ESCORT self diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index 6bbbe8576..faf0e817c 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -242,7 +242,7 @@ function AI_ESCORT_REQUEST:SpawnEscort() self:FormationTrail( 50, 50, 50 ) if self.SpawnMode == self.__Enum.Mode.Formation then - self:JoinFormation( EscortGroup ) + self:ModeFormation( EscortGroup ) end --self:Menus( self.XStart, self.XSpace, self.YStart, self.YSpace, self.ZStart, self.ZSpace, self.ZLevels ) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index f4ad1902f..31930c045 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -950,33 +950,52 @@ function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 end ---- This releases the air unit in your flight from the formation flight. + +--- This sets your escorts to fly a mission. -- @param #AI_FORMATION self -- @param Wrapper.Group#GROUP FollowGroup FollowGroup. -- @return #AI_FORMATION -function AI_FORMATION:ReleaseFormation( FollowGroup ) +function AI_FORMATION:ModeMission( FollowGroup ) - FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) + if FollowGroup then + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) + else + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( FollowGroup ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) + end + ) + end + return self end ---- This joins up the air unit in your formation flight. +--- This sets your escorts to fly in a formation. -- @param #AI_FORMATION self -- @param Wrapper.Group#GROUP FollowGroup FollowGroup. -- @return #AI_FORMATION -function AI_FORMATION:JoinFormation( FollowGroup ) +function AI_FORMATION:ModeFormation( FollowGroup ) - -- If a formation type was defined for the AI_FORMATION object, then we can joinup. - - FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) + if FollowGroup then + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) + else + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( FollowGroup ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) + end + ) + end return self end + --- Stop function. Formation will not be updated any more. -- @param #AI_FORMATION self -- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. From 1627c92460fa0f8a7cfa377326dc57721e6db8ea Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 1 Jun 2019 23:25:44 +0200 Subject: [PATCH 285/485] Airboss v1.0.1 --- Moose Development/Moose/Ops/Airboss.lua | 40 +++++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 24a36a7c8..e0337906c 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -207,6 +207,7 @@ -- @field Core.Set#SET_GROUP excludesetAI AI groups in this set will be explicitly excluded from handling by the airboss and not forced into the Marshal pattern. -- @field #boolean menusingle If true, menu is optimized for a single carrier. -- @field #number collisiondist Distance up to which collision checks are done. +-- @field #nubmer holdtimestamp Timestamp when the carrier first came to an unexpected hold. -- @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. -- @field #string soundfolderLSO Folder withing the mission (miz) file where LSO sound files are stored. @@ -271,7 +272,7 @@ -- 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. Note that you need to be below 1300 feet to be registered in the initial zone. -- The altitude can be set via the function @{AIRBOSS.SetInitialMaxAlt}(*altitude*) function. --- As described belwo, the initial zone can be smoked or flared via the AIRBOSS F10 Help radio menu. +-- As described below, the initial zone can be smoked or flared via the AIRBOSS F10 Help radio menu. -- -- ### Landing Pattern -- @@ -1208,6 +1209,7 @@ AIRBOSS = { excludesetAI = nil, menusingle = nil, collisiondist = nil, + holdtimestamp = nil, Tmessage = nil, soundfolder = nil, soundfolderLSO = nil, @@ -1673,7 +1675,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.0.0" +AIRBOSS.version="1.0.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -3227,13 +3229,36 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Current heading and position of the carrier. local hdg=self:GetHeading() local pos=self:GetCoordinate() + local speed=self.carrier:GetVelocityKNOTS() -- Check water is ahead. local collision=self:_CheckCollisionCoord(pos:Translate(self.collisiondist, hdg)) + + local holdtime=0 + if self.holdtimestamp then + holdtime=timer.getTime()-self.holdtimestamp + end + + -- Check if carrier is stationary. + local NextWP=self:_GetNextWaypoint() + local ExpectedSpeed=UTILS.MpsToKnots(NextWP:GetVelocity()) + if speed<0.5 and ExpectedSpeed>0 and not (self.detour or self.turnintowind) then + if not self.holdtimestamp then + self:E(self.lid.."Carrier came to an unexpected standstill. Trying to re-route in 3 min.") + self.holdtimestamp=timer.getTime() + else + if holdtime>3*60 then + local coord=self:GetCoordinate():Translate(500, hdg+10) + --coord:MarkToAll("Re-route after standstill.") + self:CarrierResumeRoute(coord) + self.holdtimestamp=nil + end + end + end -- Debug info. - local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s - Turning=%s - Collision Warning=%s", - clock, self:GetState(), self.case, self.carrier:GetVelocityKNOTS(), hdg, self.currentwp, eta, tostring(self.turning), tostring(collision)) + local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s - Turning=%s - Collision Warning=%s - Detour=%s - Turn Into Wind=%s - Holdtime=%d sec", + clock, self:GetState(), self.case, speed, hdg, self.currentwp, eta, tostring(self.turning), tostring(collision), tostring(self.detour), tostring(self.turnintowind), holdtime) self:T(self.lid..text) -- Players online: @@ -13130,7 +13155,7 @@ end --- Get next waypoint of the carrier. -- @param #AIRBOSS self -- @return Core.Point#COORDINATE Coordinate of the next waypoint. --- @return #number Number of waypoint +-- @return #number Number of waypoint. function AIRBOSS:_GetNextWaypoint() -- Next waypoint. @@ -13459,6 +13484,7 @@ function AIRBOSS._ResumeRoute(group, airboss, gotocoord) -- First goto this coordinate. if gotocoord then + --gotocoord:MarkToAll(string.format("Goto waypoint speed=%.1f km/h", speedkmh)) local wp1=gotocoord:WaypointGround(speedkmh) table.insert(waypoints, wp1) end @@ -13468,7 +13494,7 @@ function AIRBOSS._ResumeRoute(group, airboss, gotocoord) -- Debug message. MESSAGE:New(text,10):ToAllIf(airboss.Debug) - airboss:I(airboss.lid..text) + airboss:I(airboss.lid..text) -- Loop over all remaining waypoints. for i=Nextwp, #airboss.waypoints do @@ -13484,6 +13510,8 @@ function AIRBOSS._ResumeRoute(group, airboss, gotocoord) speed=UTILS.KnotsToKmph(10) end + --coord:MarkToAll(string.format("Resume route WP %d, speed=%.1f km/h", i, speed)) + -- Create waypoint. local wp=coord:WaypointGround(speed) From 9fed91b16f2a1ee87fac6f8a961e3096bef7fd13 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 2 Jun 2019 23:12:08 +0200 Subject: [PATCH 286/485] AIRBOSS v1.0.2 - Added skipper menu. - Added turn help waypoints. --- Moose Development/Moose/Ops/Airboss.lua | 232 +++++++++++++++++++++++- 1 file changed, 229 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index e0337906c..654f9d22e 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -229,6 +229,11 @@ -- @field #string trapprefix File prefix for trap sheet files. -- @field #number initialmaxalt Max altitude in meters to register in the inital zone. -- @field #boolean welcome If true, display welcome message to player. +-- @field #boolean skipperMenu If true, add skipper menu. +-- @field #number skipperSpeed Speed in knots for manual recovery start. +-- @field #number skipperCase Manual recovery case. +-- @field #boolean skipperUturn U-turn on/off via menu. +-- @field #number skipperTime Recovery time in min for manual recovery. -- @extends Core.Fsm#FSM --- Be the boss! @@ -1229,6 +1234,10 @@ AIRBOSS = { trapprefix = nil, initialmaxalt = nil, welcome = nil, + skipperMenu = nil, + skipperSpeed = nil, + skipperTime = nil, + skipperUturn = nil, } --- Aircraft types capable of landing on carrier (human+AI). @@ -1675,7 +1684,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.0.1" +AIRBOSS.version="1.0.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -2312,6 +2321,26 @@ function AIRBOSS:SetHoldingOffsetAngle(offset) return self end +--- Enable F10 menu to manually start recoveries. +-- @param #AIRBOSS self +-- @param #number duration Default duration of the recovery in minutes. Default 30 min. +-- @param #number windondeck Default wind on deck in knots. Default 25 knots. +-- @param #boolean uturn U-turn after recovery window closes on=true or off=false/nil. Default off. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuRecovery(duration, windondeck, uturn) + + self.skipperMenu=true + self.skipperTime=duration or 30 + self.skipperSpeed=windondeck or 25 + if uturn then + self.skipperUturn=true + else + self.skipperUturn=false + end + + return self +end + --- Add aircraft recovery time window and recovery case. -- @param #AIRBOSS self -- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. @@ -13478,15 +13507,46 @@ function AIRBOSS._ResumeRoute(group, airboss, gotocoord) -- Waypoints array. local waypoints={} + -- Current position. + local c0=group:GetCoordinate() + -- Current positon as first waypoint. - local wp0=group:GetCoordinate():WaypointGround(speedkmh) + local wp0=c0:WaypointGround(speedkmh) table.insert(waypoints, wp0) -- First goto this coordinate. if gotocoord then + --gotocoord:MarkToAll(string.format("Goto waypoint speed=%.1f km/h", speedkmh)) + + local headingto=c0:HeadingTo(gotocoord) + + local hdg1=airboss:GetHeading() + local hdg2=c0:HeadingTo(gotocoord) + local delta=airboss:_GetDeltaHeading(hdg1, hdg2) + + --env.info(string.format("FF hdg1=%d, hdg2=%d, delta=%d", hdg1, hdg2, delta)) + + + if delta>90 then + + local gotocoordh=c0:Translate(2500, hdg1+45) + --gotocoordh:MarkToAll(string.format("Goto help waypoint 1 speed=%.1f km/h", speedkmh)) + + local wp=gotocoordh:WaypointGround(speedkmh) + table.insert(waypoints, wp) + + gotocoordh=c0:Translate(5000, hdg1+90) + --gotocoordh:MarkToAll(string.format("Goto help waypoint 2 speed=%.1f km/h", speedkmh)) + + wp=gotocoordh:WaypointGround(speedkmh) + table.insert(waypoints, wp) + + end + local wp1=gotocoord:WaypointGround(speedkmh) - table.insert(waypoints, wp1) + table.insert(waypoints, wp1) + end -- Debug message. @@ -15280,6 +15340,29 @@ function AIRBOSS:_AddF10Commands(_unitName) 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//F2 Kneeboard/F2 Skipper/ + if self.skipperMenu then + local _skipperPath =missionCommands.addSubMenuForGroup(gid, "Skipper", _kneeboardPath) + local _menusetspeed=missionCommands.addSubMenuForGroup(gid, "Set Speed", _skipperPath) + missionCommands.addCommandForGroup(gid, "10 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 10) + missionCommands.addCommandForGroup(gid, "15 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 15) + missionCommands.addCommandForGroup(gid, "20 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 20) + missionCommands.addCommandForGroup(gid, "25 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 25) + missionCommands.addCommandForGroup(gid, "30 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 30) + local _menusetrtime=missionCommands.addSubMenuForGroup(gid, "Set Time", _skipperPath) + missionCommands.addCommandForGroup(gid, "15 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 15) + missionCommands.addCommandForGroup(gid, "30 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 30) + missionCommands.addCommandForGroup(gid, "45 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 45) + missionCommands.addCommandForGroup(gid, "60 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 60) + missionCommands.addCommandForGroup(gid, "90 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 90) + missionCommands.addCommandForGroup(gid, "U-turn On/Off", _skipperPath, self._SkipperRecoveryUturn, self, _unitName) + missionCommands.addCommandForGroup(gid, "Start CASE I", _skipperPath, self._SkipperStartRecovery, self, _unitName, 1) + missionCommands.addCommandForGroup(gid, "Start CASE II", _skipperPath, self._SkipperStartRecovery, self, _unitName, 2) + missionCommands.addCommandForGroup(gid, "Start CASE III",_skipperPath, self._SkipperStartRecovery, self, _unitName, 3) + missionCommands.addCommandForGroup(gid, "Stop Recovery", _skipperPath, self._SkipperStopRecovery, self, _unitName) + end + -- F10/Airboss/ Date: Mon, 3 Jun 2019 22:58:53 +0200 Subject: [PATCH 287/485] minor --- Moose Development/Moose/Ops/Airboss.lua | 10 +++++++--- Moose Development/Moose/Ops/RecoveryTanker.lua | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 654f9d22e..6d0d68c09 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -3273,7 +3273,7 @@ function AIRBOSS:onafterStatus(From, Event, To) local ExpectedSpeed=UTILS.MpsToKnots(NextWP:GetVelocity()) if speed<0.5 and ExpectedSpeed>0 and not (self.detour or self.turnintowind) then if not self.holdtimestamp then - self:E(self.lid.."Carrier came to an unexpected standstill. Trying to re-route in 3 min.") + self:E(self.lid..string.format("Carrier came to an unexpected standstill. Trying to re-route in 3 min. Speed=%.1f knots, expected=%.1f knots", speed, ExpectedSpeed)) self.holdtimestamp=timer.getTime() else if holdtime>3*60 then @@ -13528,15 +13528,19 @@ function AIRBOSS._ResumeRoute(group, airboss, gotocoord) --env.info(string.format("FF hdg1=%d, hdg2=%d, delta=%d", hdg1, hdg2, delta)) + -- Add additional turn points if delta>90 then + + -- Turn radius 3 NM. + local turnradius=UTILS.NMToMeters(3) - local gotocoordh=c0:Translate(2500, hdg1+45) + local gotocoordh=c0:Translate(turnradius, hdg1+45) --gotocoordh:MarkToAll(string.format("Goto help waypoint 1 speed=%.1f km/h", speedkmh)) local wp=gotocoordh:WaypointGround(speedkmh) table.insert(waypoints, wp) - gotocoordh=c0:Translate(5000, hdg1+90) + gotocoordh=c0:Translate(turnradius, hdg1+90) --gotocoordh:MarkToAll(string.format("Goto help waypoint 2 speed=%.1f km/h", speedkmh)) wp=gotocoordh:WaypointGround(speedkmh) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 1ad33047f..3c4d781dc 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -1491,7 +1491,7 @@ function RECOVERYTANKER:_ActivateTACAN(delay) local unit=self.tanker:GetUnit(1) -- Check if unit is alive. - if unit:IsAlive() then + if unit and unit:IsAlive() then -- Debug message. local text=string.format("Activating recovery tanker TACAN beacon: channel=%d mode=%s, morse=%s.", self.TACANchannel, self.TACANmode, self.TACANmorse) From 94f9cb4a79a3bff5678ff7518f126e9a119baebe Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 3 Jun 2019 23:18:54 +0200 Subject: [PATCH 288/485] Update Airboss.lua --- Moose Development/Moose/Ops/Airboss.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 6d0d68c09..e6b13ad69 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -76,6 +76,7 @@ -- * [[MOOSE] Airboss - CASE I Walkthrough in the F/A-18C by TG](https://www.youtube.com/watch?v=o1UrP4Q6PMM) -- * [[MOOSE] Airboss - New LSO/Marshal Voice Overs by Raynor](https://www.youtube.com/watch?v=_Suo68bRu8k) -- * [[MOOSE] Airboss - CASE I, "Until We Go Down" featuring the F-14B by Pikes](https://www.youtube.com/watch?v=ojgHDSw3Doc) +-- * [[MOOSE] Airboss - Skipper Menu](https://youtu.be/awnecCxRoNQ) -- -- ### Lex explaining Boat Ops: -- From bc229f1a8668d03e813e31e8aac208f2ec0bf4d1 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 4 Jun 2019 17:36:23 +0200 Subject: [PATCH 289/485] Update Warehouse.lua --- Moose Development/Moose/Functional/Warehouse.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 1f6b3d441..802e22275 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1743,7 +1743,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.9.1" +WAREHOUSE.version="0.9.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. From 10bfefdd06872e8c715dc4fc5e384e453f241697 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 8 Jun 2019 23:06:45 +0200 Subject: [PATCH 290/485] pseudoATC - Fixed bug for ReportBR. --- Moose Development/Moose/Functional/PseudoATC.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua index 17f30ac98..1143b00e8 100644 --- a/Moose Development/Moose/Functional/PseudoATC.lua +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -98,7 +98,7 @@ PSEUDOATC.id="PseudoATC | " --- PSEUDOATC version. -- @field #number version -PSEUDOATC.version="0.9.1" +PSEUDOATC.version="0.9.2" ----------------------------------------------------------------------------------------------------------------------------------------- @@ -807,8 +807,9 @@ function PSEUDOATC:ReportBR(GID, UID, position, location) -- Message text. local text=string.format("%s: Bearing %s, Range %s.", location, Bs, Rs) - -- Send message to player group. - MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) + -- Send message + self:_DisplayMessageToGroup(self.group[GID].player[UID].unit, text, self.mdur, true) + end --- Report altitude above ground level of player unit. From 0a027caa4747b0aa0ff56ba38495c14249e4f2a6 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 15 Jun 2019 12:47:24 +0200 Subject: [PATCH 291/485] Test --- Moose Development/Moose/AI/AI_Escort.lua | 79 ++++++++++++++++++++---- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index ea5dccfe8..49542b3ae 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -151,6 +151,10 @@ --- @type AI_ESCORT -- @extends AI.AI_Formation#AI_FORMATION + +-- TODO: Add the menus when the class Start method is activated. +-- TODO: Remove the menus when the class Stop method is called. + --- AI_ESCORT class -- -- # AI_ESCORT construction methods. @@ -323,7 +327,7 @@ function AI_ESCORT:TestSmokeDirectionVector( SmokeDirection ) end ---- Defines the default menus +--- Defines the default menus for helicopters. -- @param #AI_ESCORT 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. @@ -333,7 +337,60 @@ end -- @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 #AI_ESCORT -function AI_ESCORT:Menus( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) +function AI_ESCORT:MenusHelicopter( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) + self:F() + +-- self:MenuScanForTargets( 100, 60 ) + + self.XStart = XStart or self.XStart + self.XSpace = XSpace or self.XSpace + self.YStart = YStart or self.YStart + self.YSpace = YSpace or self.YSpace + self.ZStart = ZStart or self.ZStart + self.ZSpace = ZSpace or self.ZSpace + self.ZLevels = ZLevels or self.ZLevels + + self:MenuJoinUp() + self:MenuFormationTrail(self.XStart,self.XSpace,self.YStart) + self:MenuFormationStack(self.XStart,self.XSpace,self.YStart,self.YSpace) + self:MenuFormationLeftLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationRightLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationLeftWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationRightWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationVic(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace) + self:MenuFormationBox(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace,self.ZLevels) + + self:MenuHoldAtEscortPosition( 30 ) + self:MenuHoldAtEscortPosition( 100 ) + self:MenuHoldAtEscortPosition( 500 ) + self:MenuHoldAtLeaderPosition( 30, 500 ) + + self:MenuFlare() + self:MenuSmoke() + + self:MenuReportTargets( 60 ) + self:MenuAssistedAttack() + self:MenuROE() + self:MenuROT() + + self:MenuResumeMission() + + + return self +end + + +--- Defines the default menus for airplanes. +-- @param #AI_ESCORT 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. +-- @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. +-- @param #number ZLevels The amount of levels on the Z-axis. +-- @return #AI_ESCORT +function AI_ESCORT:MenusAirplanes( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) self:F() -- self:MenuScanForTargets( 100, 60 ) @@ -940,7 +997,7 @@ function AI_ESCORT:MenuReportTargets( Seconds ) -- Attack Targets local FlightMenuAttackNearbyTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", self.FlightMenu ) - self.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, 5, Seconds ) + self.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, Seconds, Seconds ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup @@ -1150,7 +1207,7 @@ function AI_ESCORT:_FlightHoldPosition( OrbitGroup, OrbitHeight, OrbitSeconds ) local EscortUnit = self.PlayerUnit - self.EscortGroupSet:ForSomeGroupAlive( + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup, OrbitGroup ) if EscortGroup:IsAir() then @@ -1178,7 +1235,7 @@ end function AI_ESCORT:_FlightJoinUp() - self.EscortGroupSet:ForSomeGroupAlive( + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1205,7 +1262,7 @@ end function AI_ESCORT:_FlightFormationTrail( XStart, XSpace, YStart ) - self.EscortGroupSet:ForSomeGroupAlive( + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1232,7 +1289,7 @@ end function AI_ESCORT:_FlightFormationStack( XStart, XSpace, YStart, YSpace ) - self.EscortGroupSet:ForSomeGroupAlive( + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1255,7 +1312,7 @@ end function AI_ESCORT:_FlightFlare( Color, Message ) - self.EscortGroupSet:ForSomeGroupAlive( + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1278,7 +1335,7 @@ end function AI_ESCORT:_FlightSmoke( Color, Message ) - self.EscortGroupSet:ForSomeGroupAlive( + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1325,7 +1382,7 @@ end function AI_ESCORT:_FlightSwitchReportNearbyTargets( ReportTargets ) - self.EscortGroupSet:ForSomeGroupAlive( + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then @@ -1468,7 +1525,7 @@ end function AI_ESCORT:_FlightAttackTarget( DetectedItem ) - self.EscortGroupSet:ForSomeGroupAlive( + self.EscortGroupSet:ForEachGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup, DetectedItem ) if EscortGroup:IsAir() then From d34cff16755d642877b45d7829263a8c47d87db1 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 17 Jun 2019 09:00:45 +0200 Subject: [PATCH 292/485] Temporary --- Moose Development/Moose/AI/AI_Escort.lua | 520 ++++++++++++++--------- 1 file changed, 319 insertions(+), 201 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 49542b3ae..b81ebbb02 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -301,6 +301,43 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) self:HandleEvent( EVENTS.Dead, OnEventDeadOrCrash ) self:HandleEvent( EVENTS.Crash, OnEventDeadOrCrash ) + + self:SetFlightMenuFormation( "Trail" ) + self:SetFlightMenuFormation( "Stack" ) + self:SetFlightMenuFormation( "LeftLine" ) + self:SetFlightMenuFormation( "RightLine" ) + self:SetFlightMenuFormation( "LeftWing" ) + self:SetFlightMenuFormation( "RightWing" ) + self:SetFlightMenuFormation( "Vic" ) + self:SetFlightMenuFormation( "Box" ) + + self:SetFlightMenuHoldAtEscortPosition() + self:SetFlightMenuHoldAtLeaderPosition() + + self:SetFlightMenuFlare() + self:SetFlightMenuSmoke() + + self:SetFlightMenuROE() + self:SetFlightMenuROT() + + self:SetFlightMenuTargets() + + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + self:SetEscortMenuHoldAtEscortPosition( EscortGroup ) + self:SetEscortMenuHoldAtLeaderPosition( EscortGroup ) + + self:SetEscortMenuFlare( EscortGroup ) + self:SetEscortMenuSmoke( EscortGroup ) + + self:SetEscortMenuROE( EscortGroup ) + self:SetEscortMenuROT( EscortGroup ) + + self:SetEscortMenuTargets( EscortGroup ) + end + ) + end @@ -431,28 +468,40 @@ function AI_ESCORT:MenusAirplanes( XStart, XSpace, YStart, YSpace, ZStart, ZSpac end +function AI_ESCORT:SetFlightMenuFormation( Formation ) -function AI_ESCORT:MenuFormation( Formation, ... ) + local FormationID = "Formation" .. Formation + + local MenuFormation = self.Menu[FormationID] - if not self.FlightMenuFormation then - self.FlightMenuFormation = MENU_GROUP:New( self.PlayerGroup, "Formation", self.MainMenu ) - end - - if not self["FlightMenuFormation"..Formation] then - self["FlightMenuFormation"..Formation] = MENU_GROUP_COMMAND:New( self.PlayerGroup, Formation, self.FlightMenuFormation, + if MenuFormation then + local Arguments = MenuFormation.Arguments + local FlightMenuFormation = MENU_GROUP:New( self.PlayerGroup, "Formation", self.MainMenu ) + local MenuFlightFormationID = MENU_GROUP_COMMAND:New( self.PlayerGroup, Formation, FlightMenuFormation, function ( self, Formation, ... ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup, self, Formation, ... ) if EscortGroup:IsAir() then - self:E({Formation=Formation}) - self["Formation"..Formation]( self, ... ) + self:E({FormationID=FormationID}) + self[FormationID]( self, ... ) end end, self, Formation, ... ) - end, self, Formation, ... + end, self, Formation, Arguments ) end + + return self +end + + +function AI_ESCORT:MenuFormation( Formation, ... ) + + local FormationID = "Formation"..Formation + self.Menu[FormationID] = self.Menu[FormationID] or {} + self.Menu[FormationID][#self.Menu[FormationID]+1] = {} + self.Menu[FormationID][#self.Menu[FormationID]+1].Arguments = ... end @@ -644,7 +693,28 @@ function AI_ESCORT:MenuJoinUp() end -function AI_ESCORT:EscortMenuHoldAtEscortPosition( EscortGroup ) +function AI_ESCORT:SetFlightMenuHoldAtEscortPosition() + + for _, MenuHoldAtEscortPosition in pairs( self.Menu.HoldAtEscortPosition ) do + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + + local FlightMenuHoldPosition = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + MenuHoldAtEscortPosition.MenuText, + FlightMenuReportNavigation, + AI_ESCORT._FlightHoldPosition, + self, + nil, + MenuHoldAtEscortPosition.Height, + MenuHoldAtEscortPosition.Speed + ) + + end + return self +end + +function AI_ESCORT:SetEscortMenuHoldAtEscortPosition( EscortGroup ) for _, HoldAtEscortPosition in pairs( self.Menu.HoldAtEscortPosition ) do if EscortGroup:IsAir() then @@ -702,38 +772,38 @@ function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) end end - local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) - - local FlightMenuHoldPosition = MENU_GROUP_COMMAND - :New( - self.PlayerGroup, - MenuText, - FlightMenuReportNavigation, - AI_ESCORT._FlightHoldPosition, - self, - nil, - Height, - Speed - ) - self.Menu.HoldAtEscortPosition = self.Menu.HoldAtEscortPosition or {} self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition+1] = {} self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].Height = Height self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].Speed = Speed self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].MenuText = MenuText - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - self:EscortMenuHoldAtEscortPosition( EscortGroup ) - end - ) - return self end -function AI_ESCORT:EscortMenuHoldAtLeaderPosition( EscortGroup ) +function AI_ESCORT:SetFlightMenuHoldAtLeaderPosition() + + for _, MenuHoldAtLeaderPosition in pairs( self.Menu.HoldAtLeaderPosition ) do + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + + local FlightMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + MenuHoldAtLeaderPosition.MenuText, + FlightMenuReportNavigation, + AI_ESCORT._FlightHoldPosition, + self, + self.PlayerGroup, + MenuHoldAtLeaderPosition.Height, + MenuHoldAtLeaderPosition.Speed + ) + end + + return self +end + +function AI_ESCORT:SetEscortMenuHoldAtLeaderPosition( EscortGroup ) for _, HoldAtLeaderPosition in pairs( self.Menu.HoldAtLeaderPosition ) do if EscortGroup:IsAir() then @@ -792,33 +862,12 @@ function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) end end - local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) - - local FlightMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND - :New( - self.PlayerGroup, - MenuText, - FlightMenuReportNavigation, - AI_ESCORT._FlightHoldPosition, - self, - self.PlayerGroup, - Height, - Speed - ) - self.Menu.HoldAtLeaderPosition = self.Menu.HoldAtLeaderPosition or {} self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition+1] = {} self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].Height = Height self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].Speed = Speed self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].MenuText = MenuText - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - self:EscortMenuHoldAtLeaderPosition( EscortGroup ) - end - ) - return self end @@ -879,6 +928,41 @@ function AI_ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) end +function AI_ESCORT:SetFlightMenuFlare() + + for _, MenuFlare in pairs( self.Menu.Flare) do + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuText, FlightMenuReportNavigation ) + + local FlightMenuFlareGreenFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Green, "Released a green flare!" ) + local FlightMenuFlareRedFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Red, "Released a red flare!" ) + local FlightMenuFlareWhiteFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.White, "Released a white flare!" ) + local FlightMenuFlareYellowFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release yellow flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) + end + + return self +end + +function AI_ESCORT:SetEscortMenuFlare( EscortGroup ) + + for _, MenuFlare in pairs( self.Menu.Flare) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) + local EscortMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuFlare.MenuText, EscortMenuReportNavigation ) + + local EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green, "Released a green flare!" ) + local EscortMenuFlareRed = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Red, "Released a red flare!" ) + local EscortMenuFlareWhite = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.White, "Released a white flare!" ) + local EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release yellow flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Yellow, "Released a yellow flare!" ) + end + end + + return self +end + + --- Defines a menu slot to let the escort disperse a flare in a certain color. -- This menu will appear under **Navigation**. @@ -896,32 +980,52 @@ function AI_ESCORT:MenuFlare( MenuTextFormat ) MenuText = MenuTextFormat end - local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) - local FlightMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuText, FlightMenuReportNavigation ) - - local FlightMenuFlareGreenFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Green, "Released a green flare!" ) - local FlightMenuFlareRedFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Red, "Released a red flare!" ) - local FlightMenuFlareWhiteFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.White, "Released a white flare!" ) - local FlightMenuFlareYellowFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release yellow flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) - - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - - local EscortGroupName = EscortGroup:GetName() - local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) - local EscortMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuText, EscortMenuReportNavigation ) - - local EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green, "Released a green flare!" ) - local EscortMenuFlareRed = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Red, "Released a red flare!" ) - local EscortMenuFlareWhite = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.White, "Released a white flare!" ) - local EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release yellow flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Yellow, "Released a yellow flare!" ) - end - ) + self.Menu.Flare = self.Menu.Flare or {} + self.Menu.Flare[#self.Menu.Flare+1] = {} + self.Menu.Flare[#self.Menu.Flare].MenuText = MenuText return self end + +function AI_ESCORT:SetFlightMenuSmoke() + + for _, MenuSmoke in pairs( self.Menu.Smoke) do + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuText, FlightMenuReportNavigation ) + + local FlightMenuSmokeGreenFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) + local FlightMenuSmokeRedFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) + local FlightMenuSmokeWhiteFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) + local FlightMenuSmokeOrangeFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release orange smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + local FlightMenuSmokeBlueFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release blue smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) + end + + return self +end + + +function AI_ESCORT:SetEscortMenuSmoke( EscortGroup ) + + for _, MenuSmoke in pairs( self.Menu.Smoke) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) + local EscortMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuSmoke.MenuText, EscortMenuReportNavigation ) + + local EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green, "Releasing green smoke!" ) + local EscortMenuSmokeRed = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Red, "Releasing red smoke!" ) + local EscortMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.White, "Releasing white smoke!" ) + local EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release orange smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + local EscortMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release blue smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Blue, "Releasing blue smoke!" ) + end + end + + return self +end + + --- Defines a menu slot to let the escort disperse a smoke in a certain color. -- This menu will appear under **Navigation**. -- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. @@ -939,89 +1043,75 @@ function AI_ESCORT:MenuSmoke( MenuTextFormat ) MenuText = MenuTextFormat end - local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) - local FlightMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuText, FlightMenuReportNavigation ) - - local FlightMenuSmokeGreenFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) - local FlightMenuSmokeRedFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) - local FlightMenuSmokeWhiteFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) - local FlightMenuSmokeOrangeFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release orange smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) - local FlightMenuSmokeBlueFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release blue smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) - - - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if not EscortGroup:IsAir() then - - local EscortGroupName = EscortGroup:GetName() - local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) - local EscortMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuText, EscortMenuReportNavigation ) - - local EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green, "Releasing green smoke!" ) - local EscortMenuSmokeRed = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Red, "Releasing red smoke!" ) - local EscortMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.White, "Releasing white smoke!" ) - local EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release orange smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Orange, "Releasing orange smoke!" ) - local EscortMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release blue smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Blue, "Releasing blue smoke!" ) - end - end - ) + self.Menu.Smoke = self.Menu.Smoke or {} + self.Menu.Smoke[#self.Menu.Smoke+1] = {} + self.Menu.Smoke[#self.Menu.Smoke].MenuText = MenuText return self end + +function AI_ESCORT:SetFlightMenuTargets() + + for _, MenuTargets in pairs( self.Menu.Targets) do + local FlightMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", self.FlightMenu ) + + -- Report Targets + local FlightMenuReportTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", FlightMenuReportTargets, AI_ESCORT._FlightReportNearbyTargetsNow, self ) + local FlightMenuReportTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, true ) + local FlightMenuReportTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, false ) + + -- Attack Targets + local FlightMenuAttackNearbyTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", self.FlightMenu ) + + MenuTargets.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, MenuTargets.Interval, MenuTargets.Interval ) + end + + return self +end + + +function AI_ESCORT:SetEscortMenuTargets( EscortGroup ) + + for _, MenuTargets in pairs( self.Menu.Targets) do + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu ) + + -- Report Targets + --EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) + --EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) + --EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) + + -- Attack Targets + local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) + + EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, timer, MenuTargets.Interval ) + end + end + + return self +end + + + --- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. -- This menu will appear under **Report targets**. -- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. -- @param #AI_ESCORT self -- @param DCS#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. -- @return #AI_ESCORT -function AI_ESCORT:MenuReportTargets( Seconds ) +function AI_ESCORT:MenuTargets( Seconds ) self:F( { Seconds } ) if not Seconds then Seconds = 30 end - self.FlightMenuReportTargetsInterval = Seconds + self.Menu.Targets = self.Menu.Targets or {} + self.Menu.Targets[#self.Menu.Targets+1] = {} + self.Menu.Targets[#self.Menu.Targets].Interval = Seconds - local timer = 1 - - local FlightMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", self.FlightMenu ) - - -- Report Targets - local FlightMenuReportTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", FlightMenuReportTargets, AI_ESCORT._FlightReportNearbyTargetsNow, self ) - local FlightMenuReportTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, true ) - local FlightMenuReportTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, false ) - - -- Attack Targets - local FlightMenuAttackNearbyTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", self.FlightMenu ) - - self.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, Seconds, Seconds ) - - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if EscortGroup:IsAir() then - - local EscortGroupName = EscortGroup:GetName() - local EscortMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu ) - - - -- Report Targets - --EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) - --EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) - --EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) - - -- Attack Targets - local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) - - EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, timer, Seconds ) - timer=timer+1 - end - end - ) - return self end @@ -1047,49 +1137,108 @@ function AI_ESCORT:MenuAssistedAttack() return self end +function AI_ESCORT:SetFlightMenuROE() + + for _, MenuROE in pairs( self.Menu.ROE) do + local FlightMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", self.FlightMenu ) + + local FlightMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", FlightMenuROE, AI_ESCORT._FlightROEHoldFire, self, "Holding weapons!" ) + local FlightMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", FlightMenuROE, AI_ESCORT._FlightROEReturnFire, self, "Returning fire!" ) + local FlightMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", FlightMenuROE, AI_ESCORT._FlightROEOpenFire, self, "Open fire at designated targets!" ) + local FlightMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", FlightMenuROE, AI_ESCORT._FlightROEWeaponFree, self, "Engaging all targets!" ) + end + + return self +end + + +function AI_ESCORT:SetEscortMenuROE( EscortGroup ) + + for _, MenuROE in pairs( self.Menu.ROE) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", EscortGroup.EscortMenu ) + + if EscortGroup:OptionROEHoldFirePossible() then + local EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEHoldFire, "Holding weapons!" ) + end + if EscortGroup:OptionROEReturnFirePossible() then + local EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEReturnFire, "Returning fire!" ) + end + if EscortGroup:OptionROEOpenFirePossible() then + EscortGroup.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEOpenFire, "Opening fire on designated targets!!" ) + end + if EscortGroup:OptionROEWeaponFreePossible() then + EscortGroup.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEWeaponFree, "Opening fire on targets of opportunity!" ) + end + end + end + + return self +end + + --- Defines a menu to let the escort set its rules of engagement. -- All rules of engagement will appear under the menu **ROE**. -- @param #AI_ESCORT self -- @return #AI_ESCORT -function AI_ESCORT:MenuROE( MenuTextFormat ) - self:F( MenuTextFormat ) +function AI_ESCORT:MenuROE() + self:F() - local FlightMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", self.FlightMenu ) + self.Menu.ROE = self.Menu.ROE or {} + self.Menu.ROE[#self.Menu.ROE+1] = {} - local FlightMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", FlightMenuROE, AI_ESCORT._FlightROEHoldFire, self, "Holding weapons!" ) - local FlightMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", FlightMenuROE, AI_ESCORT._FlightROEReturnFire, self, "Returning fire!" ) - local FlightMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", FlightMenuROE, AI_ESCORT._FlightROEOpenFire, self, "Open fire at designated targets!" ) - local FlightMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", FlightMenuROE, AI_ESCORT._FlightROEWeaponFree, self, "Engaging all targets!" ) + return self +end - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if not EscortGroup.EscortMenuROE then - -- Rules of Engagement - - local EscortGroupName = EscortGroup:GetName() - local EscortMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", EscortGroup.EscortMenu ) - - if EscortGroup:OptionROEHoldFirePossible() then - local EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEHoldFire, "Holding weapons!" ) + +function AI_ESCORT:SetFlightMenuROT() + + for _, MenuROT in pairs( self.Menu.ROT) do + local FlightMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", self.FlightMenu ) + + local FlightMenuROTNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", FlightMenuROT, AI_ESCORT._FlightROTNoReaction, self, "Fighting until death!" ) + local FlightMenuROTPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", FlightMenuROT, AI_ESCORT._FlightROTPassiveDefense, self, "Defending using jammers, chaff and flares!" ) + local FlightMenuROTEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", FlightMenuROT, AI_ESCORT._FlightROTEvadeFire, self, "Evading on enemy fire!" ) + local FlightMenuROTVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", FlightMenuROT, AI_ESCORT._FlightROTVertical, self, "Evading on enemy fire with vertical manoeuvres!" ) + end + + return self +end + + +function AI_ESCORT:SetEscortMenuROT( EscortGroup ) + + for _, MenuROT in pairs( self.Menu.ROT) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", EscortGroup.EscortMenu ) + + if not EscortGroup.EscortMenuEvasion then + -- Reaction to Threats + if EscortGroup:OptionROTNoReactionPossible() then + local EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTNoReaction, "Fighting until death!" ) end - if EscortGroup:OptionROEReturnFirePossible() then - local EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEReturnFire, "Returning fire!" ) + if EscortGroup:OptionROTPassiveDefensePossible() then + local EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) end - if EscortGroup:OptionROEOpenFirePossible() then - EscortGroup.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEOpenFire, "Opening fire on designated targets!!" ) + if EscortGroup:OptionROTEvadeFirePossible() then + local EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTEvadeFire, "Evading on enemy fire!" ) end - if EscortGroup:OptionROEWeaponFreePossible() then - EscortGroup.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEWeaponFree, "Opening fire on targets of opportunity!" ) + if EscortGroup:OptionROTVerticalPossible() then + local EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) end end end - ) - + end + return self end + --- Defines a menu to let the escort set its evasion when under threat. -- All rules of engagement will appear under the menu **Evasion**. -- @param #AI_ESCORT self @@ -1097,39 +1246,8 @@ end function AI_ESCORT:MenuROT( MenuTextFormat ) self:F( MenuTextFormat ) - local FlightMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", self.FlightMenu ) - - local FlightMenuROTNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", FlightMenuROT, AI_ESCORT._FlightROTNoReaction, self, "Fighting until death!" ) - local FlightMenuROTPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", FlightMenuROT, AI_ESCORT._FlightROTPassiveDefense, self, "Defending using jammers, chaff and flares!" ) - local FlightMenuROTEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", FlightMenuROT, AI_ESCORT._FlightROTEvadeFire, self, "Evading on enemy fire!" ) - local FlightMenuROTVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", FlightMenuROT, AI_ESCORT._FlightROTVertical, self, "Evading on enemy fire with vertical manoeuvres!" ) - - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if EscortGroup:IsAir() then - - local EscortGroupName = EscortGroup:GetName() - local EscortMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", EscortGroup.EscortMenu ) - - if not EscortGroup.EscortMenuEvasion then - -- Reaction to Threats - if EscortGroup:OptionROTNoReactionPossible() then - local EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTNoReaction, "Fighting until death!" ) - end - if EscortGroup:OptionROTPassiveDefensePossible() then - local EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) - end - if EscortGroup:OptionROTEvadeFirePossible() then - local EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTEvadeFire, "Evading on enemy fire!" ) - end - if EscortGroup:OptionROTVerticalPossible() then - local EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) - end - end - end - end - ) + self.Menu.ROT = self.Menu.ROT or {} + self.Menu.ROT[#self.Menu.ROT+1] = {} return self end From 6a1bf507003a6692a42bbcde3dd505019d935f65 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 17 Jun 2019 10:19:15 +0300 Subject: [PATCH 293/485] Updated --- Moose Development/Moose/AI/AI_Escort.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index b81ebbb02..408ab2be8 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -405,7 +405,7 @@ function AI_ESCORT:MenusHelicopter( XStart, XSpace, YStart, YSpace, ZStart, ZSpa self:MenuFlare() self:MenuSmoke() - self:MenuReportTargets( 60 ) + self:MenuTargets( 60 ) self:MenuAssistedAttack() self:MenuROE() self:MenuROT() @@ -456,7 +456,7 @@ function AI_ESCORT:MenusAirplanes( XStart, XSpace, YStart, YSpace, ZStart, ZSpac self:MenuFlare() self:MenuSmoke() - self:MenuReportTargets( 60 ) + self:MenuTargets( 60 ) self:MenuAssistedAttack() self:MenuROE() self:MenuROT() @@ -501,7 +501,7 @@ function AI_ESCORT:MenuFormation( Formation, ... ) local FormationID = "Formation"..Formation self.Menu[FormationID] = self.Menu[FormationID] or {} self.Menu[FormationID][#self.Menu[FormationID]+1] = {} - self.Menu[FormationID][#self.Menu[FormationID]+1].Arguments = ... + self.Menu[FormationID][#self.Menu[FormationID]].Arguments = ... end @@ -932,7 +932,7 @@ function AI_ESCORT:SetFlightMenuFlare() for _, MenuFlare in pairs( self.Menu.Flare) do local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) - local FlightMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuText, FlightMenuReportNavigation ) + local FlightMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuFlare.MenuText, FlightMenuReportNavigation ) local FlightMenuFlareGreenFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Green, "Released a green flare!" ) local FlightMenuFlareRedFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Red, "Released a red flare!" ) @@ -992,7 +992,7 @@ function AI_ESCORT:SetFlightMenuSmoke() for _, MenuSmoke in pairs( self.Menu.Smoke) do local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) - local FlightMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuText, FlightMenuReportNavigation ) + local FlightMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuSmoke.MenuText, FlightMenuReportNavigation ) local FlightMenuSmokeGreenFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) local FlightMenuSmokeRedFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) @@ -1086,7 +1086,7 @@ function AI_ESCORT:SetEscortMenuTargets( EscortGroup ) -- Attack Targets local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) - EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, timer, MenuTargets.Interval ) + EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, 1, MenuTargets.Interval ) end end @@ -1117,7 +1117,7 @@ end --- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. -- This menu will appear under **Request assistance from**. --- Note that this method needs to be preceded with the method MenuReportTargets. +-- Note that this method needs to be preceded with the method MenuTargets. -- @param #AI_ESCORT self -- @return #AI_ESCORT function AI_ESCORT:MenuAssistedAttack() From 9c523cad5224405007b3f95b083b88d513c96aa2 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 17 Jun 2019 20:25:50 +0300 Subject: [PATCH 294/485] Updates --- Moose Development/Moose/AI/AI_Escort.lua | 151 +++++++++++--------- Moose Development/Moose/AI/AI_Formation.lua | 32 +++++ 2 files changed, 112 insertions(+), 71 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 408ab2be8..3dc05027a 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -223,9 +223,11 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) self.PlayerUnit = self.FollowUnit -- Wrapper.Unit#UNIT self.PlayerGroup = self.FollowUnit:GetGroup() -- Wrapper.Group#GROUP + self.EscortName = EscortName + self.EscortGroupSet = EscortGroupSet - self.EscortGroupSet:SetSomeIteratorLimit( 5 ) + self.EscortGroupSet:SetSomeIteratorLimit( 8 ) self.EscortBriefing = EscortBriefing @@ -248,14 +250,9 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) self.GT1 = 0 - self.MainMenu = MENU_GROUP:New( self.PlayerGroup, EscortName ) - self.FlightMenu = MENU_GROUP:New( self.PlayerGroup, "Flight", self.MainMenu ) - EscortGroupSet:ForEachGroup( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetName(), self.MainMenu ) - -- Set EscortGroup known at EscortUnit. if not self.PlayerUnit._EscortGroups then self.PlayerUnit._EscortGroups = {} @@ -280,7 +277,6 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) EscortGroupSet:ForEachGroup( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetName(), self.MainMenu ) EscortGroup:WayPointInitialize() EscortGroup:OptionROTVertical() @@ -302,6 +298,10 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) self:HandleEvent( EVENTS.Dead, OnEventDeadOrCrash ) self:HandleEvent( EVENTS.Crash, OnEventDeadOrCrash ) + self.MainMenu = MENU_GROUP:New( self.PlayerGroup, self.EscortName ) + self.FlightMenu = MENU_GROUP:New( self.PlayerGroup, "Flight", self.MainMenu ) + + self:SetFlightMenuJoinUp() self:SetFlightMenuFormation( "Trail" ) self:SetFlightMenuFormation( "Stack" ) self:SetFlightMenuFormation( "LeftLine" ) @@ -325,6 +325,12 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) + + EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetCallsign(), self.MainMenu ) + + self:SetEscortMenuJoinUp( EscortGroup ) + self:SetEscortMenuResumeMission( EscortGroup ) + self:SetEscortMenuHoldAtEscortPosition( EscortGroup ) self:SetEscortMenuHoldAtLeaderPosition( EscortGroup ) @@ -335,10 +341,10 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) self:SetEscortMenuROT( EscortGroup ) self:SetEscortMenuTargets( EscortGroup ) + end ) - end --- Set a Detection method for the EscortUnit to be reported upon. @@ -379,13 +385,13 @@ function AI_ESCORT:MenusHelicopter( XStart, XSpace, YStart, YSpace, ZStart, ZSpa -- self:MenuScanForTargets( 100, 60 ) - self.XStart = XStart or self.XStart - self.XSpace = XSpace or self.XSpace - self.YStart = YStart or self.YStart - self.YSpace = YSpace or self.YSpace - self.ZStart = ZStart or self.ZStart - self.ZSpace = ZSpace or self.ZSpace - self.ZLevels = ZLevels or self.ZLevels + self.XStart = XStart or 50 + self.XSpace = XSpace or 50 + self.YStart = YStart or 50 + self.YSpace = YSpace or 50 + self.ZStart = ZStart or 50 + self.ZSpace = ZSpace or 50 + self.ZLevels = ZLevels or 10 self:MenuJoinUp() self:MenuFormationTrail(self.XStart,self.XSpace,self.YStart) @@ -410,9 +416,6 @@ function AI_ESCORT:MenusHelicopter( XStart, XSpace, YStart, YSpace, ZStart, ZSpa self:MenuROE() self:MenuROT() - self:MenuResumeMission() - - return self end @@ -432,13 +435,13 @@ function AI_ESCORT:MenusAirplanes( XStart, XSpace, YStart, YSpace, ZStart, ZSpac -- self:MenuScanForTargets( 100, 60 ) - self.XStart = XStart or self.XStart - self.XSpace = XSpace or self.XSpace - self.YStart = YStart or self.YStart - self.YSpace = YSpace or self.YSpace - self.ZStart = ZStart or self.ZStart - self.ZSpace = ZSpace or self.ZSpace - self.ZLevels = ZLevels or self.ZLevels + self.XStart = XStart or 50 + self.XSpace = XSpace or 50 + self.YStart = YStart or 50 + self.YSpace = YSpace or 50 + self.ZStart = ZStart or 50 + self.ZSpace = ZSpace or 50 + self.ZLevels = ZLevels or 10 self:MenuJoinUp() self:MenuFormationTrail(self.XStart,self.XSpace,self.YStart) @@ -461,9 +464,6 @@ function AI_ESCORT:MenusAirplanes( XStart, XSpace, YStart, YSpace, ZStart, ZSpac self:MenuROE() self:MenuROT() - self:MenuResumeMission() - - return self end @@ -476,17 +476,18 @@ function AI_ESCORT:SetFlightMenuFormation( Formation ) if MenuFormation then local Arguments = MenuFormation.Arguments + self:I({Arguments=unpack(Arguments)}) local FlightMenuFormation = MENU_GROUP:New( self.PlayerGroup, "Formation", self.MainMenu ) local MenuFlightFormationID = MENU_GROUP_COMMAND:New( self.PlayerGroup, Formation, FlightMenuFormation, function ( self, Formation, ... ) self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup - function( EscortGroup, self, Formation, ... ) + function( EscortGroup, self, Formation, Arguments ) if EscortGroup:IsAir() then self:E({FormationID=FormationID}) - self[FormationID]( self, ... ) + self[FormationID]( self, unpack(Arguments) ) end - end, self, Formation, ... + end, self, Formation, Arguments ) end, self, Formation, Arguments ) @@ -500,8 +501,7 @@ function AI_ESCORT:MenuFormation( Formation, ... ) local FormationID = "Formation"..Formation self.Menu[FormationID] = self.Menu[FormationID] or {} - self.Menu[FormationID][#self.Menu[FormationID]+1] = {} - self.Menu[FormationID][#self.Menu[FormationID]].Arguments = ... + self.Menu[FormationID].Arguments = arg end @@ -656,11 +656,20 @@ function AI_ESCORT:MenuFormationBox( XStart, XSpace, YStart, YSpace, ZStart, ZSp return self end +function AI_ESCORT:SetFlightMenuJoinUp() + + if self.Menu.JoinUp == true then + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) + end + +end + --- Sets a menu slot to join formation for an escort. -- @param #AI_ESCORT self -- @return #AI_ESCORT -function AI_ESCORT:EscortMenuJoinUp( EscortGroup ) +function AI_ESCORT:SetEscortMenuJoinUp( EscortGroup ) if self.Menu.JoinUp == true then if EscortGroup:IsAir() then @@ -679,16 +688,6 @@ function AI_ESCORT:MenuJoinUp() self.Menu.JoinUp = true - local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) - local FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) - - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - self:EscortMenuJoinUp( EscortGroup ) - end - ) - return self end @@ -1079,14 +1078,14 @@ function AI_ESCORT:SetEscortMenuTargets( EscortGroup ) local EscortMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu ) -- Report Targets - --EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) + EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", EscortMenuReportTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) --EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) --EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) -- Attack Targets local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) - EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, 1, MenuTargets.Interval ) + --EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, 1, MenuTargets.Interval ) end end @@ -1256,18 +1255,13 @@ end -- All rules of engagement will appear under the menu **Resume mission from**. -- @param #AI_ESCORT self -- @return #AI_ESCORT -function AI_ESCORT:MenuResumeMission() +function AI_ESCORT:SetEscortMenuResumeMission( EscortGroup ) self:F() - self.EscortGroupSet:ForSomeGroupAlive( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - if EscortGroup:IsAir() then - local EscortGroupName = EscortGroup:GetName() - EscortGroup.EscortMenuResumeMission = MENU_GROUP:New( self.PlayerGroup, "Resume from", EscortGroup.EscortMenu ) - end - end - ) + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + EscortGroup.EscortMenuResumeMission = MENU_GROUP:New( self.PlayerGroup, "Resume from", EscortGroup.EscortMenu ) + end return self end @@ -1346,8 +1340,6 @@ function AI_ESCORT:_JoinUp( EscortGroup ) local EscortUnit = self.PlayerUnit self:ModeFormation( EscortGroup ) - - EscortGroup:SetState( self, "Mode", self.__Enum.Mode.Follow ) end @@ -1549,7 +1541,16 @@ function AI_ESCORT.___Resume( EscortGroup, self ) local PlayerGroup = self.PlayerGroup - EscortGroup:MessageTypeToGroup( "Destroyed all targets. Rejoining.", MESSAGE.Type.Information, PlayerGroup ) + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTVertical() + + EscortGroup:SetState( EscortGroup, "Mode", EscortGroup:GetState( EscortGroup, "PreviousMode" ) ) + + if EscortGroup:GetState( EscortGroup, "Mode" ) == self.__Enum.Mode.Mission then + EscortGroup:MessageTypeToGroup( "Destroyed all targets. Resuming route.", MESSAGE.Type.Information, PlayerGroup ) + else + EscortGroup:MessageTypeToGroup( "Destroyed all targets. Rejoining formation.", MESSAGE.Type.Information, PlayerGroup ) + end end @@ -1583,7 +1584,7 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) local EscortUnit = self.PlayerUnit - self:ModeMission( EscortGroup ) + self:ModeAttack( EscortGroup ) if EscortGroup:IsAir() then EscortGroup:OptionROEOpenFire() @@ -1607,7 +1608,7 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) Tasks[#Tasks+1] = EscortGroup:TaskCombo( AttackUnitTasks ) Tasks[#Tasks+1] = EscortGroup:TaskFunction( "AI_ESCORT.___Resume", self ) - EscortGroup:SetTask( + EscortGroup:PushTask( EscortGroup:TaskCombo( Tasks ), 1 @@ -1628,7 +1629,7 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) end, Tasks ) - EscortGroup:SetTask( + EscortGroup:PushTask( EscortGroup:TaskCombo( Tasks ), 1 @@ -1797,9 +1798,10 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) if EscortGroup:IsAlive() and self.PlayerUnit:IsAlive() then - if true then - local EscortGroupName = EscortGroup:GetName() + local EscortGroupName = EscortGroup:GetCallsign() + + local DetectedTargetsReport = REPORT:New( "Reporting target locations from my position:\n" ) -- A new report to display the detected targets as a message to the player. if EscortGroup.EscortMenuTargetAssistance then @@ -1812,14 +1814,17 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) local TimeUpdate = timer.getTime() - local EscortGroupName = EscortGroup:GetName() local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) - + + local DetectedTargets = false for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do + DetectedTargets = true - local DetectedItemReportSummary = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local DetectedMenu = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ):Text("\n") - local DetectedMenu = DetectedItemReportSummary:Text("\n") + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportSummary = DetectedItemReportSummary:Text(", ") + DetectedTargetsReport:AddIndent( ReportSummary, "-" ) if EscortGroup:IsAir() then @@ -1850,6 +1855,12 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) EscortMenuAttackTargets:RemoveSubMenus( TimeUpdate, "Esort" ) + if DetectedTargets then + EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.PlayerGroup ) + else + EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.PlayerGroup ) + end + if EscortGroup.EscortMenuResumeMission then EscortGroup.EscortMenuResumeMission:RemoveSubMenus() @@ -1865,8 +1876,6 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) end return true - else - end end return false @@ -1881,7 +1890,7 @@ function AI_ESCORT:_FlightReportTargetsScheduler() local EscortGroup = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP - local DetectedTargetsReport = REPORT:New( "Reporting detected targets:\n" ) -- A new report to display the detected targets as a message to the player. + local DetectedTargetsReport = REPORT:New( "Reporting target locations from your position:\n" ) -- A new report to display the detected targets as a message to the player. if EscortGroup and ( self.PlayerUnit:IsAlive() and EscortGroup:IsAlive() ) then diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 31930c045..dbed463b1 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -143,6 +143,8 @@ AI_FORMATION.__Enum.Formation = { AI_FORMATION.__Enum.Mode = { Mission = 0, Formation = 1, + Attack = 2, + Reconnaissance = 3, } @@ -958,11 +960,13 @@ end function AI_FORMATION:ModeMission( FollowGroup ) if FollowGroup then + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) else self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( FollowGroup ) + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) end ) @@ -973,6 +977,30 @@ function AI_FORMATION:ModeMission( FollowGroup ) end +--- This sets your escorts to execute an attack. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup FollowGroup. +-- @return #AI_FORMATION +function AI_FORMATION:ModeAttack( FollowGroup ) + + if FollowGroup then + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Attack ) + else + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( FollowGroup ) + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Attack ) + end + ) + end + + + return self +end + + --- This sets your escorts to fly in a formation. -- @param #AI_FORMATION self -- @param Wrapper.Group#GROUP FollowGroup FollowGroup. @@ -980,11 +1008,13 @@ end function AI_FORMATION:ModeFormation( FollowGroup ) if FollowGroup then + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) else self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( FollowGroup ) + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) end ) @@ -1047,6 +1077,8 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 -- @param Wrapper.Unit#UNIT ClientUnit function( FollowGroup, Formation, ClientUnit, CT1, CV1, CT2, CV2 ) + self:I({Mode=FollowGroup:GetState( FollowGroup, "Mode" )}) + if FollowGroup:GetState( FollowGroup, "Mode" ) == self.__Enum.Mode.Formation then FollowGroup:OptionROTEvadeFire() From 1e0e67c13d60dbeb7de3b4ba8de6658bc2776f27 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 18 Jun 2019 07:06:00 +0300 Subject: [PATCH 295/485] Updates --- Moose Development/Moose/AI/AI_Escort.lua | 157 +++++++++++------- .../Moose/AI/AI_Escort_Request.lua | 32 ++-- Moose Development/Moose/Core/Spawn.lua | 13 ++ 3 files changed, 121 insertions(+), 81 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 3dc05027a..c10d6cbee 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -270,36 +270,8 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) return self end ---- @param #AI_ESCORT self --- @param Core.Set#SET_GROUP EscortGroupSet -function AI_ESCORT:onafterStart( EscortGroupSet ) - EscortGroupSet:ForEachGroup( - --- @param Core.Group#GROUP EscortGroup - function( EscortGroup ) - EscortGroup:WayPointInitialize() - - EscortGroup:OptionROTVertical() - EscortGroup:OptionROEOpenFire() - end - ) - - local LeaderEscort = EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP - - local Report = REPORT:New( "Escort reporting:" ) - Report:Add( "Joining Up " .. EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.PlayerUnit ) ) - - LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) - - self.Detection = DETECTION_AREAS:New( EscortGroupSet, 5000 ) - - self.Detection:__Start( 30 ) - - self:HandleEvent( EVENTS.Dead, OnEventDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, OnEventDeadOrCrash ) - - self.MainMenu = MENU_GROUP:New( self.PlayerGroup, self.EscortName ) - self.FlightMenu = MENU_GROUP:New( self.PlayerGroup, "Flight", self.MainMenu ) +function AI_ESCORT:_InitFlightMenus() self:SetFlightMenuJoinUp() self:SetFlightMenuFormation( "Trail" ) @@ -322,25 +294,77 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) self:SetFlightMenuTargets() +end + +function AI_ESCORT:_InitEscortMenus( EscortGroup ) + + EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetCallsign(), self.MainMenu ) + + self:SetEscortMenuJoinUp( EscortGroup ) + self:SetEscortMenuResumeMission( EscortGroup ) + + self:SetEscortMenuHoldAtEscortPosition( EscortGroup ) + self:SetEscortMenuHoldAtLeaderPosition( EscortGroup ) + + self:SetEscortMenuFlare( EscortGroup ) + self:SetEscortMenuSmoke( EscortGroup ) + + self:SetEscortMenuROE( EscortGroup ) + self:SetEscortMenuROT( EscortGroup ) + + self:SetEscortMenuTargets( EscortGroup ) + +end + +function AI_ESCORT:_InitEscortRoute( EscortGroup ) + + EscortGroup.MissionRoute = EscortGroup:GetTaskRoute() + +end + + +--- @param #AI_ESCORT self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT:onafterStart( EscortGroupSet ) + + self:F() + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + EscortGroup:WayPointInitialize() + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEOpenFire() + end + ) + + -- TODO:Revise this... + local LeaderEscort = EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP + if LeaderEscort then + local Report = REPORT:New( "Escort reporting:" ) + Report:Add( "Joining Up " .. EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.PlayerUnit ) ) + LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) + end + + self.Detection = DETECTION_AREAS:New( EscortGroupSet, 5000 ) + + self.Detection:__Start( 30 ) + + self:HandleEvent( EVENTS.Dead, OnEventDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, OnEventDeadOrCrash ) + + self.MainMenu = MENU_GROUP:New( self.PlayerGroup, self.EscortName ) + self.FlightMenu = MENU_GROUP:New( self.PlayerGroup, "Flight", self.MainMenu ) + + self:_InitFlightMenus() + self.EscortGroupSet:ForSomeGroupAlive( --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) - EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetCallsign(), self.MainMenu ) - - self:SetEscortMenuJoinUp( EscortGroup ) - self:SetEscortMenuResumeMission( EscortGroup ) - - self:SetEscortMenuHoldAtEscortPosition( EscortGroup ) - self:SetEscortMenuHoldAtLeaderPosition( EscortGroup ) - - self:SetEscortMenuFlare( EscortGroup ) - self:SetEscortMenuSmoke( EscortGroup ) - - self:SetEscortMenuROE( EscortGroup ) - self:SetEscortMenuROT( EscortGroup ) - - self:SetEscortMenuTargets( EscortGroup ) + self:_InitEscortMenus( EscortGroup ) + self:_InitEscortRoute( EscortGroup ) end ) @@ -1086,6 +1110,7 @@ function AI_ESCORT:SetEscortMenuTargets( EscortGroup ) local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) --EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, 1, MenuTargets.Interval ) + EscortGroup.ResumeScheduler = SCHEDULER:New( self, self._ResumeScheduler, { EscortGroup }, 1, 60 ) end end @@ -1561,8 +1586,10 @@ end function AI_ESCORT:_ResumeMission( EscortGroup, WayPoint ) --self.FollowScheduler:Stop( self.FollowSchedule ) + + self:ModeMission( EscortGroup ) - local WayPoints = EscortGroup:GetTaskRoute() + local WayPoints = EscortGroup.MissionRoute self:T( WayPoint, WayPoints ) for WayPointIgnore = 1, WayPoint do @@ -1788,6 +1815,32 @@ function AI_ESCORT:RegisterRoute() return TaskPoints end +--- Resume Scheduler. +-- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +function AI_ESCORT:_ResumeScheduler( EscortGroup ) + self:F( EscortGroup:GetName() ) + + if EscortGroup:IsAlive() and self.PlayerUnit:IsAlive() then + + + local EscortGroupName = EscortGroup:GetCallsign() + + if EscortGroup.EscortMenuResumeMission then + EscortGroup.EscortMenuResumeMission:RemoveSubMenus() + + local TaskPoints = EscortGroup.MissionRoute + + for WayPointID, WayPoint in pairs( TaskPoints ) do + local EscortVec3 = EscortGroup:GetVec3() + local Distance = ( ( WayPoint.x - EscortVec3.x )^2 + + ( WayPoint.y - EscortVec3.z )^2 + ) ^ 0.5 / 1000 + MENU_GROUP_COMMAND:New( self.PlayerGroup, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", EscortGroup.EscortMenuResumeMission, AI_ESCORT._ResumeMission, self, EscortGroup, WayPointID ) + end + end + end +end --- Report Targets Scheduler. @@ -1861,20 +1914,6 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.PlayerGroup ) end - if EscortGroup.EscortMenuResumeMission then - EscortGroup.EscortMenuResumeMission:RemoveSubMenus() - - local TaskPoints = EscortGroup:GetTaskRoute() - - for WayPointID, WayPoint in pairs( TaskPoints ) do - local EscortVec3 = EscortGroup:GetVec3() - local Distance = ( ( WayPoint.x - EscortVec3.x )^2 + - ( WayPoint.y - EscortVec3.z )^2 - ) ^ 0.5 / 1000 - MENU_GROUP_COMMAND:New( self.PlayerGroup, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", EscortGroup.EscortMenuResumeMission, AI_ESCORT._ResumeMission, self, EscortGroup, WayPointID ) - end - end - return true end diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index faf0e817c..7a2fecb55 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -226,43 +226,27 @@ end function AI_ESCORT_REQUEST:SpawnEscort() local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Hot ) - self.EscortGroupSet:AddGroup( EscortGroup ) EscortGroup:OptionROTVertical() EscortGroup:OptionROEHoldFire() self:ScheduleOnce( 0.1, function( EscortGroup ) + + self.EscortGroupSet:AddGroup( EscortGroup ) + local LeaderEscort = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP - local Report = REPORT:New() - Report:Add( "Joining Up " .. self.EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) - self:FormationTrail( 50, 50, 50 ) if self.SpawnMode == self.__Enum.Mode.Formation then self:ModeFormation( EscortGroup ) end - --self:Menus( self.XStart, self.XSpace, self.YStart, self.YSpace, self.ZStart, self.ZSpace, self.ZLevels ) - - EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetName(), self.MainMenu ) - - self:EscortMenuJoinUp( EscortGroup ) - - self:EscortMenuHoldAtEscortPosition( EscortGroup ) - self:EscortMenuHoldAtLeaderPosition( EscortGroup ) - - self:MenuFlare() - self:MenuSmoke() - - self:MenuReportTargets( 60 ) - self:MenuAssistedAttack() - self:MenuROE() - self:MenuROT() - - self:MenuResumeMission() + self:_InitFlightMenus() + self:_InitEscortMenus( EscortGroup ) + self:_InitEscortRoute( EscortGroup ) --- @param #AI_ESCORT self -- @param Core.Event#EVENTDATA EventData @@ -283,6 +267,8 @@ end -- @param Core.Set#SET_GROUP EscortGroupSet function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) + self:F() + if not self.MenuRequestEscort then self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request new escort ", self.MainMenu, function() @@ -291,6 +277,8 @@ function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) ) end + self:GetParent( self ).onafterStart( self, EscortGroupSet ) + self:HandleEvent( EVENTS.Dead, self.OnEventDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self.OnEventDeadOrCrash ) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 3495907ad..e0f7055c0 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2839,6 +2839,19 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 SpawnTemplate.units[UnitID].unitId = nil end end + + -- Callsign + for UnitID = 1, #SpawnTemplate.units do + local Callsign = SpawnTemplate.units[UnitID].callsign + if Callsign[1] ~= nil then -- blue callsign + Callsign[2] = ( ( SpawnIndex - 1 ) % 10 ) + 1 + local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string + local CallsignLen = CallsignName:len() + SpawnTemplate.units[UnitID].callsign["name"] = CallsignName:sub(1,CallsignLen) .. SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3] + else + SpawnTemplate.units[UnitID].callsign = Callsign + SpawnIndex + end + end self:T3( { "Template:", SpawnTemplate } ) return SpawnTemplate From 1d4dad01804c68a55c067dbcd4b5b3cd1969697d Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 18 Jun 2019 20:03:24 +0300 Subject: [PATCH 296/485] Updates --- Moose Development/Moose/AI/AI_Escort.lua | 31 +++++++++++---------- Moose Development/Moose/AI/AI_Formation.lua | 3 +- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index c10d6cbee..6a817ffbf 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -1099,17 +1099,17 @@ function AI_ESCORT:SetEscortMenuTargets( EscortGroup ) for _, MenuTargets in pairs( self.Menu.Targets) do if EscortGroup:IsAir() then local EscortGroupName = EscortGroup:GetName() - local EscortMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu ) + --local EscortMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu ) -- Report Targets - EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", EscortMenuReportTargets, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) + EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) --EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) --EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) -- Attack Targets - local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) + --local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) - --EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, 1, MenuTargets.Interval ) + EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, 1, MenuTargets.Interval ) EscortGroup.ResumeScheduler = SCHEDULER:New( self, self._ResumeScheduler, { EscortGroup }, 1, 60 ) end end @@ -1365,6 +1365,8 @@ function AI_ESCORT:_JoinUp( EscortGroup ) local EscortUnit = self.PlayerUnit self:ModeFormation( EscortGroup ) + + EscortGroup:MessageTypeToGroup( "Joining up!", MESSAGE.Type.Information, EscortUnit:GetGroup() ) end @@ -1615,7 +1617,7 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) if EscortGroup:IsAir() then EscortGroup:OptionROEOpenFire() - EscortGroup:OptionROTPassiveDefense() + EscortGroup:OptionROTVertical() EscortGroup:SetState( EscortGroup, "Escort", self ) local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) @@ -1664,8 +1666,7 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) end - EscortGroup:MessageTypeToGroup( "Engaging!", MESSAGE.Type.Information, EscortUnit ) - + EscortGroup:MessageTypeToGroup( "Engaging Target!", MESSAGE.Type.Information, self.PlayerGroup ) end @@ -1846,7 +1847,7 @@ end --- Report Targets Scheduler. -- @param #AI_ESCORT self -- @param Wrapper.Group#GROUP EscortGroup -function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) +function AI_ESCORT:_ReportTargetsScheduler( EscortGroup, Report ) self:F( EscortGroup:GetName() ) if EscortGroup:IsAlive() and self.PlayerUnit:IsAlive() then @@ -1854,7 +1855,7 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) local EscortGroupName = EscortGroup:GetCallsign() - local DetectedTargetsReport = REPORT:New( "Reporting target locations from my position:\n" ) -- A new report to display the detected targets as a message to the player. + local DetectedTargetsReport = REPORT:New( "Reporting targets:\n" ) -- A new report to display the detected targets as a message to the player. if EscortGroup.EscortMenuTargetAssistance then @@ -1908,10 +1909,12 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup ) EscortMenuAttackTargets:RemoveSubMenus( TimeUpdate, "Esort" ) - if DetectedTargets then - EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.PlayerGroup ) - else - EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.PlayerGroup ) + if Report then + if DetectedTargets then + EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.PlayerGroup ) + else + EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.PlayerGroup ) + end end return true @@ -1929,7 +1932,7 @@ function AI_ESCORT:_FlightReportTargetsScheduler() local EscortGroup = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP - local DetectedTargetsReport = REPORT:New( "Reporting target locations from your position:\n" ) -- A new report to display the detected targets as a message to the player. + local DetectedTargetsReport = REPORT:New( "Reporting your targets:\n" ) -- A new report to display the detected targets as a message to the player. if EscortGroup and ( self.PlayerUnit:IsAlive() and EscortGroup:IsAlive() ) then diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index dbed463b1..7d0be781a 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -1142,7 +1142,8 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) local CVI = { x = CV2.x + CS * 10 * math.sin(Ca), - y = GH2.y + ( Distance + FollowFormation.x ) / 10, -- + FollowFormation.y, + --y = GH2.y + ( Distance + FollowFormation.x ) / 10, -- + FollowFormation.y, + y = GH2.y, z = CV2.z + CS * 10 * math.cos(Ca), } From b4324cb05753c33bafe78b172bead155ee79c959 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 18 Jun 2019 20:18:43 +0300 Subject: [PATCH 297/485] Optimized inclination for formation flying. --- Moose Development/Moose/AI/AI_Formation.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 7d0be781a..cf1971f8f 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -1141,8 +1141,12 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) + local Inclination = ( Distance + FollowFormation.x ) / 10 + if Inclination < -30 then + Inclination = - 30 + end local CVI = { x = CV2.x + CS * 10 * math.sin(Ca), - --y = GH2.y + ( Distance + FollowFormation.x ) / 10, -- + FollowFormation.y, + y = GH2.y + Inclination, -- + FollowFormation.y, y = GH2.y, z = CV2.z + CS * 10 * math.cos(Ca), } From b1cee9969fbc91b55222da02f4dc2082d0ff7c20 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 19 Jun 2019 05:17:51 +0300 Subject: [PATCH 298/485] Fixes --- Moose Development/Moose/AI/AI_A2G_Dispatcher.lua | 4 ++-- Moose Development/Moose/Core/Spawn.lua | 2 +- 2 files 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 557c9bc13..e0484ee64 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -4108,7 +4108,7 @@ do -- AI_A2G_DISPATCHER self:F( { "Target ID", DetectedItem.ItemID } ) - self:F( { DefenseLimit = self.DefenseLimitmit, DefenseTotal = DefenseTotal } ) + self:F( { DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } ) DetectedSet:Flush( self ) local DetectedID = DetectedItem.ID @@ -4149,7 +4149,7 @@ do -- AI_A2G_DISPATCHER end end - if EngageCoordinate and DefenseTotal < self.DefenseLimit then + if EngageCoordinate and ( ( self.DefenseLimit and DefenseTotal < self.DefenseLimit ) or true ) then do local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged... if DefendersMissing > 0 then diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index ce03cd14e..721c44420 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2848,7 +2848,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 -- Callsign for UnitID = 1, #SpawnTemplate.units do local Callsign = SpawnTemplate.units[UnitID].callsign - if Callsign[1] ~= nil then -- blue callsign + if type(Callsign) ~= "number" then -- blue callsign Callsign[2] = ( ( SpawnIndex - 1 ) % 10 ) + 1 local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string local CallsignLen = CallsignName:len() From 4296dbade7416a91bf954b334389ad94eb53cea0 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 19 Jun 2019 06:57:40 +0300 Subject: [PATCH 299/485] Small fixes --- Moose Development/Moose/AI/AI_Escort.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 6a817ffbf..41ea97a03 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -404,7 +404,7 @@ end -- @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 #AI_ESCORT -function AI_ESCORT:MenusHelicopter( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) +function AI_ESCORT:MenusHelicopters( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) self:F() -- self:MenuScanForTargets( 100, 60 ) From bc9060adfc74bba001660f7ff61f9ef59899ae44 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 19 Jun 2019 18:13:00 +0300 Subject: [PATCH 300/485] Initial flight mode is formation. --- Moose Development/Moose/AI/AI_Escort.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 41ea97a03..51574b4e7 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -365,9 +365,11 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) self:_InitEscortMenus( EscortGroup ) self:_InitEscortRoute( EscortGroup ) - + + self:ModeFormation( EscortGroup ) end ) + end @@ -1907,7 +1909,7 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup, Report ) end - EscortMenuAttackTargets:RemoveSubMenus( TimeUpdate, "Esort" ) + EscortMenuAttackTargets:RemoveSubMenus( TimeUpdate, "Escort" ) if Report then if DetectedTargets then From a907369082380ab989c28748d5aa6a37c8b55089 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 19 Jun 2019 18:17:12 +0300 Subject: [PATCH 301/485] FlightMode --- Moose Development/Moose/AI/AI_Escort.lua | 10 +++++----- Moose Development/Moose/AI/AI_Escort_Request.lua | 2 +- Moose Development/Moose/AI/AI_Formation.lua | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 51574b4e7..9363d28ac 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -366,7 +366,7 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) self:_InitEscortMenus( EscortGroup ) self:_InitEscortRoute( EscortGroup ) - self:ModeFormation( EscortGroup ) + self:SetFlightModeFormation( EscortGroup ) end ) @@ -1305,7 +1305,7 @@ function AI_ESCORT:_HoldPosition( OrbitGroup, EscortGroup, OrbitHeight, OrbitSec local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT - self:ModeMission( EscortGroup ) + self:SetFlightModeMission( EscortGroup ) local PointFrom = {} local GroupVec3 = EscortGroup:GetUnit(1):GetVec3() @@ -1366,7 +1366,7 @@ function AI_ESCORT:_JoinUp( EscortGroup ) local EscortUnit = self.PlayerUnit - self:ModeFormation( EscortGroup ) + self:SetFlightModeFormation( EscortGroup ) EscortGroup:MessageTypeToGroup( "Joining up!", MESSAGE.Type.Information, EscortUnit:GetGroup() ) end @@ -1591,7 +1591,7 @@ function AI_ESCORT:_ResumeMission( EscortGroup, WayPoint ) --self.FollowScheduler:Stop( self.FollowSchedule ) - self:ModeMission( EscortGroup ) + self:SetFlightModeMission( EscortGroup ) local WayPoints = EscortGroup.MissionRoute self:T( WayPoint, WayPoints ) @@ -1615,7 +1615,7 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) local EscortUnit = self.PlayerUnit - self:ModeAttack( EscortGroup ) + self:SetFlightModeAttack( EscortGroup ) if EscortGroup:IsAir() then EscortGroup:OptionROEOpenFire() diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index 7a2fecb55..518d306aa 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -241,7 +241,7 @@ function AI_ESCORT_REQUEST:SpawnEscort() LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) if self.SpawnMode == self.__Enum.Mode.Formation then - self:ModeFormation( EscortGroup ) + self:SetFlightModeFormation( EscortGroup ) end self:_InitFlightMenus() diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index cf1971f8f..4e26ca02e 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -957,7 +957,7 @@ end -- @param #AI_FORMATION self -- @param Wrapper.Group#GROUP FollowGroup FollowGroup. -- @return #AI_FORMATION -function AI_FORMATION:ModeMission( FollowGroup ) +function AI_FORMATION:SetFlightModeMission( FollowGroup ) if FollowGroup then FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) @@ -981,7 +981,7 @@ end -- @param #AI_FORMATION self -- @param Wrapper.Group#GROUP FollowGroup FollowGroup. -- @return #AI_FORMATION -function AI_FORMATION:ModeAttack( FollowGroup ) +function AI_FORMATION:SetFlightModeAttack( FollowGroup ) if FollowGroup then FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) @@ -1005,7 +1005,7 @@ end -- @param #AI_FORMATION self -- @param Wrapper.Group#GROUP FollowGroup FollowGroup. -- @return #AI_FORMATION -function AI_FORMATION:ModeFormation( FollowGroup ) +function AI_FORMATION:SetFlightModeFormation( FollowGroup ) if FollowGroup then FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) From 6f4d6c3daf3f3b09157d79f8ed3d9cb576ed1ea3 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 19 Jun 2019 18:32:21 +0300 Subject: [PATCH 302/485] Updates --- Moose Development/Moose/AI/AI_Escort.lua | 9 +++----- Moose Development/Moose/AI/AI_Formation.lua | 24 +++++++++++++++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 9363d28ac..1e2807e5a 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -1576,9 +1576,9 @@ function AI_ESCORT.___Resume( EscortGroup, self ) EscortGroup:SetState( EscortGroup, "Mode", EscortGroup:GetState( EscortGroup, "PreviousMode" ) ) if EscortGroup:GetState( EscortGroup, "Mode" ) == self.__Enum.Mode.Mission then - EscortGroup:MessageTypeToGroup( "Destroyed all targets. Resuming route.", MESSAGE.Type.Information, PlayerGroup ) + EscortGroup:MessageTypeToGroup( "Resuming route.", MESSAGE.Type.Information, PlayerGroup ) else - EscortGroup:MessageTypeToGroup( "Destroyed all targets. Rejoining formation.", MESSAGE.Type.Information, PlayerGroup ) + EscortGroup:MessageTypeToGroup( "Rejoining formation.", MESSAGE.Type.Information, PlayerGroup ) end end @@ -1613,8 +1613,6 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) self:F( EscortGroup ) - local EscortUnit = self.PlayerUnit - self:SetFlightModeAttack( EscortGroup ) if EscortGroup:IsAir() then @@ -1854,7 +1852,6 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup, Report ) if EscortGroup:IsAlive() and self.PlayerUnit:IsAlive() then - local EscortGroupName = EscortGroup:GetCallsign() local DetectedTargetsReport = REPORT:New( "Reporting targets:\n" ) -- A new report to display the detected targets as a message to the player. @@ -1881,7 +1878,7 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup, Report ) local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) local ReportSummary = DetectedItemReportSummary:Text(", ") DetectedTargetsReport:AddIndent( ReportSummary, "-" ) - + if EscortGroup:IsAir() then MENU_GROUP_COMMAND:New( self.PlayerGroup, diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 4e26ca02e..d10fa193a 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -141,10 +141,10 @@ AI_FORMATION.__Enum.Formation = { -- @field #number Mission -- @field #number Formation AI_FORMATION.__Enum.Mode = { - Mission = 0, - Formation = 1, - Attack = 2, - Reconnaissance = 3, + Mission = "M", + Formation = "F", + Attack = "A", + Reconnaissance = "R", } @@ -952,6 +952,22 @@ function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 end +--- Gets your escorts to flight mode. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup FollowGroup. +-- @return #AI_FORMATION +function AI_FORMATION:GetFlightMode( FollowGroup ) + + if FollowGroup then + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) + end + + + return FollowGroup:GetState( FollowGroup, "Mode" ) +end + + --- This sets your escorts to fly a mission. -- @param #AI_FORMATION self From 83f8e76584a0e761a686576aa8b429c952789f2c Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 20 Jun 2019 20:42:47 +0300 Subject: [PATCH 303/485] Fixed error in SPAWN logic. Sorry. fixed now. --- Moose Development/Moose/Core/Spawn.lua | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 721c44420..ff6387c5a 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2848,13 +2848,15 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 -- Callsign for UnitID = 1, #SpawnTemplate.units do local Callsign = SpawnTemplate.units[UnitID].callsign - if type(Callsign) ~= "number" then -- blue callsign - Callsign[2] = ( ( SpawnIndex - 1 ) % 10 ) + 1 - local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string - local CallsignLen = CallsignName:len() - SpawnTemplate.units[UnitID].callsign["name"] = CallsignName:sub(1,CallsignLen) .. SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3] - else - SpawnTemplate.units[UnitID].callsign = Callsign + SpawnIndex + if Callsign then + if type(Callsign) ~= "number" then -- blue callsign + Callsign[2] = ( ( SpawnIndex - 1 ) % 10 ) + 1 + local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string + local CallsignLen = CallsignName:len() + SpawnTemplate.units[UnitID].callsign["name"] = CallsignName:sub(1,CallsignLen) .. SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3] + else + SpawnTemplate.units[UnitID].callsign = Callsign + SpawnIndex + end end end From 3612d5d17149f8ddaaddff909995eea0b070c32c Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 20 Jun 2019 21:50:52 +0300 Subject: [PATCH 304/485] Fixes --- Moose Development/Moose/AI/AI_Escort_Request.lua | 5 ++--- Moose Development/Moose/AI/AI_Formation.lua | 11 +++++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index 518d306aa..1b7a4288f 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -240,9 +240,8 @@ function AI_ESCORT_REQUEST:SpawnEscort() Report:Add( "Joining Up " .. self.EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) - if self.SpawnMode == self.__Enum.Mode.Formation then - self:SetFlightModeFormation( EscortGroup ) - end + self:SetFlightModeFormation( EscortGroup ) + self:FormationTrail() self:_InitFlightMenus() self:_InitEscortMenus( EscortGroup ) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index d10fa193a..fea9c7c0e 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -701,6 +701,13 @@ end function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, Formation ) --R2.1 self:F( { FollowGroupSet, From , Event ,To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, Formation } ) + XStart = XStart or self.XStart + XSpace = XSpace or self.XSpace + YStart = YStart or self.YStart + YSpace = YSpace or self.YSpace + ZStart = ZStart or self.ZStart + ZSpace = ZSpace or self.ZSpace + FollowGroupSet:Flush( self ) local FollowSet = FollowGroupSet:GetSet() @@ -1093,10 +1100,10 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 -- @param Wrapper.Unit#UNIT ClientUnit function( FollowGroup, Formation, ClientUnit, CT1, CV1, CT2, CV2 ) - self:I({Mode=FollowGroup:GetState( FollowGroup, "Mode" )}) - if FollowGroup:GetState( FollowGroup, "Mode" ) == self.__Enum.Mode.Formation then + self:I({Mode=FollowGroup:GetState( FollowGroup, "Mode" )}) + FollowGroup:OptionROTEvadeFire() FollowGroup:OptionROEReturnFire() From 2a485ef85a9fbef214f5fb995cc4c227ed434131 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 21 Jun 2019 19:57:55 +0200 Subject: [PATCH 305/485] Rescue Helo v1.0.7 RESCUEHELO v1.0.7 Compatibility fix for AI_FORMATION changes. --- Moose Development/Moose/Ops/RescueHelo.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index f2fa53b80..dff109c8f 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -235,7 +235,7 @@ RESCUEHELO.UID=0 --- Class version. -- @field #string version -RESCUEHELO.version="1.0.6" +RESCUEHELO.version="1.0.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -927,6 +927,9 @@ function RESCUEHELO:onafterStart(From, Event, To) -- Formation parameters. self.formation:FormationCenterWing(-self.offsetX, 50, math.abs(self.altitude), 50, self.offsetZ, 50) + -- Formation mode. + self.formation:SetFlightModeFormation(self.helo) + -- Start formation FSM. self.formation:__Start(delay) From c88f7bede5094056292aff3454c9381ee8633d1a Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 24 Jun 2019 13:38:01 +0200 Subject: [PATCH 306/485] SPOT LaseCoordinate Alarm state ships zonRadius in task fire at point. --- Moose Development/Moose/Core/Spot.lua | 85 ++++++++++++++++++- .../Moose/Wrapper/Controllable.lua | 9 +- .../Moose/Wrapper/Positionable.lua | 18 ++++ 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Core/Spot.lua b/Moose Development/Moose/Core/Spot.lua index dbc3ea683..6395cd7b4 100644 --- a/Moose Development/Moose/Core/Spot.lua +++ b/Moose Development/Moose/Core/Spot.lua @@ -126,6 +126,39 @@ do -- @param Wrapper.Positionable#POSITIONABLE Target -- @param #number LaserCode Laser code. -- @param #number Duration Duration of lasing in seconds. + + self:AddTransition( "Off", "LaseOnCoordinate", "On" ) + + --- LaseOnCoordinate Handler OnBefore for SPOT. + -- @function [parent=#SPOT] OnBeforeLaseOnCoordinate + -- @param #SPOT self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- LaseOnCoordinate Handler OnAfter for SPOT. + -- @function [parent=#SPOT] OnAfterLaseOnCoordinate + -- @param #SPOT self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- LaseOnCoordinate Trigger for SPOT. + -- @function [parent=#SPOT] LaseOnCoordinate + -- @param #SPOT self + -- @param Core.Point#COORDINATE Coordinate The coordinate to lase. + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. + + --- LaseOn Asynchronous Trigger for SPOT + -- @function [parent=#SPOT] __LaseOn + -- @param #SPOT self + -- @param #number Delay + -- @param Wrapper.Positionable#POSITIONABLE Target + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. + self:AddTransition( "On", "Lasing", "On" ) @@ -194,7 +227,8 @@ do return self end - --- @param #SPOT self + --- On after LaseOn event. Activates the laser spot. + -- @param #SPOT self -- @param From -- @param Event -- @param To @@ -226,6 +260,43 @@ do self:__Lasing( -1 ) end + + + --- On after LaseOnCoordinate event. Activates the laser spot. + -- @param #SPOT self + -- @param From + -- @param Event + -- @param To + -- @param Core.Point#COORDINATE Coordinate The coordinate at which the laser is pointing. + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. + function SPOT:onafterLaseOnCoordinate(From, Event, To, Coordinate, LaserCode, Duration) + self:F( { "LaseOnCoordinate", Coordinate, LaserCode, Duration } ) + + local function StopLase( self ) + self:LaseOff() + end + + self.Target = nil + self.TargetCoord=Coordinate + self.LaserCode = LaserCode + + self.Lasing = true + + local RecceDcsUnit = self.Recce:GetDCSObject() + + local aimat=Coordinate + aimat.y=aimat.y+1 + + self.SpotIR = Spot.createInfraRed( RecceDcsUnit, { x = 0, y = 2, z = 0 }, aimat:GetVec3() ) + self.SpotLaser = Spot.createLaser( RecceDcsUnit, { x = 0, y = 2, z = 0 }, aimat:GetVec3(), LaserCode ) + + if Duration then + self.ScheduleID = self.LaseScheduler:Schedule( self, StopLase, {self}, Duration ) + end + + self:__Lasing(-1) + end --- @param #SPOT self -- @param Core.Event#EVENTDATA EventData @@ -246,10 +317,20 @@ do -- @param To function SPOT:onafterLasing( From, Event, To ) - if self.Target:IsAlive() then + if self.Target and self.Target:IsAlive() then self.SpotIR:setPoint( self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/100):AddX(math.random(-100,100)/100):GetVec3() ) self.SpotLaser:setPoint( self.Target:GetPointVec3():AddY(1):GetVec3() ) self:__Lasing( -0.2 ) + elseif self.TargetCoord then + + local aimat=self.TargetCoord --Core.Point#COORDINATE + aimat.y=aimat.y+1+math.random(-100,100)/100 + aimat.x=aimat.x+math.random(-100,100)/100 + + self.SpotIR:setPoint(aimat:GetVec3()) + self.SpotLaser:setPoint(self.TargetCoord:GetVec3()) + + self:__Lasing( -0.2 ) else self:F( { "Target is not alive", self.Target:IsAlive() } ) end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 7cfb56abb..a1302776d 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1388,7 +1388,7 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType ) DCSTask = { id = 'FireAtPoint', params = { point = Vec2, - radius = Radius, + zoneRadius = Radius, expendQty = 100, -- dummy value expendQtyEnabled = false, } @@ -3165,7 +3165,8 @@ function CONTROLLABLE:OptionAlarmStateAuto() if self:IsGround() then Controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.AUTO) elseif self:IsShip() then - Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.AUTO) + --Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.AUTO) + Controller:setOption(9, 0) end return self @@ -3189,6 +3190,7 @@ function CONTROLLABLE:OptionAlarmStateGreen() elseif self:IsShip() then -- AI.Option.Naval.id.ALARM_STATE does not seem to exist! --Controller:setOption( AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.GREEN ) + Controller:setOption(9, 1) end return self @@ -3210,7 +3212,8 @@ function CONTROLLABLE:OptionAlarmStateRed() if self:IsGround() then Controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED) elseif self:IsShip() then - Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.RED) + --Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.RED) + Controller:setOption(9, 2) end return self diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 443192ed8..7f5bdd17f 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -1184,6 +1184,24 @@ function POSITIONABLE:LaseUnit( Target, LaserCode, Duration ) --R2.1 end +--- Start Lasing a COORDINATE. +-- @param #POSITIONABLE self +-- @param Core.Point#COORDIUNATE Coordinate The coordinate where the lase is pointing at. +-- @param #number LaserCode Laser code or random number in [1000, 9999]. +-- @param #number Duration Duration of lasing in seconds. +-- @return Core.Spot#SPOT +function POSITIONABLE:LaseCoordinate(Coordinate, LaserCode, Duration) + self:F2() + + LaserCode = LaserCode or math.random(1000, 9999) + + self.Spot = SPOT:New(self) -- Core.Spot#SPOT + self.Spot:LaseOnCoordinate(Coordinate, LaserCode, Duration) + self.LaserCode = LaserCode + + return self.Spot +end + --- Stop Lasing a POSITIONABLE -- @param #POSITIONABLE self -- @return #POSITIONABLE From 1a534108bf8afc050a6e466be8ff7edfcbc20dd3 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 24 Jun 2019 14:19:36 +0200 Subject: [PATCH 307/485] Updates ARTY v1.1.0 - added attack group - target data struckture WAREHOUSE v0.9.4 - assignment as descriptor AIRBOSS v1.0.3 - removed warehouse table - no stoping of recovery window if pattern is not empty - changes in patrol route --- .../Moose/Functional/Artillery.lua | 156 +++++++++++++++- .../Moose/Functional/Warehouse.lua | 19 +- Moose Development/Moose/Ops/Airboss.lua | 172 ++++++++++++++---- 3 files changed, 303 insertions(+), 44 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 637560d03..9e389bebe 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -40,7 +40,7 @@ -- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. -- @field #table targets All targets assigned. -- @field #table moves All moves assigned. --- @field #table currentTarget Holds the current target, if there is one assigned. +-- @field #ARTY.Target currentTarget Holds the current target, if there is one assigned. -- @field #table currentMove Holds the current commanded move, if there is one assigned. -- @field #number Nammo0 Initial amount total ammunition (shells+rockets+missiles) of the whole group. -- @field #number Nshells0 Initial amount of shells of the whole group. @@ -668,13 +668,28 @@ ARTY.db={ }, } +--- Target. +-- @type ARTY.Target +-- @field #string name Name of target. +-- @field Core.Point#COORDINATE coord Target coordinates. +-- @field #number radius Shelling radius in meters. +-- @field #number nshells Number of shells (or other weapon types) fired upon target. +-- @field #number engaged Number of times this target was engaged. +-- @field #boolean underfire If true, target is currently under fire. +-- @field #number prio Priority of target. +-- @field #number maxengage Max number of times, the target will be engaged. +-- @field #number time Abs. mission time in seconds, when the target is scheduled to be attacked. +-- @field #number weapontype Type of weapon used for engagement. See #ARTY.WeaponType. +-- @field #number Tassigned Abs. mission time when target was assigned. +-- @field #boolean attackgroup If true, use task attack group rather than fire at point for engagement. + --- Some ID to identify who we are in output of the DCS.log file. -- @field #string id ARTY.id="ARTY | " --- Arty script version. -- @field #string version -ARTY.version="1.0.7" +ARTY.version="1.1.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1209,6 +1224,97 @@ function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, w return _name end +--- Assign a target group to the ARTY group. Note that this will use the Attack Group Task rather than the Fire At Point Task. +-- @param #ARTY self +-- @param Wrapper.Group#GROUP group Target group. +-- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. +-- @param #number radius (Optional) Radius. Default is 100 m. +-- @param #number nshells (Optional) How many shells (or rockets) are fired on target per engagement. Default 5. +-- @param #number maxengage (Optional) How many times a target is engaged. Default 1. +-- @param #string time (Optional) Day time at which the target should be engaged. Passed as a string in format "08:13:45". Current task will be canceled. +-- @param #number weapontype (Optional) Type of weapon to be used to attack this target. Default ARTY.WeaponType.Auto, i.e. the DCS logic automatically determins the appropriate weapon. +-- @param #string name (Optional) Name of the target. Default is LL DMS coordinate of the target. If the name was already given, the numbering "#01", "#02",... is appended automatically. +-- @param #boolean unique (Optional) Target is unique. If the target name is already known, the target is rejected. Default false. +-- @return #string Name of the target. Can be used for further reference, e.g. deleting the target from the list. +-- @usage paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 10, 300, 10, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1") +-- paladin:Start() +function ARTY:AssignAttackGroup(group, prio, radius, nshells, maxengage, time, weapontype, name, unique) + + -- Set default values. + nshells=nshells or 5 + radius=radius or 100 + maxengage=maxengage or 1 + prio=prio or 50 + prio=math.max( 1, prio) + prio=math.min(100, prio) + if unique==nil then + unique=false + end + weapontype=weapontype or ARTY.WeaponType.Auto + + -- TODO Check if we have a group object. + if type(group)=="string" then + group=GROUP:FindByName(group) + end + + if group and group:IsAlive() then + + local coord=group:GetCoordinate() + + -- Name of the target. + local _name=group:GetName() + local _unique=true + + -- Check if the name has already been used for another target. If so, the function returns a new unique name. + _name,_unique=self:_CheckName(self.targets, _name, not unique) + + -- Target name should be unique and is not. + if unique==true and _unique==false then + self:T(ARTY.id..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name)) + return nil + end + + -- Time in seconds. + local _time + if type(time)=="string" then + _time=self:_ClockToSeconds(time) + elseif type(time)=="number" then + _time=timer.getAbsTime()+time + else + _time=timer.getAbsTime() + end + + -- Prepare target array. + local target={} --#ARTY.Target + target.attackgroup=true + target.name=_name + target.coord=coord + target.radius=radius + target.nshells=nshells + target.engaged=0 + target.underfire=false + target.prio=prio + target.time=_time + target.maxengage=maxengage + target.weapontype=weapontype + + -- Add to table. + table.insert(self.targets, target) + + -- Trigger new target event. + self:__NewTarget(1, target) + + return _name + else + self:E("ERROR: Group does not exist!") + end + + return nil +end + + + --- Assign coordinate to where the ARTY group should move. -- @param #ARTY self -- @param Core.Point#COORDINATE coord Coordinates of the new position. @@ -2766,7 +2872,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #table target Array holding the target info. +-- @param #ARTY.Target target Array holding the target info. function ARTY:onafterOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onafterOpenFire", Event, From, To) @@ -2828,7 +2934,11 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) --end -- Start firing. - self:_FireAtCoord(target.coord, target.radius, target.nshells, target.weapontype) + if target.attackgroup then + self:_AttackGroup(target) + else + self:_FireAtCoord(target.coord, target.radius, target.nshells, target.weapontype) + end end @@ -3321,6 +3431,39 @@ function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) group:SetTask(fire) end +--- Set task for firing at a coordinate. +-- @param #ARTY self +-- @param #ARTY.Target target Target data. +function ARTY:_AttackGroup(target) + + -- Controllable. + local group=self.Controllable --Wrapper.Group#GROUP + + local weapontype=target.weapontype + + -- Tactical nukes are actually cannon shells. + if weapontype==ARTY.WeaponType.TacticalNukes or weapontype==ARTY.WeaponType.IlluminationShells or weapontype==ARTY.WeaponType.SmokeShells then + weapontype=ARTY.WeaponType.Cannon + end + + -- Set ROE to weapon free. + group:OptionROEOpenFire() + + -- Target group. + local targetgroup=GROUP:FindByName(target.name) + + -- Get task. + local fire=group:TaskAttackGroup(targetgroup, weapontype, AI.Task.WeaponExpend.ONE, 1) + + self:E("FF") + self:E(fire) + + -- Execute task. + group:PushTask(fire) +end + + + --- Model a nuclear blast/destruction by creating fires and destroy scenery. -- @param #ARTY self -- @param Core.Point#COORDINATE _coord Coordinate of the impact point (center of the blast). @@ -4860,13 +5003,14 @@ end --- Returns the target parameters as formatted string. -- @param #ARTY self +-- @param #ARTY.Target target The target data. -- @return #string name, prio, radius, nshells, engaged, maxengage, time, weapontype function ARTY:_TargetInfo(target) local clock=tostring(self:_SecondsToClock(target.time)) local weapon=self:_WeaponTypeName(target.weapontype) local _underfire=tostring(target.underfire) - return string.format("%s: prio=%d, radius=%d, nshells=%d, engaged=%d/%d, weapontype=%s, time=%s, underfire=%s", - target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, weapon, clock,_underfire) + return string.format("%s: prio=%d, radius=%d, nshells=%d, engaged=%d/%d, weapontype=%s, time=%s, underfire=%s, attackgroup=%s", + target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, weapon, clock,_underfire, tostring(target.attackgroup)) end --- Returns a formatted string with information about all move parameters. diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 802e22275..0ff01cfbd 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1634,11 +1634,13 @@ WAREHOUSE = { -- @field #string UNITTYPE Typename of the DCS unit, e.g. "A-10C". -- @field #string ATTRIBUTE Generalized attribute @{#WAREHOUSE.Attribute}. -- @field #string CATEGORY Asset category of type DCS#Group.Category, i.e. GROUND, AIRPLANE, HELICOPTER, SHIP, TRAIN. +-- @field #string ASSIGNMENT Assignment of asset when it was added. WAREHOUSE.Descriptor = { GROUPNAME="templatename", UNITTYPE="unittype", ATTRIBUTE="attribute", CATEGORY="category", + ASSIGNMENT="assignment", } --- Generalized asset attributes. Can be used to request assets with certain general characteristics. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit. @@ -1743,7 +1745,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.9.2" +WAREHOUSE.version="0.9.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -3684,7 +3686,7 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu self:_DebugMessage(string.format("Warehouse %s: Adding %d NEW assets of group %s to stock.", self.alias, n, tostring(group:GetName())), 5) -- This is a group that is not in the db yet. Add it n times. - local assets=self:_RegisterAsset(group, n, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill) + local assets=self:_RegisterAsset(group, n, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill, assignment) -- Add created assets to stock of this warehouse. for _,asset in pairs(assets) do @@ -3720,8 +3722,9 @@ end -- @param #number loadradius Radius in meters when cargo is loaded into the carrier. -- @param #table liveries Table of liveries. -- @param DCS#AI.Skill skill Skill of AI. +-- @param #string assignment Assignment attached to the asset item. -- @return #table A table containing all registered assets. -function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill) +function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill, assignment) self:F({groupname=group:GetName(), ngroups=ngroups, forceattribute=forceattribute, forcecargobay=forcecargobay, forceweight=forceweight}) -- Set default. @@ -3823,6 +3826,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, asset.livery=liveries[math.random(#liveries)] end asset.skill=skill + asset.assignment=assignment if i==1 then self:_AssetItemInfo(asset) @@ -3937,8 +3941,15 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto okay=false end + elseif AssetDescriptor==WAREHOUSE.Descriptor.ASSIGNMENT then + + if type(AssetDescriptorValue)~="string" then + self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a string!", 5) + okay=false + end + else - self:_ErrorMessage("ERROR: Invalid request. Asset descriptor is not ATTRIBUTE, CATEGORY, GROUPNAME or UNITTYPE!", 5) + self:_ErrorMessage("ERROR: Invalid request. Asset descriptor is not ATTRIBUTE, CATEGORY, GROUPNAME, UNITTYPE or ASSIGNMENT!", 5) okay=false end diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index e6b13ad69..385016112 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -16,7 +16,7 @@ -- * Advanced F10 radio menu including carrier info, weather, radio frequencies, TACAN/ICLS channels, player LSO grades, 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 players to sections (WIP). +-- * Combine multiple human players to sections. -- * Many parameters customizable by convenient user API functions. -- * Multiple carrier support due to object oriented approach. -- * Unlimited number of players. @@ -32,7 +32,7 @@ -- **Supported Aircraft:** -- -- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI) --- * [F-14B Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI) [**WIP**] +-- * [F-14B Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI) -- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI) -- * [AV-8B N/A Harrier](https://forums.eagle.ru/forumdisplay.php?f=555) (Player & AI) [**WIP**] -- * F/A-18C Hornet (AI) @@ -46,10 +46,8 @@ -- the no other fixed wing aircraft (human or AI controlled) are supposed to land on the Tarawa. Currently only Case I is supported. Case II/III take slightly steps from the CVN carrier. -- However, the two Case II/III pattern are very similar so this is not a big drawback. -- --- Heatblur's mighty F-14B Tomcat has just been added (March 13th 2019). Beware that this is currently WIP - both the module and the AIRBOSS implementation. +-- Heatblur's mighty F-14B Tomcat has been added (March 13th 2019) as well. -- --- **PLEASE NOTE** that his class is work in progress. 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 -- @@ -174,7 +172,6 @@ -- @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. -- @field DCS#Vec3 Corientation Carrier orientation in space. -- @field DCS#Vec3 Corientlast Last known carrier orientation. -- @field Core.Point#COORDINATE Cposition Carrier position. @@ -1182,7 +1179,6 @@ AIRBOSS = { NmaxStack = nil, handleai = nil, tanker = nil, - warehouse = nil, Corientation = nil, Corientlast = nil, Cposition = nil, @@ -1685,7 +1681,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.0.2" +AIRBOSS.version="1.0.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1802,6 +1798,15 @@ function AIRBOSS:New(carriername, alias) -- Init player scores table. self.playerscores={} + + -- Initialize ME waypoints. + self:_InitWaypoints() + + -- Current waypoint. + self.currentwp=1 + + -- Patrol route. + self:_PatrolRoute() ------------- --- Defaults: @@ -2432,6 +2437,19 @@ function AIRBOSS:SetExcludeAI(setgroup) return self end +--- Add a group to the exclude set. If no set exists, it is created. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group The group to be excluded. +-- @return #AIRBOSS self +function AIRBOSS:AddExcludeAI(group) + + self.excludesetAI=self.excludesetAI or SET_GROUP:New() + + self.excludesetAI:AddGroup(group) + + 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. @@ -3044,15 +3062,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. --- @return #AIRBOSS self -function AIRBOSS:SetWarehouse(warehouse) - self.warehouse=warehouse - return self -end - --- Set default player skill. New players will be initialized with this skill. -- -- * "Flight Student" = @{#AIRBOSS.Difficulty.Easy} @@ -3202,9 +3211,6 @@ function AIRBOSS:onafterStart(From, Event, To) self.Corientlast=self.Corientation self.Tpupdate=timer.getTime() - -- Init patrol route of carrier. - self:_PatrolRoute(1) - -- Check if no recovery window is set. DISABLED! if #self.recoverytimes==0 and false then @@ -3232,9 +3238,6 @@ function AIRBOSS:onafterStart(From, Event, To) self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) self:HandleEvent(EVENTS.MissionEnd) - -- DCS event handler. - --world.addEventHandler(self) - -- Start status check in 1 second. self:__Status(1) end @@ -3617,15 +3620,27 @@ function AIRBOSS:_CheckRecoveryTimes() if self:IsRecovering() and not recovery.OVER then - -- Set carrier to idle. - self:RecoveryStop() - state="closing now" + if #self.Qpattern>0 then + + local extmin=5*#self.Qpattern + recovery.STOP=recovery.STOP+extmin*60 + + local text=string.format("We still got flights in the pattern.\nRecovery time prolonged by %d minutes.\nNow get your act together and no more bolters!", extmin) + self:MessageToPattern(text, "AIRBOSS", "99", 10, false, nil) - -- Closed. - recovery.OPEN=false + else + + -- Set carrier to idle. + self:RecoveryStop() + state="closing now" + + -- Closed. + recovery.OPEN=false + + -- Window just closed. + recovery.OVER=true - -- Window just closed. - recovery.OVER=true + end else -- Carrier is already idle. @@ -3689,10 +3704,11 @@ function AIRBOSS:_CheckRecoveryTimes() --Debug info self:T(self.lid..string.format("Heading=%03d°, Wind=%03d° %.1f kts, Delta=%03d° ==> U-turn=%s", hdg, wind,UTILS.MpsToKnots(vwind), delta, tostring(uturn))) + + -- Time into the wind 1 day or if longer recovery time + the 5 min early. + local t=math.max(nextwindow.STOP-nextwindow.START+300, 60*60*24) - - -- Time into the wind + the 5 min early. - local t=nextwindow.STOP-nextwindow.START+300 + -- Recovery wind on deck in knots. local v=UTILS.KnotsToMps(nextwindow.SPEED) -- Check that we do not go above max possible speed. @@ -13206,6 +13222,42 @@ function AIRBOSS:_GetNextWaypoint() return nextwp,Nextwp end + +--- Initialize Mission Editor waypoints. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:_InitWaypoints() + + -- Waypoints of group as defined in the ME. + local Waypoints=self.carrier:GetGroup():GetTemplateRoutePoints() + + -- Init array. + self.waypoints={} + + -- Set waypoint table. + for i,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 + + end + + return self +end + +--[[ + --- Patrol carrier. -- @param #AIRBOSS self -- @param #number n Current waypoint. @@ -13259,6 +13311,58 @@ function AIRBOSS:_PatrolRoute(n) return self end +]] + +--- Patrol carrier. +-- @param #AIRBOSS self +-- @param #number n Next waypoint number. +-- @return #AIRBOSS self +function AIRBOSS:_PatrolRoute(n) + + -- Get next waypoint coordinate and number. + local nextWP, N=self:_GetNextWaypoint() + + -- Default resume is to next waypoint. + n=n or N + + -- Get carrier group. + local CarrierGroup=self.carrier:GetGroup() + + -- Waypoints table. + local Waypoints={} + + -- Create a waypoint from the current coordinate. + local wp=self:GetCoordinate():WaypointGround(CarrierGroup:GetVelocityKMH()) + + -- Add current position as first waypoint. + table.insert(Waypoints, wp) + + -- Loop over waypoints. + for i=n,#self.waypoints do + local coord=self.waypoints[i] --Core.Point#COORDINATE + + -- Create a waypoint from the coordinate. + local wp=coord:WaypointGround(UTILS.MpsToKmph(coord.Velocity)) + + -- Passing waypoint taskfunction + local TaskPassingWP=CarrierGroup:TaskFunction("AIRBOSS._PassingWaypoint", self, i, #self.waypoints) + + -- Call task function when carrier arrives at waypoint. + CarrierGroup:SetTaskWaypoint(wp, TaskPassingWP) + + -- Add waypoint to table. + table.insert(Waypoints, wp) + end + + -- 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. @@ -13484,7 +13588,7 @@ function AIRBOSS._PassingWaypoint(group, airboss, i, final) -- If final waypoint reached, do route all over again. if i==final and final>1 and airboss.adinfinitum then - airboss:_PatrolRoute(i) + airboss:_PatrolRoute() end end From ac1d07ec66831a7b56e43457ceb0ed1b541dc2a8 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 24 Jun 2019 23:17:18 +0200 Subject: [PATCH 308/485] updates --- Moose Development/Moose/AI/AI_Formation.lua | 2 +- .../Moose/Functional/Artillery.lua | 345 ++++++++++-------- Moose Development/Moose/Wrapper/Airbase.lua | 6 +- 3 files changed, 187 insertions(+), 166 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index fea9c7c0e..21482e930 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -1102,7 +1102,7 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 if FollowGroup:GetState( FollowGroup, "Mode" ) == self.__Enum.Mode.Formation then - self:I({Mode=FollowGroup:GetState( FollowGroup, "Mode" )}) + self:T({Mode=FollowGroup:GetState( FollowGroup, "Mode" )}) FollowGroup:OptionROTEvadeFire() FollowGroup:OptionROEReturnFire() diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 9e389bebe..e206d9962 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -98,7 +98,8 @@ -- @field #boolean markreadonly Marks for targets are readonly and cannot be removed by players. Default is false. -- @field #boolean autorelocate ARTY group will automatically move to within the max/min firing range. -- @field #number autorelocatemaxdist Max distance [m] the ARTY group will travel to get within firing range. Default 50000 m = 50 km. --- @field #boolean autorelocateonroad ARTY group will use mainly road to automatically get within firing range. Default is false. +-- @field #boolean autorelocateonroad ARTY group will use mainly road to automatically get within firing range. Default is false. +-- @field #number coalition The coalition of the arty group. -- @extends Core.Fsm#FSM_CONTROLLABLE --- Enables mission designers easily to assign targets for artillery units. Since the implementation is based on a Finite State Model (FSM), the mission designer can @@ -587,6 +588,7 @@ ARTY={ autorelocate=false, autorelocatemaxdist=50000, autorelocateonroad=false, + coalition=nil, } --- Weapong type ID. See [here](http://wiki.hoggit.us/view/DCS_enum_weapon_flag). @@ -689,7 +691,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #string version -ARTY.version="1.1.0" +ARTY.version="1.1.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -782,6 +784,9 @@ function ARTY:New(group, alias) -- Set the group name self.groupname=group:GetName() + -- Get coalition. + self.coalition=group:GetCoalition() + -- Set an alias name. if alias~=nil then self.alias=tostring(alias) @@ -1931,8 +1936,8 @@ function ARTY:onafterStart(Controllable, From, Event, To) self.Controllable:OptionROEHoldFire() -- Add event handler. - self:HandleEvent(EVENTS.Shot, self._OnEventShot) - self:HandleEvent(EVENTS.Dead, self._OnEventDead) + self:HandleEvent(EVENTS.Shot) --, self._OnEventShot) + self:HandleEvent(EVENTS.Dead) --, self._OnEventDead) --self:HandleEvent(EVENTS.MarkAdded, self._OnEventMarkAdded) -- Add DCS event handler - necessary for S_EVENT_MARK_* events. So we only start it, if this was requested. @@ -2010,7 +2015,7 @@ function ARTY:_StatusReport(display) end text=text..string.format("******************************************************") env.info(ARTY.id..text) - MESSAGE:New(text, 20):Clear():ToCoalitionIf(self.Controllable:GetCoalition(), display) + MESSAGE:New(text, 20):Clear():ToCoalitionIf(self.coalition, display) end @@ -2021,7 +2026,7 @@ end --- Eventhandler for shot event. -- @param #ARTY self -- @param Core.Event#EVENTDATA EventData -function ARTY:_OnEventShot(EventData) +function ARTY:OnEventShot(EventData) self:F(EventData) -- Weapon data. @@ -2299,7 +2304,7 @@ end function ARTY:_OnEventMarkRemove(Event) -- Get battery coalition and name. - local batterycoalition=self.Controllable:GetCoalition() + local batterycoalition=self.coalition --local batteryname=self.groupname if Event.text~=nil and Event.text:find("BATTERY") then @@ -2385,7 +2390,7 @@ function ARTY:_OnEventMarkChange(Event) _coord.y=_coord:GetLandHeight() -- Get battery coalition and name. - local batterycoalition=self.Controllable:GetCoalition() + local batterycoalition=self.coalition local batteryname=self.groupname -- Check if the coalition is the same or an authorization key has been defined. @@ -2496,7 +2501,7 @@ function ARTY:_OnEventMarkChange(Event) if _assign.setrearmingplace and self.ismobile then self:SetRearmingPlace(_coord) _coord:RemoveMark(Event.idx) - _coord:MarkToCoalition(string.format("Rearming place for battery %s", self.groupname), self.Controllable:GetCoalition(), false, string.format("New rearming place for battery %s defined.", self.groupname)) + _coord:MarkToCoalition(string.format("Rearming place for battery %s", self.groupname), self.coalition, false, string.format("New rearming place for battery %s defined.", self.groupname)) if self.Debug then _coord:SmokeOrange() end @@ -2504,7 +2509,7 @@ function ARTY:_OnEventMarkChange(Event) if _assign.setrearminggroup then _coord:RemoveMark(Event.idx) local rearminggroupcoord=_assign.setrearminggroup:GetCoordinate() - rearminggroupcoord:MarkToCoalition(string.format("Rearming group for battery %s", self.groupname), self.Controllable:GetCoalition(), false, string.format("New rearming group for battery %s defined.", self.groupname)) + rearminggroupcoord:MarkToCoalition(string.format("Rearming group for battery %s", self.groupname), self.coalition, false, string.format("New rearming group for battery %s defined.", self.groupname)) self:SetRearmingGroup(_assign.setrearminggroup) if self.Debug then rearminggroupcoord:SmokeOrange() @@ -2608,17 +2613,19 @@ end --- Event handler for event Dead. -- @param #ARTY self -- @param Core.Event#EVENTDATA EventData -function ARTY:_OnEventDead(EventData) +function ARTY:OnEventDead(EventData) self:F(EventData) -- Name of controllable. local _name=self.groupname + + env.info("FF Dead event for arty group") -- Check for correct group. if EventData.IniGroupName==_name then -- Dead Unit. - self:T2(string.format("%s: Captured dead event for unit %s.", _name, EventData.IniUnitName)) + self:I(string.format("%s: Captured dead event for unit %s.", _name, tostring(EventData.IniUnitName))) -- FSM Dead event. We give one second for update of data base. self:__Dead(1) @@ -2639,145 +2646,155 @@ end function ARTY:onafterStatus(Controllable, From, Event, To) self:_EventFromTo("onafterStatus", Event, From, To) - -- We have a cargo group ==> check if group was loaded into a carrier. - if self.cargogroup then - if self.cargogroup:IsLoaded() and not self:is("InTransit") then - -- Group is now InTransit state. Current target is canceled. - self:T(ARTY.id..string.format("Group %s has been loaded into a carrier and is now transported.", self.alias)) - self:Loaded() - elseif self.cargogroup:IsUnLoaded() then - -- Group has been unloaded and is combat ready again. - self:T(ARTY.id..string.format("Group %s has been unloaded from the carrier.", self.alias)) - self:UnLoaded() - end - end - - -- Debug current status info. - if self.Debug then - self:_StatusReport() - end - - -- Group is being transported as cargo ==> skip everything and check again in 5 seconds. - if self:is("InTransit") then - self:__Status(-5) - return - end + -- FSM state. + local fsmstate=self:GetState() + self:I(ARTY.id..string.format("Status of group %s: %s", self.alias, fsmstate)) - -- Group on the move. - if self:is("Moving") then - self:T2(ARTY.id..string.format("%s: Moving", Controllable:GetName())) - end + if self.Controllable and self.Controllable:IsAlive() then - -- Group is rearming. - if self:is("Rearming") then - local _rearmed=self:_CheckRearmed() - if _rearmed then - self:T2(ARTY.id..string.format("%s: Rearming ==> Rearmed", Controllable:GetName())) - self:Rearmed() - end - end - - -- Group finished rearming. - if self:is("Rearmed") then - local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) - self:T2(ARTY.id..string.format("%s: Rearmed. Distance ARTY to InitalCoord = %d m", Controllable:GetName(), distance)) - -- Check that ARTY group is back and set it to combat ready. - if distance <= self.RearmingDistance then - self:T2(ARTY.id..string.format("%s: Rearmed ==> CombatReady", Controllable:GetName())) - self:CombatReady() - end - end - - -- Group arrived at destination. - if self:is("Arrived") then - self:T2(ARTY.id..string.format("%s: Arrived ==> CombatReady", Controllable:GetName())) - self:CombatReady() - end - - -- Group is firing on target. - if self:is("Firing") then - -- Check that firing started after ~5 min. If not, target is removed. - self:_CheckShootingStarted() - end - - -- Check if targets are in range and update target.inrange value. - self:_CheckTargetsInRange() - - -- Check if selected weapon type for target is possible at all. E.g. request rockets for Paladin. - local notpossible={} - for i=1,#self.targets do - local _target=self.targets[i] - local possible=self:_CheckWeaponTypePossible(_target) - if not possible then - table.insert(notpossible, _target.name) - end - end - for _,targetname in pairs(notpossible) do - self:E(ARTY.id..string.format("%s: Removing target %s because requested weapon is not possible with this type of unit.", self.groupname, targetname)) - self:RemoveTarget(targetname) - end - - -- Get a valid timed target if it is due to be attacked. - local _timedTarget=self:_CheckTimedTargets() - - -- Get a valid normal target (one that is not timed). - local _normalTarget=self:_CheckNormalTargets() - - -- Get a commaned move to another location. - local _move=self:_CheckMoves() - - if _move then - - -- Command to move. - self:Move(_move) - - elseif _timedTarget then - - -- Cease fire on current target first. - if self.currentTarget then - self:CeaseFire(self.currentTarget) - end - - -- Open fire on timed target. - self:OpenFire(_timedTarget) - - elseif _normalTarget then - - -- Open fire on normal target. - self:OpenFire(_normalTarget) - - end - - -- Get ammo. - local nammo, nshells, nrockets, nmissiles=self:GetAmmo() - - -- Check if we have a target in the queue for which weapons are still available. - local gotsome=false - if #self.targets>0 then - for i=1,#self.targets do - local _target=self.targets[i] - if self:_CheckWeaponTypeAvailable(_target)>0 then - gotsome=true + -- We have a cargo group ==> check if group was loaded into a carrier. + if self.cargogroup then + if self.cargogroup:IsLoaded() and not self:is("InTransit") then + -- Group is now InTransit state. Current target is canceled. + self:T(ARTY.id..string.format("Group %s has been loaded into a carrier and is now transported.", self.alias)) + self:Loaded() + elseif self.cargogroup:IsUnLoaded() then + -- Group has been unloaded and is combat ready again. + self:T(ARTY.id..string.format("Group %s has been unloaded from the carrier.", self.alias)) + self:UnLoaded() end end - else - -- No targets in the queue. - gotsome=true - end - -- No ammo available. Either completely blank or only queued targets for ammo which is out. - if (nammo==0 or not gotsome) and not (self:is("Moving") or self:is("Rearming") or self:is("OutOfAmmo")) then - self:Winchester() + -- Debug current status info. + if self.Debug then + self:_StatusReport() + end + + -- Group is being transported as cargo ==> skip everything and check again in 5 seconds. + if self:is("InTransit") then + self:__Status(-5) + return + end + + -- Group on the move. + if self:is("Moving") then + self:T2(ARTY.id..string.format("%s: Moving", Controllable:GetName())) + end + + -- Group is rearming. + if self:is("Rearming") then + local _rearmed=self:_CheckRearmed() + if _rearmed then + self:T2(ARTY.id..string.format("%s: Rearming ==> Rearmed", Controllable:GetName())) + self:Rearmed() + end + end + + -- Group finished rearming. + if self:is("Rearmed") then + local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) + self:T2(ARTY.id..string.format("%s: Rearmed. Distance ARTY to InitalCoord = %d m", Controllable:GetName(), distance)) + -- Check that ARTY group is back and set it to combat ready. + if distance <= self.RearmingDistance then + self:T2(ARTY.id..string.format("%s: Rearmed ==> CombatReady", Controllable:GetName())) + self:CombatReady() + end + end + + -- Group arrived at destination. + if self:is("Arrived") then + self:T2(ARTY.id..string.format("%s: Arrived ==> CombatReady", Controllable:GetName())) + self:CombatReady() + end + + -- Group is firing on target. + if self:is("Firing") then + -- Check that firing started after ~5 min. If not, target is removed. + self:_CheckShootingStarted() + end + + -- Check if targets are in range and update target.inrange value. + self:_CheckTargetsInRange() + + -- Check if selected weapon type for target is possible at all. E.g. request rockets for Paladin. + local notpossible={} + for i=1,#self.targets do + local _target=self.targets[i] + local possible=self:_CheckWeaponTypePossible(_target) + if not possible then + table.insert(notpossible, _target.name) + end + end + for _,targetname in pairs(notpossible) do + self:E(ARTY.id..string.format("%s: Removing target %s because requested weapon is not possible with this type of unit.", self.groupname, targetname)) + self:RemoveTarget(targetname) + end + + -- Get a valid timed target if it is due to be attacked. + local _timedTarget=self:_CheckTimedTargets() + + -- Get a valid normal target (one that is not timed). + local _normalTarget=self:_CheckNormalTargets() + + -- Get a commaned move to another location. + local _move=self:_CheckMoves() + + if _move then + + -- Command to move. + self:Move(_move) + + elseif _timedTarget then + + -- Cease fire on current target first. + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + -- Open fire on timed target. + self:OpenFire(_timedTarget) + + elseif _normalTarget then + + -- Open fire on normal target. + self:OpenFire(_normalTarget) + + end + + -- Get ammo. + local nammo, nshells, nrockets, nmissiles=self:GetAmmo() + + -- Check if we have a target in the queue for which weapons are still available. + local gotsome=false + if #self.targets>0 then + for i=1,#self.targets do + local _target=self.targets[i] + if self:_CheckWeaponTypeAvailable(_target)>0 then + gotsome=true + end + end + else + -- No targets in the queue. + gotsome=true + end + + -- No ammo available. Either completely blank or only queued targets for ammo which is out. + if (nammo==0 or not gotsome) and not (self:is("Moving") or self:is("Rearming") or self:is("OutOfAmmo")) then + self:Winchester() + end + + -- Group is out of ammo. + if self:is("OutOfAmmo") then + self:T2(ARTY.id..string.format("%s: OutOfAmmo ==> Rearm ==> Rearming", Controllable:GetName())) + self:Rearm() + end + + -- Call status again in ~10 sec. + self:__Status(self.StatusInterval) + + else + self:E(string.format("Arty group %s is not alive!", self.groupname)) end - - -- Group is out of ammo. - if self:is("OutOfAmmo") then - self:T2(ARTY.id..string.format("%s: OutOfAmmo ==> Rearm ==> Rearming", Controllable:GetName())) - self:Rearm() - end - - -- Call status again in ~10 sec. - self:__Status(self.StatusInterval) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2925,7 +2942,7 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) -- Send message. local text=string.format("%s, opening fire on target %s with %d %s. Distance %.1f km.", Controllable:GetName(), target.name, target.nshells, _type, range/1000) self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report) --if self.Debug then -- local _coord=target.coord --Core.Point#COORDINATE @@ -2959,7 +2976,7 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) -- Send message. local text=string.format("%s, ceasing fire on target %s.", Controllable:GetName(), target.name) self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report) -- Get target array index. local id=self:_GetTargetIndexByName(target.name) @@ -3013,7 +3030,7 @@ function ARTY:onafterWinchester(Controllable, From, Event, To) -- Send message. local text=string.format("%s, winchester!", Controllable:GetName()) self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) end @@ -3079,7 +3096,7 @@ function ARTY:onafterRearm(Controllable, From, Event, To) -- Send message. local text=string.format("%s, %s, request rearming at rearming place.", Controllable:GetName(), self.RearmingGroup:GetName()) self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) -- Distances. local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) @@ -3104,7 +3121,7 @@ function ARTY:onafterRearm(Controllable, From, Event, To) -- Send message. local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingGroup:GetName()) self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) -- Distance between ARTY group and rearming unit. local distance=coordARTY:Get2DDistance(coordRARM) @@ -3123,7 +3140,7 @@ function ARTY:onafterRearm(Controllable, From, Event, To) -- Send message. local text=string.format("%s, moving to rearming place.", Controllable:GetName()) self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) -- Distance. local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) @@ -3152,7 +3169,7 @@ function ARTY:onafterRearmed(Controllable, From, Event, To) -- Send message. local text=string.format("%s, rearming complete.", Controllable:GetName()) self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) -- "Rearm" tactical nukes as well. self.Nukes=self.Nukes0 @@ -3204,7 +3221,7 @@ function ARTY:_CheckRearmed() if _rearmpc>1 then local text=string.format("%s, rearming %d %% complete.", self.alias, _rearmpc) self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) end -- Return if ammo is full. @@ -3299,7 +3316,7 @@ function ARTY:onafterArrived(Controllable, From, Event, To) -- Send message local text=string.format("%s, arrived at destination.", Controllable:GetName()) self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) -- Remove executed move from queue. if self.currentMove then @@ -3354,16 +3371,20 @@ function ARTY:onafterDead(Controllable, From, Event, To) self:_EventFromTo("onafterDead", Event, From, To) -- Number of units left in the group. + --[[ local units=self.Controllable:GetUnits() local nunits=0 if units~=nil then nunits=#units end + ]] + + local nunits=self.Controllable:CountAliveUnits() -- Message. local text=string.format("%s, one of our units just died! %d units left.", self.groupname, nunits) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:T(ARTY.id..text) + self:I(ARTY.id..text) -- Go to stop state. if nunits==0 then @@ -3382,7 +3403,7 @@ function ARTY:onafterStop(Controllable, From, Event, To) self:_EventFromTo("onafterStop", Event, From, To) -- Debug info. - self:T(ARTY.id..string.format("Stopping ARTY FSM for group %s.", Controllable:GetName())) + self:I(ARTY.id..string.format("Stopping ARTY FSM for group %s.", tostring(Controllable:GetName()))) -- Cease Fire on current target. if self.currentTarget then @@ -3933,7 +3954,7 @@ function ARTY:_MarkerKeyAuthentification(text) -- Set battery and coalition. --local batteryname=self.groupname - local batterycoalition=self.Controllable:GetCoalition() + local batterycoalition=self.coalition -- Get assignment. local mykey=nil @@ -4248,7 +4269,7 @@ function ARTY:_MarkRequestMoves() else text=text..string.format("\n- no queued relocations") end - MESSAGE:New(text, 20):Clear():ToCoalition(self.Controllable:GetCoalition()) + MESSAGE:New(text, 20):Clear():ToCoalition(self.coalition) end --- Request Targets. @@ -4266,7 +4287,7 @@ function ARTY:_MarkRequestTargets() else text=text..string.format("\n- no queued targets") end - MESSAGE:New(text, 20):Clear():ToCoalition(self.Controllable:GetCoalition()) + MESSAGE:New(text, 20):Clear():ToCoalition(self.coalition) end --- Create a name for an engagement initiated by placing a marker. @@ -4448,7 +4469,7 @@ function ARTY:_CheckTargetsInRange() -- 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) + MESSAGE:New(text,10):ToCoalitionIf(self.coalition, self.report or self.Debug) end end @@ -4485,7 +4506,7 @@ function ARTY:_CheckTargetsInRange() end -- Send info message. - MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.coalition, self.report or self.Debug) -- Assign relocation move. self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true) @@ -4918,7 +4939,7 @@ function ARTY:_TargetInRange(target, message) -- Debug output. if not _inrange then self:T(ARTY.id..text) - MESSAGE:New(text, 5):ToCoalitionIf(self.Controllable:GetCoalition(), (self.report and message) or (self.Debug and message)) + MESSAGE:New(text, 5):ToCoalitionIf(self.coalition, (self.report and message) or (self.Debug and message)) end -- Remove target if ARTY group cannot move, e.g. Mortas. No chance to be ever in range - unless they are cargo. diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index ab38769f4..2de2ef7e2 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -553,14 +553,14 @@ function AIRBASE:GetParkingSpotsTable(termtype) local spots={} for _,_spot in pairs(parkingdata) do if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then - self:I({_spot=_spot}) + self:T2({_spot=_spot}) local _free=_isfree(_spot) local _coord=COORDINATE:NewFromVec3(_spot.vTerminalPos) table.insert(spots, {Coordinate=_coord, TerminalID=_spot.Term_Index, TerminalType=_spot.Term_Type, TOAC=_spot.TO_AC, Free=_free, TerminalID0=_spot.Term_Index_0, DistToRwy=_spot.fDistToRW}) end end - self:I({ spots = spots } ) + self:T2({ spots = spots } ) return spots end @@ -711,7 +711,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE local _termid=parkingspot.TerminalID - self:I({_termid=_termid}) + self:T2({_termid=_termid}) if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) then From 2a7b9cf8986073b1b809164ffcc576e0e8301d12 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 26 Jun 2019 19:19:17 +0200 Subject: [PATCH 309/485] DETECTION, SET, ARTY, WAREHOUSE, AIRBOSS, A2A_DISPATCHER DETECTION - Fixed bug with late activated groups. ARTY v1.1.2 - Added attack group task for ships. - Added respawn option. WAREHOUSE v0.9.5 - Added respawn option. AIRBOSS v1.0.3 - Recovery time extended if flights are still in the pattern. SET - Added CountAlive() function. A2A_DISPATCHER - Bug fixes in :ParkDefender() function. --- .../Moose/AI/AI_A2A_Dispatcher.lua | 8 +- Moose Development/Moose/Core/Set.lua | 29 +++ .../Moose/Functional/Artillery.lua | 184 +++++++++++++++--- .../Moose/Functional/Detection.lua | 3 +- .../Moose/Functional/Warehouse.lua | 89 +++++++-- Moose Development/Moose/Ops/Airboss.lua | 8 +- 6 files changed, 269 insertions(+), 52 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 7396cb7a6..cd1836f6f 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -1095,7 +1095,7 @@ do -- AI_A2A_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() - self:ParkDefender( Squadron, Defender ) + self:ParkDefender( Squadron ) return end if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then @@ -1122,7 +1122,7 @@ do -- AI_A2A_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() - self:ParkDefender( Squadron, Defender ) + self:ParkDefender( Squadron ) end end end @@ -2873,7 +2873,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 ) + Dispatcher:ParkDefender( Squadron ) end end end @@ -3060,7 +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 ) + Dispatcher:ParkDefender( Squadron ) end end end -- if DefenderGCI then diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index f0e24adf5..20554631b 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -1571,6 +1571,35 @@ do -- SET_GROUP return Count end + --- Iterate the SET_GROUP and count how many GROUPs and UNITs are alive. + -- @param #SET_GROUP self + -- @return #number The number of GROUPs completely in the Zone + -- @return #number The number of UNITS alive. + function SET_GROUP:CountAlive() + local CountG = 0 + local CountU = 0 + + local Set = self:GetSet() + + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if GroupData and GroupData:IsAlive() then + + CountG = CountG + 1 + + --Count Units. + for _,_unit in pairs(GroupData:GetUnits()) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + CountU=CountU+1 + end + end + end + + end + + return CountG,CountU + 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. diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index e206d9962..e5d30114c 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -100,6 +100,8 @@ -- @field #number autorelocatemaxdist Max distance [m] the ARTY group will travel to get within firing range. Default 50000 m = 50 km. -- @field #boolean autorelocateonroad ARTY group will use mainly road to automatically get within firing range. Default is false. -- @field #number coalition The coalition of the arty group. +-- @field #boolean respawnafterdeath Respawn arty group after all units are dead. +-- @field #number respawndelay Respawn delay in seconds. -- @extends Core.Fsm#FSM_CONTROLLABLE --- Enables mission designers easily to assign targets for artillery units. Since the implementation is based on a Finite State Model (FSM), the mission designer can @@ -589,6 +591,8 @@ ARTY={ autorelocatemaxdist=50000, autorelocateonroad=false, coalition=nil, + respawnafterdeath=false, + respawndelay=nil } --- Weapong type ID. See [here](http://wiki.hoggit.us/view/DCS_enum_weapon_flag). @@ -691,7 +695,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #string version -ARTY.version="1.1.1" +ARTY.version="1.1.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -863,6 +867,7 @@ function ARTY:New(group, alias) self:AddTransition("*", "Status", "*") self:AddTransition("*", "NewMove", "*") self:AddTransition("*", "Dead", "*") + self:AddTransition("*", "Respawn", "CombatReady") -- Transport as cargo (not in diagram). self:AddTransition("*", "Loaded", "InTransit") @@ -973,7 +978,15 @@ function ARTY:New(group, alias) -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. + -- @param #string Unitname Name of the dead unit. + --- User function for OnAfter "Respawn" event. + -- @function [parent=#ARTY] OnAfterRespawn + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. --- User function for OnEnter "CombatReady" state. -- @function [parent=#ARTY] OnEnterCombatReady @@ -1021,7 +1034,7 @@ function ARTY:New(group, alias) -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. - -- @param #string To To state. + -- @param #string To To state. --- Function to start the ARTY FSM process. @@ -1045,11 +1058,13 @@ function ARTY:New(group, alias) --- Function called when a unit of the ARTY group died. Triggers the FSM event "Dead". -- @function [parent=#ARTY] Dead -- @param #ARTY self + -- @param #string unitname Name of the unit that died. --- Function called when a unit of the ARTY group died after a delay. Triggers the FSM event "Dead". -- @function [parent=#ARTY] __Dead -- @param #ARTY self -- @param #number Delay in seconds. + -- @param #string unitname Name of the unit that died. --- Add a new target for the ARTY group. Triggers the FSM event "NewTarget". -- @function [parent=#ARTY] NewTarget @@ -1133,7 +1148,15 @@ function ARTY:New(group, alias) -- @param #ARTY self -- @param #number delay Delay in seconds. - + --- Respawn ARTY group. + -- @function [parent=#ARTY] Respawn + -- @param #ARTY self + + --- Respawn ARTY group after a delay. + -- @function [parent=#ARTY] __Respawn + -- @param #ARTY self + -- @param #number delay Delay in seconds. + return self end @@ -1399,14 +1422,17 @@ end --- Set alias, i.e. the name the group will use when sending messages. -- @param #ARTY self -- @param #string alias The alias for the group. +-- @return self function ARTY:SetAlias(alias) self:F({alias=alias}) self.alias=tostring(alias) + return self end --- Add ARTY group to one or more clusters. Enables addressing all ARTY groups within a cluster simultaniously via marker assignments. -- @param #ARTY self -- @param #table clusters Table of cluster names the group should belong to. +-- @return self function ARTY:AddToCluster(clusters) self:F({clusters=clusters}) @@ -1425,99 +1451,122 @@ function ARTY:AddToCluster(clusters) -- Add names to cluster array. for _,cluster in pairs(names) do table.insert(self.clusters, cluster) - end + end + + return self end --- Set minimum firing range. Targets closer than this distance are not engaged. -- @param #ARTY self -- @param #number range Min range in kilometers. Default is 0.1 km. +-- @return self function ARTY:SetMinFiringRange(range) self:F({range=range}) self.minrange=range*1000 or 100 + return self end --- Set maximum firing range. Targets further away than this distance are not engaged. -- @param #ARTY self -- @param #number range Max range in kilometers. Default is 1000 km. +-- @return self function ARTY:SetMaxFiringRange(range) self:F({range=range}) self.maxrange=range*1000 or 1000*1000 + return self end --- Set time interval between status updates. During the status check, new events are triggered. -- @param #ARTY self -- @param #number interval Time interval in seconds. Default 10 seconds. +-- @return self function ARTY:SetStatusInterval(interval) self:F({interval=interval}) self.StatusInterval=interval or 10 + return self end --- Set time how it is waited a unit the first shot event happens. If no shot is fired after this time, the task to fire is aborted and the target removed. -- @param #ARTY self -- @param #number waittime Time in seconds. Default 300 seconds. +-- @return self function ARTY:SetWaitForShotTime(waittime) self:F({waittime=waittime}) self.WaitForShotTime=waittime or 300 + return self end --- Define the safe distance between ARTY group and rearming unit or rearming place at which rearming process is possible. -- @param #ARTY self --- @param #number distance Safe distance in meters. Default is 100 m. +-- @param #number distance Safe distance in meters. Default is 100 m. +-- @return self function ARTY:SetRearmingDistance(distance) self:F({distance=distance}) self.RearmingDistance=distance or 100 + return self end --- Assign a group, which is responsible for rearming the ARTY group. If the group is too far away from the ARTY group it will be guided towards the ARTY group. -- @param #ARTY self -- @param Wrapper.Group#GROUP group Group that is supposed to rearm the ARTY group. For the blue coalition, this is often a unarmed M818 transport whilst for red an unarmed Ural-375 transport can be used. +-- @return self function ARTY:SetRearmingGroup(group) self:F({group=group}) self.RearmingGroup=group + return self end --- Set the speed the rearming group moves at towards the ARTY group or the rearming place. -- @param #ARTY self -- @param #number speed Speed in km/h. +-- @return self function ARTY:SetRearmingGroupSpeed(speed) self:F({speed=speed}) self.RearmingGroupSpeed=speed + return self end --- Define if rearming group uses mainly roads to drive to the ARTY group or rearming place. -- @param #ARTY self -- @param #boolean onroad If true, rearming group uses mainly roads. If false, it drives directly to the ARTY group or rearming place. +-- @return self function ARTY:SetRearmingGroupOnRoad(onroad) self:F({onroad=onroad}) if onroad==nil then onroad=true end self.RearmingGroupOnRoad=onroad + return self end --- Define if ARTY group uses mainly roads to drive to the rearming place. -- @param #ARTY self -- @param #boolean onroad If true, ARTY group uses mainly roads. If false, it drives directly to the rearming place. +-- @return self function ARTY:SetRearmingArtyOnRoad(onroad) self:F({onroad=onroad}) if onroad==nil then onroad=true end self.RearmingArtyOnRoad=onroad + return self end --- Defines the rearming place of the ARTY group. If the place is too far away from the ARTY group it will be routed to the place. -- @param #ARTY self -- @param Core.Point#COORDINATE coord Coordinates of the rearming place. +-- @return self function ARTY:SetRearmingPlace(coord) self:F({coord=coord}) self.RearmingPlaceCoord=coord + return self end --- Set automatic relocation of ARTY group if a target is assigned which is out of range. The unit will drive automatically towards or away from the target to be in max/min firing range. -- @param #ARTY self -- @param #number maxdistance (Optional) The maximum distance in km the group will travel to get within firing range. Default is 50 km. No automatic relocation is performed if targets are assigned which are further away. --- @param #boolean onroad (Optional) If true, ARTY group uses roads whenever possible. Default false, i.e. group will move in a straight line to the assigned coordinate. +-- @param #boolean onroad (Optional) If true, ARTY group uses roads whenever possible. Default false, i.e. group will move in a straight line to the assigned coordinate. +-- @return self function ARTY:SetAutoRelocateToFiringRange(maxdistance, onroad) self:F({distance=maxdistance, onroad=onroad}) self.autorelocate=true @@ -1527,12 +1576,14 @@ function ARTY:SetAutoRelocateToFiringRange(maxdistance, onroad) onroad=false end self.autorelocateonroad=onroad + return self end --- Set relocate after firing. Group will find a new location after each engagement. Default is off -- @param #ARTY self -- @param #number rmax (Optional) Max distance in meters, the group will move to relocate. Default is 800 m. -- @param #number rmin (Optional) Min distance in meters, the group will move to relocate. Default is 300 m. +-- @return self function ARTY:SetAutoRelocateAfterEngagement(rmax, rmin) self.relocateafterfire=true self.relocateRmax=rmax or 800 @@ -1540,37 +1591,59 @@ function ARTY:SetAutoRelocateAfterEngagement(rmax, rmin) -- Ensure that Rmin<=Rmax self.relocateRmin=math.min(self.relocateRmin, self.relocateRmax) + + return self end --- Report messages of ARTY group turned on. This is the default. -- @param #ARTY self +-- @return self function ARTY:SetReportON() self.report=true + return self end --- Report messages of ARTY group turned off. Default is on. -- @param #ARTY self +-- @return self function ARTY:SetReportOFF() self.report=false + return self +end + +--- Respawn group once all units are dead. +-- @param #ARTY self +-- @param #number delay (Optional) Delay before respawn in seconds. +-- @return self +function ARTY:SetRespawnOnDeath(delay) + self.respawnafterdeath=true + self.respawndelay=delay + return self end --- Turn debug mode on. Information is printed to screen. -- @param #ARTY self +-- @return self function ARTY:SetDebugON() self.Debug=true + return self end --- Turn debug mode off. This is the default setting. -- @param #ARTY self +-- @return self function ARTY:SetDebugOFF() self.Debug=false + return self end --- Set default speed the group is moving at if not specified otherwise. -- @param #ARTY self -- @param #number speed Speed in km/h. +-- @return self function ARTY:SetSpeed(speed) self.Speed=speed + return self end --- Delete a target from target list. If the target is currently engaged, it is cancelled. @@ -1639,50 +1712,60 @@ end --- Define shell types that are counted to determine the ammo amount the ARTY group has. -- @param #ARTY self -- @param #table tableofnames Table of shell type names. +-- @return self function ARTY:SetShellTypes(tableofnames) self:F2(tableofnames) self.ammoshells={} for _,_type in pairs(tableofnames) do table.insert(self.ammoshells, _type) end + return self end --- Define rocket types that are counted to determine the ammo amount the ARTY group has. -- @param #ARTY self -- @param #table tableofnames Table of rocket type names. +-- @return self function ARTY:SetRocketTypes(tableofnames) self:F2(tableofnames) self.ammorockets={} for _,_type in pairs(tableofnames) do table.insert(self.ammorockets, _type) end + return self end --- Define missile types that are counted to determine the ammo amount the ARTY group has. -- @param #ARTY self -- @param #table tableofnames Table of rocket type names. +-- @return self function ARTY:SetMissileTypes(tableofnames) self:F2(tableofnames) self.ammomissiles={} for _,_type in pairs(tableofnames) do table.insert(self.ammomissiles, _type) end + return self end --- Set number of tactical nuclear warheads available to the group. -- Note that it can be max the number of normal shells. Also if all normal shells are empty, firing nuclear shells is also not possible any more until group gets rearmed. -- @param #ARTY self -- @param #number n Number of warheads for the whole group. +-- @return self function ARTY:SetTacNukeShells(n) self.Nukes=n + return self end --- Set nuclear warhead explosion strength. -- @param #ARTY self -- @param #number strength Explosion strength in kilo tons TNT. Default is 0.075 kt. +-- @return self function ARTY:SetTacNukeWarhead(strength) self.nukewarhead=strength or 0.075 self.nukewarhead=self.nukewarhead*1000*1000 -- convert to kg TNT. + return self end --- Set number of illumination shells available to the group. @@ -1690,10 +1773,12 @@ end -- @param #ARTY self -- @param #number n Number of illumination shells for the whole group. -- @param #number power (Optional) Power of illumination warhead in mega candela. Default 1.0 mcd. +-- @return self function ARTY:SetIlluminationShells(n, power) self.Nillu=n self.illuPower=power or 1.0 self.illuPower=self.illuPower * 1000000 + return self end --- Set minimum and maximum detotation altitude for illumination shells. A value between min/max is selected randomly. @@ -1701,6 +1786,7 @@ end -- @param #ARTY self -- @param #number minalt (Optional) Minium altitude in meters. Default 500 m. -- @param #number maxalt (Optional) Maximum altitude in meters. Default 1000 m. +-- @return self function ARTY:SetIlluminationMinMaxAlt(minalt, maxalt) self.illuMinalt=minalt or 500 self.illuMaxalt=maxalt or 1000 @@ -1708,6 +1794,7 @@ function ARTY:SetIlluminationMinMaxAlt(minalt, maxalt) if self.illuMinalt>self.illuMaxalt then self.illuMinalt=self.illuMaxalt end + return self end --- Set number of smoke shells available to the group. @@ -1715,38 +1802,46 @@ end -- @param #ARTY self -- @param #number n Number of smoke shells for the whole group. -- @param Utilities.Utils#SMOKECOLOR color (Optional) Color of the smoke. Default SMOKECOLOR.Red. +-- @return self function ARTY:SetSmokeShells(n, color) self.Nsmoke=n self.smokeColor=color or SMOKECOLOR.Red + return self end --- Set nuclear fires and extra demolition explosions. -- @param #ARTY self -- @param #number nfires (Optional) Number of big smoke and fire objects created in the demolition zone. -- @param #number demolitionrange (Optional) Demolition range in meters. +-- @return self function ARTY:SetTacNukeFires(nfires, range) self.nukefire=true self.nukefires=nfires self.nukerange=range + return self end --- Enable assigning targets and moves by placing markers on the F10 map. -- @param #ARTY self -- @param #number key (Optional) Authorization key. Only players knowing this key can assign targets. Default is no authorization required. -- @param #boolean readonly (Optional) Marks are readonly and cannot be removed by players. This also means that targets cannot be cancelled by removing the mark. Default false. +-- @return self function ARTY:SetMarkAssignmentsOn(key, readonly) self.markkey=key self.markallow=true if readonly==nil then self.markreadonly=false end + return self end --- Disable assigning targets by placing markers on the F10 map. -- @param #ARTY self +-- @return self function ARTY:SetMarkTargetsOff() self.markallow=false self.markkey=nil + return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2618,17 +2713,19 @@ function ARTY:OnEventDead(EventData) -- Name of controllable. local _name=self.groupname - - env.info("FF Dead event for arty group") -- Check for correct group. - if EventData.IniGroupName==_name then + if EventData and EventData.IniGroupName and EventData.IniGroupName==_name then + + -- Name of the dead unit. + local unitname=tostring(EventData.IniUnitName) -- Dead Unit. - self:I(string.format("%s: Captured dead event for unit %s.", _name, tostring(EventData.IniUnitName))) + self:T(ARTY.id..string.format("%s: Captured dead event for unit %s.", _name, unitname)) -- FSM Dead event. We give one second for update of data base. - self:__Dead(1) + --self:__Dead(1, unitname) + self:Dead(unitname) end end @@ -2793,7 +2890,7 @@ function ARTY:onafterStatus(Controllable, From, Event, To) self:__Status(self.StatusInterval) else - self:E(string.format("Arty group %s is not alive!", self.groupname)) + self:E(ARTY.id..string.format("Arty group %s is not alive!", self.groupname)) end end @@ -3367,32 +3464,64 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function ARTY:onafterDead(Controllable, From, Event, To) +-- @param #string Unitname Name of the unit that died. +function ARTY:onafterDead(Controllable, From, Event, To, Unitname) self:_EventFromTo("onafterDead", Event, From, To) - -- Number of units left in the group. - --[[ - local units=self.Controllable:GetUnits() - local nunits=0 - if units~=nil then - nunits=#units - end - ]] - + -- Number of units still alive. + --local nunits=self.Controllable and self.Controllable:CountAliveUnits() or 0 local nunits=self.Controllable:CountAliveUnits() -- Message. - local text=string.format("%s, one of our units just died! %d units left.", self.groupname, nunits) + local text=string.format("%s, our unit %s just died! %d units left.", self.groupname, Unitname, nunits) MESSAGE:New(text, 5):ToAllIf(self.Debug) self:I(ARTY.id..text) -- Go to stop state. if nunits==0 then - self:Stop() + + -- Cease Fire on current target. + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + if self.respawnafterdeath then + -- Respawn group. + if not self.respawning then + self.respawning=true + self:__Respawn(self.respawndelay or 1) + end + else + -- Stop FSM. + self:Stop() + end end end + +--- After "Dead" event, when a unit has died. When all units of a group are dead trigger "Stop" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterRespawn(Controllable, From, Event, To) + self:_EventFromTo("onafterRespawn", Event, From, To) + + env.info("FF Respawning arty group") + + local group=self.Controllable --Wrapper.Group#GROUP + + -- Respawn group. + self.Controllable=group:Respawn() + + self.respawning=false + + -- Call status again. + self:__Status(-1) +end + --- After "Stop" event. Unhandle events and cease fire on current target. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -3452,7 +3581,7 @@ function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) group:SetTask(fire) end ---- Set task for firing at a coordinate. +--- Set task for attacking a group. -- @param #ARTY self -- @param #ARTY.Target target Target data. function ARTY:_AttackGroup(target) @@ -3476,11 +3605,8 @@ function ARTY:_AttackGroup(target) -- Get task. local fire=group:TaskAttackGroup(targetgroup, weapontype, AI.Task.WeaponExpend.ONE, 1) - self:E("FF") - self:E(fire) - -- Execute task. - group:PushTask(fire) + group:SetTask(fire) end diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 133706fb9..d6f7c70fd 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -541,7 +541,8 @@ do -- DETECTION_BASE end - self.DetectionCount = self.DetectionSet:Count() + -- Count alive(!) groups only. Solves issue #1173 https://github.com/FlightControl-Master/MOOSE/issues/1173 + self.DetectionCount = self.DetectionSet:CountAlive() self.DetectionSet:ForEachGroupAlive( function( DetectionGroup ) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 0ff01cfbd..871da0168 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -77,6 +77,8 @@ -- @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. -- @field #number lowfuelthresh Low fuel threshold. Triggers the event AssetLowFuel if for any unit fuel goes below this number. +-- @field #boolean respawnafterdestroyed If true, warehouse is respawned after it was destroyed. Assets are kept. +-- @field #number respawndelay Delay before respawn in seconds. -- @extends Core.Fsm#FSM --- Have your assets at the right place at the right time - or not! @@ -1569,6 +1571,8 @@ WAREHOUSE = { saveparking = false, isunit = false, lowfuelthresh = 0.15, + respawnafterdestroyed=false, + respawndelay = nil, } --- Item of the warehouse stock table. @@ -1745,7 +1749,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.9.4" +WAREHOUSE.version="0.9.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1908,6 +1912,7 @@ function WAREHOUSE:New(warehouse, alias) self:AddTransition("*", "AirbaseRecaptured", "*") -- Airbase was re-captured from other coalition. self:AddTransition("*", "AssetDead", "*") -- An asset group died. self:AddTransition("*", "Destroyed", "Destroyed") -- Warehouse was destroyed. All assets in stock are gone and warehouse is stopped. + self:AddTransition("Destroyed", "Respawn", "Running") -- Respawn warehouse after it was destroyed. ------------------------ --- Pseudo Functions --- @@ -1940,6 +1945,22 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Respawn". + -- @function [parent=#WAREHOUSE] Respawn + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Respawn" after a delay. + -- @function [parent=#WAREHOUSE] __Respawn + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- On after "Respawn" event user function. + -- @function [parent=#WAREHOUSE] OnAfterRespawn + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + --- Triggers the FSM event "Pause". Pauses the warehouse. Assets can still be added and requests be made. However, requests are not processed. -- @function [parent=#WAREHOUSE] Pause -- @param #WAREHOUSE self @@ -2533,6 +2554,15 @@ function WAREHOUSE:SetSaveOnMissionEnd(path, filename) return self end +--- Set respawn after destroy. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetRespawnAfterDestroyed(delay) + self.respawnafterdestroyed=true + self.respawndelay=delay + return self +end + --- Set the airbase belonging to this warehouse. -- Note that it has to be of the same coalition as the warehouse. @@ -3960,7 +3990,7 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto end -- Warehouse is destroyed? - if self:IsDestroyed() then + if self:IsDestroyed() and not self.respawnafterdestroyed then self:_ErrorMessage("ERROR: Invalid request. Warehouse is destroyed!", 0) okay=false end @@ -4827,6 +4857,21 @@ function WAREHOUSE:onbeforeChangeCountry(From, Event, To, Country) return false end +--- Respawn warehouse. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function WAREHOUSE:onafterRespawn(From, Event, To) + + -- Info message. + local text=string.format("Respawning warehouse %s.", self.alias) + self:_InfoMessage(text) + + -- Respawn warehouse. + self.warehouse:ReSpawn() + +end --- On after "ChangeCountry" event. Warehouse is respawned with the specified country. All queued requests are deleted and the owned airbase is reset if the coalition is changed by changing the -- country. @@ -4963,22 +5008,34 @@ end function WAREHOUSE:onafterDestroyed(From, Event, To) -- Message. - local text=string.format("Warehouse %s was destroyed! Assets lost %d.", self.alias, #self.stock) + local text=string.format("Warehouse %s was destroyed! Assets lost %d. Respawn=%s", self.alias, #self.stock, tostring(self.respawnafterdestroyed)) self:_InfoMessage(text) - -- Remove all table entries from waiting queue and stock. - for k,_ in pairs(self.queue) do - self.queue[k]=nil + if self.respawnafterdestroyed then + + if self.respawndelay then + self:Pause() + self:__Respawn(self.respawndelay) + else + self:Respawn() + end + + else + + -- Remove all table entries from waiting queue and stock. + for k,_ in pairs(self.queue) do + self.queue[k]=nil + end + for k,_ in pairs(self.stock) do + self.stock[k]=nil + end + + --self.queue=nil + --self.queue={} + + --self.stock=nil + --self.stock={} end - for k,_ in pairs(self.stock) do - self.stock[k]=nil - end - - --self.queue=nil - --self.queue={} - - --self.stock=nil - --self.stock={} end @@ -6343,7 +6400,7 @@ function WAREHOUSE:_CheckRequestConsistancy(queue) end -- Is receiving warehouse destroyed? - if request.warehouse:IsDestroyed() then + if request.warehouse:IsDestroyed() and not self.respawnafterdestroyed then self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is destroyed!")) valid=false end diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 385016112..846ed502a 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -3620,9 +3620,13 @@ function AIRBOSS:_CheckRecoveryTimes() if self:IsRecovering() and not recovery.OVER then - if #self.Qpattern>0 then + -- Get number of airborne aircraft units(!) currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + if npattern>0 then - local extmin=5*#self.Qpattern + -- Extend recovery time. 5 min per flight. + local extmin=5*npattern recovery.STOP=recovery.STOP+extmin*60 local text=string.format("We still got flights in the pattern.\nRecovery time prolonged by %d minutes.\nNow get your act together and no more bolters!", extmin) From 35653898cd2fa7387f7aefc9c862d38d9a476cbc Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 28 Jun 2019 17:15:40 +0300 Subject: [PATCH 310/485] WIP --- .../Moose/AI/AI_Escort_Dispatcher.lua | 152 ++++++++++++++++++ .../Moose/AI/AI_Escort_Dispatcher_Request.lua | 149 +++++++++++++++++ .../Moose/AI/AI_Escort_Request.lua | 8 +- Moose Development/Moose/Core/Event.lua | 8 +- Moose Development/Moose/Modules.lua | 2 + 5 files changed, 311 insertions(+), 8 deletions(-) create mode 100644 Moose Development/Moose/AI/AI_Escort_Dispatcher.lua create mode 100644 Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua new file mode 100644 index 000000000..686805d5f --- /dev/null +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua @@ -0,0 +1,152 @@ +--- **AI** -- (R2.4) - Models the assignment of AI escorts to player flights. +-- +-- ## Features: +-- -- +-- * Provides the facilities to trigger escorts when players join flight units. +-- * Provide different means how escorts can be triggered: +-- * Directly when a player joins a plane. +-- * Through the menu. +-- +-- === +-- +-- ## Test Missions: +-- +-- Test missions can be located on the main GITHUB site. +-- +-- [FlightControl-Master/MOOSE_MISSIONS] +-- +-- === +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Escort_Dispatcher +-- @image AI_Escort_Dispatcher.JPG + + +--- @type AI_ESCORT_DISPATCHER +-- @field Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. +-- @field Core.Set#SET_GROUP EscortGroupSet The set of group AI escorting the EscortUnit. +-- @field #string EscortName Name of the escort. +-- @field #string EscortBriefing A text showing the AI_ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @extends Core.Fsm#FSM + + +--- A dynamic cargo handling capability for AI groups. +-- +-- === +-- +-- @field #AI_ESCORT_DISPATCHER +AI_ESCORT_DISPATCHER = { + ClassName = "AI_ESCORT_DISPATCHER", +} + +--- @field #list +AI_ESCORT_DISPATCHER.AI_Escorts = {} + + +--- Creates a new AI_ESCORT_DISPATCHER object. +-- @param #AI_ESCORT_DISPATCHER self +-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. +-- @param Core.Spawn#SPAWN EscortSpawn The spawn object that will spawn in the Escorts. +-- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where the escorts are spawned. +-- @param #string EscortName Name of the escort. +-- @param #string EscortBriefing A text showing the AI_ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #AI_ESCORT_DISPATCHER +function AI_ESCORT_DISPATCHER:New( CarrierSet, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) + + local self = BASE:Inherit( self, FSM:New() ) -- #AI_ESCORT_DISPATCHER + + self.CarrierSet = CarrierSet + self.EscortSpawn = EscortSpawn + self.EscortAirbase = EscortAirbase + self.EscortName = EscortName + self.EscortBriefing = EscortBriefing + + self:SetStartState( "Idle" ) + + self:AddTransition( "Monitoring", "Monitor", "Monitoring" ) + + self:AddTransition( "Idle", "Start", "Monitoring" ) + self:AddTransition( "Monitoring", "Stop", "Idle" ) + + -- Put a Dead event handler on CarrierSet, to ensure that when a carrier is destroyed, that all internal parameters are reset. + function self.CarrierSet.OnAfterRemoved( CarrierSet, From, Event, To, CarrierName, Carrier ) + self:F( { Carrier = Carrier:GetName() } ) + end + + return self +end + +function AI_ESCORT_DISPATCHER:onafterStart( From, Event, To ) + + self:HandleEvent( EVENTS.Birth ) + + self:HandleEvent( EVENTS.PlayerLeaveUnit ) + +end + + +--- @param #AI_ESCORT_DISPATCHER self +-- @param Core.Event#EVENTDATA EventData +function AI_ESCORT_DISPATCHER:OnEventBirth( EventData ) + + local PlayerGroupName = EventData.IniGroupName + local PlayerGroup = EventData.IniGroup + local PlayerUnit = EventData.IniUnit + + self:I({PlayerGroupName = PlayerGroupName } ) + self:I({PlayerGroup = PlayerGroup}) + self:I({FirstGroup = self.CarrierSet:GetFirst()}) + self:I({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) + + if self.CarrierSet:FindGroup( PlayerGroupName ) then + if not self.AI_Escorts[PlayerGroupName] then + local LeaderUnit = PlayerUnit + local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase ) + local EscortSet = SET_GROUP:New() + EscortSet:AddGroup( EscortGroup ) + self:ScheduleOnce( 0.1, + function() + self.AI_Escorts[PlayerGroupName] = AI_ESCORT:New( LeaderUnit, EscortSet, self.EscortName, self.EscortBriefing ) + self.AI_Escorts[PlayerGroupName]:FormationTrail( 0, 100, 0 ) + if EscortGroup:IsHelicopter() then + self.AI_Escorts[PlayerGroupName]:MenusHelicopters() + else + self.AI_Escorts[PlayerGroupName]:MenusAirplanes() + end + self.AI_Escorts[PlayerGroupName]:__Start( 0.1 ) + end + ) + end + end + +end + + +--- Start Trigger for AI_ESCORT_DISPATCHER +-- @function [parent=#AI_ESCORT_DISPATCHER] Start +-- @param #AI_ESCORT_DISPATCHER self + +--- Start Asynchronous Trigger for AI_ESCORT_DISPATCHER +-- @function [parent=#AI_ESCORT_DISPATCHER] __Start +-- @param #AI_ESCORT_DISPATCHER self +-- @param #number Delay + +--- Stop Trigger for AI_ESCORT_DISPATCHER +-- @function [parent=#AI_ESCORT_DISPATCHER] Stop +-- @param #AI_ESCORT_DISPATCHER self + +--- Stop Asynchronous Trigger for AI_ESCORT_DISPATCHER +-- @function [parent=#AI_ESCORT_DISPATCHER] __Stop +-- @param #AI_ESCORT_DISPATCHER self +-- @param #number Delay + + + + + + diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua new file mode 100644 index 000000000..05f503fe7 --- /dev/null +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua @@ -0,0 +1,149 @@ +--- **AI** -- (R2.4) - Models the assignment of AI escorts to player flights upon request using the radio menu. +-- +-- ## Features: +-- -- +-- * Provides the facilities to trigger escorts when players join flight units. +-- * Provide different means how escorts can be triggered: +-- * Directly when a player joins a plane. +-- * Through the menu. +-- +-- === +-- +-- ## Test Missions: +-- +-- Test missions can be located on the main GITHUB site. +-- +-- [FlightControl-Master/MOOSE_MISSIONS] +-- +-- === +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_ESCORT_DISPATCHER_REQUEST +-- @image AI_ESCORT_DISPATCHER_REQUEST.JPG + + +--- @type AI_ESCORT_DISPATCHER_REQUEST +-- @field Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. +-- @field Core.Set#SET_GROUP EscortGroupSet The set of group AI escorting the EscortUnit. +-- @field #string EscortName Name of the escort. +-- @field #string EscortBriefing A text showing the AI_ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @extends Core.Fsm#FSM + + +--- A dynamic cargo handling capability for AI groups. +-- +-- === +-- +-- @field #AI_ESCORT_DISPATCHER_REQUEST +AI_ESCORT_DISPATCHER_REQUEST = { + ClassName = "AI_ESCORT_DISPATCHER_REQUEST", +} + +--- @field #list +AI_ESCORT_DISPATCHER_REQUEST.AI_Escorts = {} + + +--- Creates a new AI_ESCORT_DISPATCHER_REQUEST object. +-- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. +-- @param Core.Spawn#SPAWN EscortSpawn The spawn object that will spawn in the Escorts. +-- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where the escorts are spawned. +-- @param #string EscortName Name of the escort. +-- @param #string EscortBriefing A text showing the AI_ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #AI_ESCORT_DISPATCHER_REQUEST +function AI_ESCORT_DISPATCHER_REQUEST:New( CarrierSet, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) + + local self = BASE:Inherit( self, FSM:New() ) -- #AI_ESCORT_DISPATCHER_REQUEST + + self.CarrierSet = CarrierSet + self.EscortSpawn = EscortSpawn + self.EscortAirbase = EscortAirbase + self.EscortName = EscortName + self.EscortBriefing = EscortBriefing + + self:SetStartState( "Idle" ) + + self:AddTransition( "Monitoring", "Monitor", "Monitoring" ) + + self:AddTransition( "Idle", "Start", "Monitoring" ) + self:AddTransition( "Monitoring", "Stop", "Idle" ) + + -- Put a Dead event handler on CarrierSet, to ensure that when a carrier is destroyed, that all internal parameters are reset. + function self.CarrierSet.OnAfterRemoved( CarrierSet, From, Event, To, CarrierName, Carrier ) + self:F( { Carrier = Carrier:GetName() } ) + end + + return self +end + +function AI_ESCORT_DISPATCHER_REQUEST:onafterStart( From, Event, To ) + + self:HandleEvent( EVENTS.Birth ) + + self:HandleEvent( EVENTS.PlayerLeaveUnit ) + +end + + +--- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param Core.Event#EVENTDATA EventData +function AI_ESCORT_DISPATCHER_REQUEST:OnEventBirth( EventData ) + + local PlayerGroupName = EventData.IniGroupName + local PlayerGroup = EventData.IniGroup + local PlayerUnit = EventData.IniUnit + + self:I({PlayerGroupName = PlayerGroupName } ) + self:I({PlayerGroup = PlayerGroup}) + self:I({FirstGroup = self.CarrierSet:GetFirst()}) + self:I({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) + + if self.CarrierSet:FindGroup( PlayerGroupName ) then + if not self.AI_Escorts[PlayerGroupName] then + local LeaderUnit = PlayerUnit + self:ScheduleOnce( 0.1, + function() + self.AI_Escorts[PlayerGroupName] = AI_ESCORT_REQUEST:New( LeaderUnit, self.EscortSpawn, self.EscortAirbase, self.EscortName, self.EscortBriefing ) + self.AI_Escorts[PlayerGroupName]:FormationTrail( 0, 100, 0 ) + if PlayerGroup:IsHelicopter() then + self.AI_Escorts[PlayerGroupName]:MenusHelicopters() + else + self.AI_Escorts[PlayerGroupName]:MenusAirplanes() + end + self.AI_Escorts[PlayerGroupName]:__Start( 0.1 ) + end + ) + end + end + +end + + +--- Start Trigger for AI_ESCORT_DISPATCHER_REQUEST +-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] Start +-- @param #AI_ESCORT_DISPATCHER_REQUEST self + +--- Start Asynchronous Trigger for AI_ESCORT_DISPATCHER_REQUEST +-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] __Start +-- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param #number Delay + +--- Stop Trigger for AI_ESCORT_DISPATCHER_REQUEST +-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] Stop +-- @param #AI_ESCORT_DISPATCHER_REQUEST self + +--- Stop Asynchronous Trigger for AI_ESCORT_DISPATCHER_REQUEST +-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] __Stop +-- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param #number Delay + + + + + + diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index 1b7a4288f..9cc537b4c 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -206,7 +206,7 @@ AI_ESCORT_REQUEST = { -- Escort:__Start( 5 ) function AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) - self.EscortGroupSet = SET_GROUP:New() + self.EscortGroupSet = SET_GROUP:New():FilterDeads():FilterCrashes() self.EscortSpawn = EscortSpawn self.EscortAirbase = EscortAirbase @@ -227,12 +227,12 @@ function AI_ESCORT_REQUEST:SpawnEscort() local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Hot ) - EscortGroup:OptionROTVertical() - EscortGroup:OptionROEHoldFire() - self:ScheduleOnce( 0.1, function( EscortGroup ) + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEHoldFire() + self.EscortGroupSet:AddGroup( EscortGroup ) local LeaderEscort = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 83c344e7a..0729dd498 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -864,9 +864,9 @@ function EVENT:onEvent( Event ) if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then Event.IniDCSGroupName = Event.IniDCSGroup:getName() Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - if Event.IniGroup then + --if Event.IniGroup then Event.IniGroupName = Event.IniDCSGroupName - end + --end end Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() Event.IniCoalition = Event.IniDCSUnit:getCoalition() @@ -918,9 +918,9 @@ function EVENT:onEvent( Event ) if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - if Event.TgtGroup then + --if Event.TgtGroup then Event.TgtGroupName = Event.TgtDCSGroupName - end + --end end Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 2c485f2a2..a5e12f901 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -88,6 +88,8 @@ __Moose.Include( 'Scripts/Moose/AI/AI_Bai.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Formation.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Escort.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Escort_Request.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Escort_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Escort_Dispatcher_Request.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' ) From 376a3553fab4299149c3e27aa15cd5f46f24d9d0 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 30 Jun 2019 17:34:00 +0200 Subject: [PATCH 311/485] SPOT - Fixed bug in LaseOnCoordinate. - Minor stuff. --- .../Moose/AI/AI_A2A_Dispatcher.lua | 4 ++-- Moose Development/Moose/Core/Set.lua | 6 +++++- Moose Development/Moose/Core/Spot.lua | 19 ++++++++----------- .../Moose/Functional/Detection.lua | 2 +- .../Moose/Functional/Warehouse.lua | 2 +- Moose Development/Moose/Ops/Airboss.lua | 4 +++- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index cd1836f6f..000327939 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -1617,7 +1617,7 @@ do -- AI_A2A_DISPATCHER --- 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. + -- @return #boolean true if visible. -- @usage -- -- -- Set the Squadron visible before startup of dispatcher. @@ -2733,7 +2733,7 @@ do -- AI_A2A_DISPATCHER end - --- + --- Activate resource. -- @param #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 20554631b..23b148ad3 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -558,6 +558,10 @@ do -- SET_BASE --- 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. + -- @param #table arg Arguments of the IteratorFunction. + -- @param #SET_BASE Set (Optional) The set to use. Default self:GetSet(). + -- @param #function Function (Optional) A function returning a #boolean true/false. Only if true, the IteratorFunction is called. + -- @param #table FunctionArguments (Optional) Function arguments. -- @return #SET_BASE self function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) self:F3( arg ) @@ -571,7 +575,7 @@ do -- SET_BASE local Object = ObjectData self:T3( Object ) if Function then - if Function( unpack( FunctionArguments ), Object ) == true then + if Function( unpack( FunctionArguments or {} ), Object ) == true then IteratorFunction( Object, unpack( arg ) ) end else diff --git a/Moose Development/Moose/Core/Spot.lua b/Moose Development/Moose/Core/Spot.lua index 6395cd7b4..144c83ca3 100644 --- a/Moose Development/Moose/Core/Spot.lua +++ b/Moose Development/Moose/Core/Spot.lua @@ -285,11 +285,8 @@ do local RecceDcsUnit = self.Recce:GetDCSObject() - local aimat=Coordinate - aimat.y=aimat.y+1 - - self.SpotIR = Spot.createInfraRed( RecceDcsUnit, { x = 0, y = 2, z = 0 }, aimat:GetVec3() ) - self.SpotLaser = Spot.createLaser( RecceDcsUnit, { x = 0, y = 2, z = 0 }, aimat:GetVec3(), LaserCode ) + self.SpotIR = Spot.createInfraRed( RecceDcsUnit, { x = 0, y = 1, z = 0 }, Coordinate:GetVec3() ) + self.SpotLaser = Spot.createLaser( RecceDcsUnit, { x = 0, y = 1, z = 0 }, Coordinate:GetVec3(), LaserCode ) if Duration then self.ScheduleID = self.LaseScheduler:Schedule( self, StopLase, {self}, Duration ) @@ -323,14 +320,14 @@ do self:__Lasing( -0.2 ) elseif self.TargetCoord then - local aimat=self.TargetCoord --Core.Point#COORDINATE - aimat.y=aimat.y+1+math.random(-100,100)/100 - aimat.x=aimat.x+math.random(-100,100)/100 + -- Wiggle the IR spot a bit. + local irvec3={x=self.TargetCoord.x+math.random(-100,100)/100, y=self.TargetCoord.y+math.random(-100,100)/100, z=self.TargetCoord.z} --#DCS.Vec3 + local lsvec3={x=self.TargetCoord.x, y=self.TargetCoord.y, z=self.TargetCoord.z} --#DCS.Vec3 - self.SpotIR:setPoint(aimat:GetVec3()) - self.SpotLaser:setPoint(self.TargetCoord:GetVec3()) + self.SpotIR:setPoint(irvec3) + self.SpotLaser:setPoint(lsvec3) - self:__Lasing( -0.2 ) + self:__Lasing(-0.25) else self:F( { "Target is not alive", self.Target:IsAlive() } ) end diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index d6f7c70fd..d57b1add6 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1898,7 +1898,7 @@ do -- DETECTION_BASE --- Get the Detection Set. -- @param #DETECTION_BASE self - -- @return Core.Set#SET_BASE + -- @return #DETECTION_BASE self function DETECTION_BASE:GetDetectionSet() local DetectionSet = self.DetectionSet diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 871da0168..527138506 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -4986,7 +4986,7 @@ function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition) end ---- On after "AssetDead" event triggerd when an asset group died. +--- On after "AssetDead" event triggered when an asset group died. -- @param #WAREHOUSE self -- @param #string From From state. -- @param #string Event Event. diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 846ed502a..74b3778a3 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -12519,7 +12519,7 @@ end -- @param #number optdist Optimal distance in meters. -- @return #string Feedback message text. -- @return #string Debriefing text. --- @#AIRBOSS.RadioCall Distance radio call. Not implemented yet. +-- @return #AIRBOSS.RadioCall Distance radio call. Not implemented yet. function AIRBOSS:_DistanceCheck(playerData, optdist) if optdist==nil then @@ -12554,8 +12554,10 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) hint=hint..string.format(" Optimal distance is %.1f NM.", UTILS.MetersToNM(optdist)) elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then -- We keep it short normally. + hint="" elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then -- No hint at all for the pros. + hint="" end -- Debriefing text. From 1afe1c48b4bd32ecde3ab131298aa57415ff288a Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 1 Jul 2019 17:28:49 +0300 Subject: [PATCH 312/485] Updates - Much improved version! --- .../Moose/AI/AI_Escort_Dispatcher.lua | 15 +++++++++------ Moose Development/Moose/AI/AI_Escort_Request.lua | 1 + Moose Development/Moose/Core/Spawn.lua | 5 +++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua index 686805d5f..a4000ccf8 100644 --- a/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua @@ -98,6 +98,7 @@ function AI_ESCORT_DISPATCHER:OnEventBirth( EventData ) local PlayerGroup = EventData.IniGroup local PlayerUnit = EventData.IniUnit + self:I({EscortAirbase= self.EscortAirbase } ) self:I({PlayerGroupName = PlayerGroupName } ) self:I({PlayerGroup = PlayerGroup}) self:I({FirstGroup = self.CarrierSet:GetFirst()}) @@ -106,11 +107,13 @@ function AI_ESCORT_DISPATCHER:OnEventBirth( EventData ) if self.CarrierSet:FindGroup( PlayerGroupName ) then if not self.AI_Escorts[PlayerGroupName] then local LeaderUnit = PlayerUnit - local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase ) - local EscortSet = SET_GROUP:New() - EscortSet:AddGroup( EscortGroup ) - self:ScheduleOnce( 0.1, - function() + local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Hot ) + self:I({EscortGroup = EscortGroup}) + + self:ScheduleOnce( 1, + function( EscortGroup ) + local EscortSet = SET_GROUP:New() + EscortSet:AddGroup( EscortGroup ) self.AI_Escorts[PlayerGroupName] = AI_ESCORT:New( LeaderUnit, EscortSet, self.EscortName, self.EscortBriefing ) self.AI_Escorts[PlayerGroupName]:FormationTrail( 0, 100, 0 ) if EscortGroup:IsHelicopter() then @@ -119,7 +122,7 @@ function AI_ESCORT_DISPATCHER:OnEventBirth( EventData ) self.AI_Escorts[PlayerGroupName]:MenusAirplanes() end self.AI_Escorts[PlayerGroupName]:__Start( 0.1 ) - end + end, EscortGroup ) end end diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index 9cc537b4c..5eb274278 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -269,6 +269,7 @@ function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) self:F() if not self.MenuRequestEscort then + self.MainMenu = MENU_GROUP:New( self.PlayerGroup, self.EscortName ) self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request new escort ", self.MainMenu, function() self:SpawnEscort() diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index ff6387c5a..dc30d2325 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2215,10 +2215,11 @@ function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, self:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, 1 ) for SpawnIndex = 2, self.SpawnMaxGroups do - self:ScheduleOnce( SpawnIndex * 0.1, SPAWN.ParkAircraft, self, SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) + self:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) + --self:ScheduleOnce( SpawnIndex * 0.1, SPAWN.ParkAircraft, self, SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) end - self:SetSpawnIndex() + self:SetSpawnIndex( 0 ) return nil end From 8780ec368723966e4bc461119dc521b32e96ea7d Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 3 Jul 2019 22:13:56 +0200 Subject: [PATCH 313/485] Docs Updated docs and function argument description of DETECTION and A2A_DISPATCHER. --- .../Moose/AI/AI_A2A_Dispatcher.lua | 248 ++++++++++++------ .../Moose/Functional/Detection.lua | 56 ++-- 2 files changed, 207 insertions(+), 97 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 000327939..a96d3e970 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -832,6 +832,27 @@ do -- AI_A2A_DISPATCHER } + --- Squadron data structure. + -- @type AI_A2A_Dispatcher.Squadron + -- @field #string Name Name of the squadron. + -- @field #number ResourceCount Number of resources. + -- @field #string AirbaseName Name of the home airbase. + -- @field Wrapper.Airbase#AIRBASE Airbase The home airbase of the squadron. + -- @field #boolean Captured If true, airbase of the squadron was captured. + -- @field #table Resources Flight group resources Resources[TemplateID][GroupName] = SpawnGroup. + -- @field #boolean Uncontrolled If true, flight groups are spawned uncontrolled and later activated. + -- @field #table Gci GCI. + -- @field #number Overhead Squadron overhead. + -- @field #number Grouping Squadron flight group size. + -- @field #number Takeoff Takeoff type. + -- @field #number TakeoffAltitude Altitude in meters for spawn in air. + -- @field #number Landing Landing type. + -- @field #number FuelThreshold Fuel threshold [0,1] for RTB. + -- @field #string TankerName Name of the refuelling tanker. + -- @field #table Table of template group names of the squadron. + -- @field #table Spawn Table of spaws Core.Spawn#SPAWN. + -- @field #table TemplatePrefixes + --- Enumerator for spawns at airbases -- @type AI_A2A_DISPATCHER.Takeoff -- @extends Wrapper.Group#GROUP.Takeoff @@ -1021,7 +1042,8 @@ do -- AI_A2A_DISPATCHER end - --- @param #AI_A2A_DISPATCHER self + --- On after "Start" event. + -- @param #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:onafterStart( From, Event, To ) self:GetParent( self, AI_A2A_DISPATCHER ).onafterStart( self, From, Event, To ) @@ -1038,7 +1060,9 @@ do -- AI_A2A_DISPATCHER end - --- @param #AI_A2A_DISPATCHER self + --- Park defender. + -- @param #AI_A2A_DISPATCHER self + -- @param #AI_A2A_Dispatcher.Squadron DefenderSquadron The squadron. function AI_A2A_DISPATCHER:ParkDefender( DefenderSquadron ) local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN @@ -1055,7 +1079,8 @@ do -- AI_A2A_DISPATCHER end - --- @param #AI_A2A_DISPATCHER self + --- Event base captured. + -- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2A_DISPATCHER:OnEventBaseCaptured( EventData ) @@ -1073,13 +1098,15 @@ do -- AI_A2A_DISPATCHER end end - --- @param #AI_A2A_DISPATCHER self + --- Event dead or crash. + -- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2A_DISPATCHER:OnEventCrashOrDead( EventData ) self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) end - --- @param #AI_A2A_DISPATCHER self + --- Event land. + -- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2A_DISPATCHER:OnEventLand( EventData ) self:F( "Landed" ) @@ -1106,7 +1133,8 @@ do -- AI_A2A_DISPATCHER end end - --- @param #AI_A2A_DISPATCHER self + --- Event engine shutdown. + -- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2A_DISPATCHER:OnEventEngineShutdown( EventData ) local DefenderUnit = EventData.IniUnit @@ -1163,7 +1191,7 @@ do -- AI_A2A_DISPATCHER --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters. -- @param #AI_A2A_DISPATCHER self - -- @param #number DisengageRadius (Optional, Default = 300000) The radius to disengage a target when too far from the home base. + -- @param #number DisengageRadius (Optional, Default = 300000) The radius in meters to disengage a target when too far from the home base. -- @return #AI_A2A_DISPATCHER -- @usage -- @@ -1196,7 +1224,7 @@ do -- AI_A2A_DISPATCHER -- -- @param #AI_A2A_DISPATCHER self -- @param #number GciRadius (Optional, Default = 200000) The radius to ground control intercept detected targets from the nearest airbase. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. @@ -1224,7 +1252,7 @@ do -- AI_A2A_DISPATCHER -- 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_A2A_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_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. @@ -1258,7 +1286,7 @@ do -- AI_A2A_DISPATCHER -- * ... -- @param #AI_A2A_DISPATCHER self -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. @@ -1279,7 +1307,7 @@ do -- AI_A2A_DISPATCHER -- 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_A2A_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_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. @@ -1301,7 +1329,7 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_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. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. @@ -1323,7 +1351,7 @@ do -- AI_A2A_DISPATCHER -- The default CAP limit is 1 CAP, which means one CAP group being spawned. -- @param #AI_A2A_DISPATCHER self -- @param #number CapLimit The maximum amount of CAP that can be airborne at the same time for the squadron. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. @@ -1339,7 +1367,10 @@ do -- AI_A2A_DISPATCHER return self end - + --- Set intercept. + -- @param #AI_A2A_DISPATCHER self + -- @param #number InterceptDelay Delay in seconds before intercept. + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetIntercept( InterceptDelay ) self.DefenderDefault.InterceptDelay = InterceptDelay @@ -1362,40 +1393,50 @@ do -- AI_A2A_DISPATCHER return FriendliesNearBy end - --- + --- Return the defender tasks table. -- @param #AI_A2A_DISPATCHER self + -- @return #table Defender tasks as table. function AI_A2A_DISPATCHER:GetDefenderTasks() return self.DefenderTasks or {} end - --- + --- Get defender task. -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return #table Defender task. function AI_A2A_DISPATCHER:GetDefenderTask( Defender ) return self.DefenderTasks[Defender] end - --- + --- Get defender task FSM. -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return Core.Fsm#FSM The FSM. function AI_A2A_DISPATCHER:GetDefenderTaskFsm( Defender ) return self:GetDefenderTask( Defender ).Fsm end - --- + --- Get target of defender. -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return Target function AI_A2A_DISPATCHER:GetDefenderTaskTarget( Defender ) return self:GetDefenderTask( Defender ).Target end --- -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return #string Squadron name of the defender task. function AI_A2A_DISPATCHER:GetDefenderTaskSquadronName( Defender ) return self:GetDefenderTask( Defender ).SquadronName end --- -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. function AI_A2A_DISPATCHER:ClearDefenderTask( Defender ) - if Defender:IsAlive() and self.DefenderTasks[Defender] then + if Defender and Defender:IsAlive() and self.DefenderTasks[Defender] then local Target = self.DefenderTasks[Defender].Target local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " Message = Message .. Defender:GetName() @@ -1410,11 +1451,12 @@ do -- AI_A2A_DISPATCHER --- -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. function AI_A2A_DISPATCHER:ClearDefenderTaskTarget( Defender ) local DefenderTask = self:GetDefenderTask( Defender ) - if Defender:IsAlive() and DefenderTask then + if Defender and Defender:IsAlive() and DefenderTask then local Target = DefenderTask.Target local Message = "Clearing (" .. DefenderTask.Type .. ") " Message = Message .. Defender:GetName() @@ -1437,8 +1479,14 @@ do -- AI_A2A_DISPATCHER end - --- + --- Set defender task. -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName Name of the squadron. + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @param #table Type Type of the defender task + -- @param Core.Fsm#FSM Fsm The defender task FSM. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem Target The defender detected item. + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target ) self:F( { SquadronName = SquadronName, Defender = Defender:GetName() } ) @@ -1455,9 +1503,11 @@ do -- AI_A2A_DISPATCHER end - --- + --- Set defender task target. -- @param #AI_A2A_DISPATCHER self - -- @param Wrapper.Group#GROUP AIGroup + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection The detection object. + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection ) local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " @@ -1540,13 +1590,13 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) -- -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - local DefenderSquadron = self.DefenderSquadrons[SquadronName] + local DefenderSquadron = self.DefenderSquadrons[SquadronName] --#AI_A2A_Dispatcher.Squadron DefenderSquadron.Name = SquadronName DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName ) @@ -1577,7 +1627,8 @@ do -- AI_A2A_DISPATCHER --- Get an item from the Squadron table. -- @param #AI_A2A_DISPATCHER self - -- @return #table + -- @param #string SquadronName Name of the squadron. + -- @return #AI_A2A_Dispatcher.Squadron Defender squadron table. function AI_A2A_DISPATCHER:GetSquadron( SquadronName ) local DefenderSquadron = self.DefenderSquadrons[SquadronName] @@ -1594,7 +1645,7 @@ do -- AI_A2A_DISPATCHER -- They will lock the parking spot. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Set the Squadron visible before startup of dispatcher. @@ -1604,7 +1655,7 @@ do -- AI_A2A_DISPATCHER self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - local DefenderSquadron = self:GetSquadron( SquadronName ) + local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_Dispatcher.Squadron DefenderSquadron.Uncontrolled = true @@ -1627,7 +1678,7 @@ do -- AI_A2A_DISPATCHER self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - local DefenderSquadron = self:GetSquadron( SquadronName ) + local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_Dispatcher.Squadron if DefenderSquadron then return DefenderSquadron.Uncontrolled == true @@ -1755,7 +1806,7 @@ do -- AI_A2A_DISPATCHER --- -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:GetCAPDelay( SquadronName ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} @@ -1771,7 +1822,7 @@ do -- AI_A2A_DISPATCHER end end - --- + --- Check if squadron can do CAP. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. -- @return #table DefenderSquadron @@ -1804,7 +1855,7 @@ do -- AI_A2A_DISPATCHER end - --- + --- Check if squadron can do GCI. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. -- @return #table DefenderSquadron @@ -1829,11 +1880,11 @@ do -- AI_A2A_DISPATCHER end - --- + --- Set squadron GCI. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param #number EngageMinSpeed The minimum speed at which the gci can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the gci can be executed. + -- @param #number EngageMinSpeed The minimum speed [km/h] at which the GCI can be executed. + -- @param #number EngageMaxSpeed The maximum speed [km/h] at which the GCI can be executed. -- @usage -- -- -- GCI Squadron execution. @@ -1923,7 +1974,7 @@ do -- AI_A2A_DISPATCHER -- -- A2ADispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -1945,7 +1996,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher:SetDefaultGrouping( 2 ) -- -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultGrouping( Grouping ) self.DefenderDefault.Grouping = Grouping @@ -1967,7 +2018,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher:SetSquadronGrouping( "SquadronName", 2 ) -- -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -1997,7 +2048,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Cold ) -- -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- function AI_A2A_DISPATCHER:SetDefaultTakeoff( Takeoff ) @@ -2027,7 +2078,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Cold ) -- -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- function AI_A2A_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff ) @@ -2086,7 +2137,7 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights by default take-off in the air. -- A2ADispatcher:SetDefaultTakeoffInAir() -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- function AI_A2A_DISPATCHER:SetDefaultTakeoffInAir() @@ -2107,7 +2158,7 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights take-off in the air. -- A2ADispatcher:SetSquadronTakeoffInAir( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- function AI_A2A_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) @@ -2130,7 +2181,7 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights by default take-off from the runway. -- A2ADispatcher:SetDefaultTakeoffFromRunway() -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- function AI_A2A_DISPATCHER:SetDefaultTakeoffFromRunway() @@ -2150,7 +2201,7 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights take-off from the runway. -- A2ADispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- function AI_A2A_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) @@ -2169,7 +2220,7 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights by default take-off at a hot parking spot. -- A2ADispatcher:SetDefaultTakeoffFromParkingHot() -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingHot() @@ -2188,7 +2239,7 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights take-off in the air. -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) @@ -2207,7 +2258,7 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights take-off from a cold parking spot. -- A2ADispatcher:SetDefaultTakeoffFromParkingCold() -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingCold() @@ -2227,7 +2278,7 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights take-off from a cold parking spot. -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) @@ -2247,7 +2298,7 @@ do -- AI_A2A_DISPATCHER -- -- Set the default takeoff altitude when taking off in the air. -- A2ADispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- function AI_A2A_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude ) @@ -2267,7 +2318,7 @@ do -- AI_A2A_DISPATCHER -- -- Set the default takeoff altitude when taking off in the air. -- A2ADispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- function AI_A2A_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) @@ -2294,7 +2345,7 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.AtEngineShutdown ) -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLanding( Landing ) self.DefenderDefault.Landing = Landing @@ -2320,7 +2371,7 @@ do -- AI_A2A_DISPATCHER -- -- Let new flights despawn after landing and parking, and after engine shutdown. -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.AtEngineShutdown ) -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLanding( SquadronName, Landing ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -2379,7 +2430,7 @@ do -- AI_A2A_DISPATCHER -- -- Let flights by default to land near the airbase and despawn. -- A2ADispatcher:SetDefaultLandingNearAirbase() -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLandingNearAirbase() self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.NearAirbase ) @@ -2398,7 +2449,7 @@ do -- AI_A2A_DISPATCHER -- -- Let flights to land near the airbase and despawn. -- A2ADispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.NearAirbase ) @@ -2416,7 +2467,7 @@ do -- AI_A2A_DISPATCHER -- -- Let flights by default land at the runway and despawn. -- A2ADispatcher:SetDefaultLandingAtRunway() -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLandingAtRunway() self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtRunway ) @@ -2435,7 +2486,7 @@ do -- AI_A2A_DISPATCHER -- -- Let flights land at the runway and despawn. -- A2ADispatcher:SetSquadronLandingAtRunway( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtRunway ) @@ -2453,7 +2504,7 @@ do -- AI_A2A_DISPATCHER -- -- Let flights by default land and despawn at engine shutdown. -- A2ADispatcher:SetDefaultLandingAtEngineShutdown() -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLandingAtEngineShutdown() self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtEngineShutdown ) @@ -2472,7 +2523,7 @@ do -- AI_A2A_DISPATCHER -- -- Let flights land and despawn at engine shutdown. -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) -- - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtEngineShutdown ) @@ -2484,7 +2535,7 @@ do -- AI_A2A_DISPATCHER -- 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_A2A_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_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. @@ -2506,7 +2557,7 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_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_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. @@ -2526,7 +2577,7 @@ do -- AI_A2A_DISPATCHER --- Set the default tanker where defenders will Refuel in the air. -- @param #AI_A2A_DISPATCHER self -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. @@ -2570,8 +2621,11 @@ do -- AI_A2A_DISPATCHER - - --- @param #AI_A2A_DISPATCHER self + --- Add defender to squadron. Resource count will get smaller. + -- @param #AI_A2A_DISPATCHER self + -- @param #AI_A2A_DISPATCHER.Squadron Squadron The squadron. + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @param #number Size Size of the group. function AI_A2A_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() @@ -2582,7 +2636,10 @@ do -- AI_A2A_DISPATCHER self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) end - --- @param #AI_A2A_DISPATCHER self + --- Remove defender from squadron. Resource count will increase. + -- @param #AI_A2A_DISPATCHER self + -- @param #AI_A2A_DISPATCHER.Squadron Squadron The squadron. + -- @param Wrapper.Group#GROUP Defender The defender group. function AI_A2A_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() @@ -2592,7 +2649,11 @@ do -- AI_A2A_DISPATCHER self.Defenders[ DefenderName ] = nil self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) end - + + --- Get squadron from defender. + -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return ? function AI_A2A_DISPATCHER:GetSquadronFromDefender( Defender ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() @@ -2605,7 +2666,6 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_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_A2A_DISPATCHER:EvaluateSWEEP( DetectedItem ) self:F( { DetectedItem.ItemID } ) @@ -2626,8 +2686,10 @@ do -- AI_A2A_DISPATCHER return nil end - --- + --- Count number of airborne CAP flights. -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName Name of the squadron. + -- @return #number Number of defender CAP groups. function AI_A2A_DISPATCHER:CountCapAirborne( SquadronName ) local CapCount = 0 @@ -2637,7 +2699,7 @@ do -- AI_A2A_DISPATCHER for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do if DefenderTask.SquadronName == SquadronName then if DefenderTask.Type == "CAP" then - if AIGroup:IsAlive() then + if AIGroup and 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! if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) @@ -2654,8 +2716,10 @@ do -- AI_A2A_DISPATCHER end - --- + --- Count number of engaging defender groups. -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detection object. + -- @return #number Number of defender groups engaging. function AI_A2A_DISPATCHER:CountDefendersEngaged( AttackerDetection ) -- First, count the active AIGroups Units, targetting the DetectedSet @@ -2665,9 +2729,10 @@ do -- AI_A2A_DISPATCHER --DetectedSet:Flush() local DefenderTasks = self:GetDefenderTasks() + for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do local Defender = DefenderGroup -- Wrapper.Group#GROUP - local DefenderTaskTarget = DefenderTask.Target + local DefenderTaskTarget = DefenderTask.Target --Functional.Detection#DETECTION_BASE.DetectedItem local DefenderSquadronName = DefenderTask.SquadronName if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then @@ -2689,8 +2754,11 @@ do -- AI_A2A_DISPATCHER return DefenderCount end - --- + --- Count defenders to be engaged if number of attackers larger than number of defenders. -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefenderCount Number of defenders. + -- @return #table Table of friendly groups. function AI_A2A_DISPATCHER:CountDefendersToBeEngaged( AttackerDetection, DefenderCount ) local Friendlies = nil @@ -2735,6 +2803,10 @@ do -- AI_A2A_DISPATCHER --- Activate resource. -- @param #AI_A2A_DISPATCHER self + -- @param #AI_A2A_DISPATCHER.Squadron DefenderSquadron The defender squadron. + -- @param #number DefendersNeeded Number of defenders needed. Default 4. + -- @return Wrapper.Group#GROUP The defender group. + -- @return #boolean Grouping. function AI_A2A_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) local SquadronName = DefenderSquadron.Name @@ -2807,8 +2879,12 @@ do -- AI_A2A_DISPATCHER return nil, nil end - --- + --- On after "CAP" event. -- @param #AI_A2A_DISPATCHER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string SquadronName Name of the squadron. function AI_A2A_DISPATCHER:onafterCAP( From, Event, To, SquadronName ) self:F({SquadronName = SquadronName}) @@ -2883,8 +2959,13 @@ do -- AI_A2A_DISPATCHER end - --- + --- On after "ENGAGE" event. -- @param #AI_A2A_DISPATCHER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. function AI_A2A_DISPATCHER:onafterENGAGE( From, Event, To, AttackerDetection, Defenders ) if Defenders then @@ -2900,8 +2981,14 @@ do -- AI_A2A_DISPATCHER end end - --- + --- On after "GCI" event. -- @param #AI_A2A_DISPATCHER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefendersMissing Number of missing defenders. + -- @param #table DefenderFriendlies Friendly defenders. function AI_A2A_DISPATCHER:onafterGCI( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies ) self:F( { From, Event, To, AttackerDetection.Index, DefendersMissing, DefenderFriendlies } ) @@ -3085,8 +3172,7 @@ do -- AI_A2A_DISPATCHER --- Creates an ENGAGE task when there are human friendlies airborne near the targets. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. - -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units or nil. function AI_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem ) self:F( { DetectedItem.ItemID } ) @@ -3106,14 +3192,14 @@ do -- AI_A2A_DISPATCHER return DefenderGroups end - return nil, nil + return nil end --- Creates an GCI task when there are targets for it. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. - -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units or nil if there are no targets to be set. + -- @return #table Table of friendly groups. function AI_A2A_DISPATCHER:EvaluateGCI( DetectedItem ) self:F( { DetectedItem.ItemID } ) @@ -3273,7 +3359,7 @@ do --- Calculates which HUMAN friendlies are nearby the area. -- @param #AI_A2A_DISPATCHER self - -- @param DetectedItem The detected item. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem 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_A2A_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) @@ -3319,7 +3405,7 @@ do --- Calculates which friendlies are nearby the area. -- @param #AI_A2A_DISPATCHER self - -- @param DetectedItem The detected item. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem 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_A2A_DISPATCHER:GetFriendliesNearBy( DetectedItem ) diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index d57b1add6..851977790 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -291,17 +291,34 @@ do -- DETECTION_BASE --- @type DETECTION_BASE.DetectedItems -- @list <#DETECTION_BASE.DetectedItem> - --- @type DETECTION_BASE.DetectedItem + --- Detected item data structrue. + -- @type DETECTION_BASE.DetectedItem -- @field #boolean IsDetected Indicates if the DetectedItem has been detected or not. - -- @field Core.Set#SET_UNIT Set - -- @field Core.Set#SET_UNIT Set -- The Set of Units in the detected area. - -- @field Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. - -- @field #boolean Changed Documents if the detected area has changes. + -- @field Core.Set#SET_UNIT Set The Set of Units in the detected area. + -- @field Core.Zone#ZONE_UNIT Zone The Zone of the detected area. + -- @field #boolean Changed Documents if the detected area has changed. -- @field #table Changes A list of the changes reported on the detected area. (It is up to the user of the detected area to consume those changes). - -- @field #number ID -- The identifier of the detected area. + -- @field #number ID The identifier of the detected area. -- @field #boolean FriendliesNearBy Indicates if there are friendlies within the detected area. -- @field Wrapper.Unit#UNIT NearestFAC The nearest FAC near the Area. -- @field Core.Point#COORDINATE Coordinate The last known coordinate of the DetectedItem. + -- @field Core.Point#COORDINATE InterceptCoord Intercept coordiante. + -- @field #number DistanceRecce Distance in meters of the Recce. + -- @field #number Index Detected item key. Could also be a string. + -- @field #string ItemID ItemPrefix .. "." .. self.DetectedItemMax. + -- @field #boolean Locked Lock detected item. + -- @field #table PlayersNearBy Table of nearby players. + -- @field #table FriendliesDistance Table of distances to friendly units. + -- @field #string TypeName Type name of the detected unit. + -- @field #string CategoryName Catetory name of the detected unit. + -- @field #string Name Name of the detected object. + -- @field #boolean IsVisible If true, detected object is visible. + -- @field #number LastTime Last time the detected item was seen. + -- @field DCS#Vec3 LastPos Last known position of the detected item. + -- @field DCS#Vec3 LastVelocity Last recorded 3D velocity vector of the detected item. + -- @field #boolean KnowType Type of detected item is known. + -- @field #boolean KnowDistance Distance to the detected item is known. + -- @field #number Distance Distance to the detected item. --- DETECTION constructor. -- @param #DETECTION_BASE self @@ -1234,7 +1251,7 @@ do -- DETECTION_BASE --- Returns if there are friendlies nearby the FAC units ... -- @param #DETECTION_BASE self - -- @param DetectedItem + -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @param DCS#Unit.Category Category The category of the unit. -- @return #boolean true if there are friendlies nearby function DETECTION_BASE:IsFriendliesNearBy( DetectedItem, Category ) @@ -1244,7 +1261,7 @@ do -- DETECTION_BASE --- Returns friendly units nearby the FAC units ... -- @param #DETECTION_BASE self - -- @param DetectedItem + -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @param DCS#Unit.Category Category The category of the unit. -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. function DETECTION_BASE:GetFriendliesNearBy( DetectedItem, Category ) @@ -1254,6 +1271,7 @@ do -- DETECTION_BASE --- Returns if there are friendlies nearby the intercept ... -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return #boolean trhe if there are friendlies near the intercept. function DETECTION_BASE:IsFriendliesNearIntercept( DetectedItem ) @@ -1262,6 +1280,7 @@ do -- DETECTION_BASE --- Returns friendly units nearby the intercept point ... -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item. -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. function DETECTION_BASE:GetFriendliesNearIntercept( DetectedItem ) @@ -1270,7 +1289,8 @@ do -- DETECTION_BASE --- Returns the distance used to identify friendlies near the deteted item ... -- @param #DETECTION_BASE self - -- @return #number The distance. + -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return #table A table of distances to friendlies. function DETECTION_BASE:GetFriendliesDistance( DetectedItem ) return DetectedItem.FriendliesDistance @@ -1278,6 +1298,7 @@ do -- DETECTION_BASE --- Returns if there are friendlies nearby the FAC units ... -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return #boolean trhe if there are friendlies nearby function DETECTION_BASE:IsPlayersNearBy( DetectedItem ) @@ -1286,6 +1307,7 @@ do -- DETECTION_BASE --- Returns friendly units nearby the FAC units ... -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item. -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. function DETECTION_BASE:GetPlayersNearBy( DetectedItem ) @@ -1294,10 +1316,11 @@ do -- DETECTION_BASE --- Background worker function to determine if there are friendlies nearby ... -- @param #DETECTION_BASE self + -- @param #table TargetData function DETECTION_BASE:ReportFriendliesNearBy( TargetData ) --self:F( { "Search Friendlies", DetectedItem = TargetData.DetectedItem } ) - local DetectedItem = TargetData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedItem = TargetData.DetectedItem --#DETECTION_BASE.DetectedItem local DetectedSet = TargetData.DetectedItem.Set local DetectedUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT @@ -1519,13 +1542,13 @@ do -- DETECTION_BASE --- Adds a new DetectedItem to the DetectedItems list. -- The DetectedItem is a table and contains a SET_UNIT in the field Set. -- @param #DETECTION_BASE self - -- @param ItemPrefix - -- @param DetectedItemKey The key of the DetectedItem. + -- @param #string ItemPrefix Prefix of detected item. + -- @param #number DetectedItemKey The key of the DetectedItem. Default self.DetectedItemMax. Could also be a string in principle. -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. -- @return #DETECTION_BASE.DetectedItem function DETECTION_BASE:AddDetectedItem( ItemPrefix, DetectedItemKey, Set ) - local DetectedItem = {} + local DetectedItem = {} --#DETECTION_BASE.DetectedItem self.DetectedItemCount = self.DetectedItemCount + 1 self.DetectedItemMax = self.DetectedItemMax + 1 @@ -1706,6 +1729,7 @@ do -- DETECTION_BASE --- Checks if there is at least one UNIT detected in the Set of the the DetectedItem. -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet. function DETECTION_BASE:IsDetectedItemDetected( DetectedItem ) @@ -1832,8 +1856,7 @@ do -- DETECTION_BASE --- Get a list of the detected item coordinates. -- @param #DETECTION_BASE self - -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem to set the coordinate at. - -- @return Core.Point#COORDINATE + -- @return #table A table of Core.Point#COORDINATE function DETECTION_BASE:GetDetectedItemCoordinates() local Coordinates = {} @@ -2033,7 +2056,8 @@ do -- DETECTION_UNITS function DETECTION_UNITS:CreateDetectionItems() -- Loop the current detected items, and check if each object still exists and is detected. - for DetectedItemKey, DetectedItem in pairs( self.DetectedItems ) do + for DetectedItemKey, _DetectedItem in pairs( self.DetectedItems ) do + local DetectedItem=_DetectedItem --#DETECTION_BASE.DetectedItem local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT From 6f140be7108e91f737c7b330d76188d2f207690e Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 5 Jul 2019 20:01:26 +0200 Subject: [PATCH 314/485] Update A2A_DISPATCHER - Docs CONTROLLABLE - OptionROEOpenFireWeaponFree() - Fixed OptionRTBBingoFuel() --- .../Moose/AI/AI_A2A_Dispatcher.lua | 53 +++++++++++++------ .../Moose/Wrapper/Controllable.lua | 52 ++++++++++++++++-- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index a96d3e970..2114b1fce 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -833,7 +833,7 @@ do -- AI_A2A_DISPATCHER --- Squadron data structure. - -- @type AI_A2A_Dispatcher.Squadron + -- @type AI_A2A_DISPATCHER.Squadron -- @field #string Name Name of the squadron. -- @field #number ResourceCount Number of resources. -- @field #string AirbaseName Name of the home airbase. @@ -978,15 +978,24 @@ do -- AI_A2A_DISPATCHER -- @param #string From -- @param #string Event -- @param #string To + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefendersMissing Number of missing defenders. + -- @param #table DefenderFriendlies Friendly defenders. --- GCI Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] GCI -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefendersMissing Number of missing defenders. + -- @param #table DefenderFriendlies Friendly defenders. --- GCI Asynchronous Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] __GCI -- @param #AI_A2A_DISPATCHER self -- @param #number Delay + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefendersMissing Number of missing defenders. + -- @param #table DefenderFriendlies Friendly defenders. self:AddTransition( "*", "ENGAGE", "*" ) @@ -996,6 +1005,8 @@ do -- AI_A2A_DISPATCHER -- @param #string From -- @param #string Event -- @param #string To + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. -- @return #boolean --- ENGAGE Handler OnAfter for AI_A2A_DISPATCHER @@ -1004,15 +1015,21 @@ do -- AI_A2A_DISPATCHER -- @param #string From -- @param #string Event -- @param #string To - + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. + --- ENGAGE Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] ENGAGE -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. --- ENGAGE Asynchronous Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] __ENGAGE -- @param #AI_A2A_DISPATCHER self -- @param #number Delay + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. -- Subscribe to the CRASH event so that when planes are shot @@ -1062,7 +1079,7 @@ do -- AI_A2A_DISPATCHER --- Park defender. -- @param #AI_A2A_DISPATCHER self - -- @param #AI_A2A_Dispatcher.Squadron DefenderSquadron The squadron. + -- @param #AI_A2A_DISPATCHER.Squadron DefenderSquadron The squadron. function AI_A2A_DISPATCHER:ParkDefender( DefenderSquadron ) local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN @@ -1384,7 +1401,7 @@ do -- AI_A2A_DISPATCHER --- Calculates which AI friendlies are nearby the area -- @param #AI_A2A_DISPATCHER self - -- @param DetectedItem + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem -- @return #table A list of the friendlies nearby. function AI_A2A_DISPATCHER:GetAIFriendliesNearBy( DetectedItem ) @@ -1489,7 +1506,7 @@ do -- AI_A2A_DISPATCHER -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target ) - self:F( { SquadronName = SquadronName, Defender = Defender:GetName() } ) + self:F( { SquadronName = SquadronName, Defender = Defender:GetName(), Type=Type, Target=Target } ) self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {} self.DefenderTasks[Defender].Type = Type @@ -1596,7 +1613,7 @@ do -- AI_A2A_DISPATCHER self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - local DefenderSquadron = self.DefenderSquadrons[SquadronName] --#AI_A2A_Dispatcher.Squadron + local DefenderSquadron = self.DefenderSquadrons[SquadronName] --#AI_A2A_DISPATCHER.Squadron DefenderSquadron.Name = SquadronName DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName ) @@ -1628,7 +1645,7 @@ do -- AI_A2A_DISPATCHER --- Get an item from the Squadron table. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName Name of the squadron. - -- @return #AI_A2A_Dispatcher.Squadron Defender squadron table. + -- @return #AI_A2A_DISPATCHER.Squadron Defender squadron table. function AI_A2A_DISPATCHER:GetSquadron( SquadronName ) local DefenderSquadron = self.DefenderSquadrons[SquadronName] @@ -1655,7 +1672,7 @@ do -- AI_A2A_DISPATCHER self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_Dispatcher.Squadron + local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_DISPATCHER.Squadron DefenderSquadron.Uncontrolled = true @@ -1678,7 +1695,7 @@ do -- AI_A2A_DISPATCHER self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_Dispatcher.Squadron + local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_DISPATCHER.Squadron if DefenderSquadron then return DefenderSquadron.Uncontrolled == true @@ -2968,6 +2985,8 @@ do -- AI_A2A_DISPATCHER -- @param #table Defenders Defenders table. function AI_A2A_DISPATCHER:onafterENGAGE( From, Event, To, AttackerDetection, Defenders ) + self:F("ENGAGING "..tostring(AttackerDetection.Name)) + if Defenders then for DefenderID, Defender in pairs( Defenders ) do @@ -2990,6 +3009,8 @@ do -- AI_A2A_DISPATCHER -- @param #number DefendersMissing Number of missing defenders. -- @param #table DefenderFriendlies Friendly defenders. function AI_A2A_DISPATCHER:onafterGCI( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies ) + + self:F("GCI "..tostring(AttackerDetection.Name)) self:F( { From, Event, To, AttackerDetection.Index, DefendersMissing, DefenderFriendlies } ) @@ -3262,7 +3283,7 @@ do -- AI_A2A_DISPATCHER end end - local Report = REPORT:New( "\nTactical Overview" ) + local Report = REPORT:New( "Tactical Overview" ) local DefenderGroupCount = 0 @@ -3299,15 +3320,15 @@ do -- AI_A2A_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- 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 + if Defender and 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", + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", Defender:GetName(), DefenderTask.Type, DefenderTask.Fsm:GetState(), @@ -3322,7 +3343,7 @@ do -- AI_A2A_DISPATCHER end if self.TacticalDisplay then - Report:Add( "\n - No Targets:") + Report:Add( "\n- No Targets:") local TaskCount = 0 for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do TaskCount = TaskCount + 1 @@ -3333,7 +3354,7 @@ do -- AI_A2A_DISPATCHER 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", + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", Defender:GetName(), DefenderTask.Type, DefenderTask.Fsm:GetState(), @@ -3344,7 +3365,7 @@ do -- AI_A2A_DISPATCHER end end end - Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) + Report:Add( string.format( "\n- %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) self:F( Report:Text( "\n" ) ) trigger.action.outText( Report:Text( "\n" ), 25 ) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index a1302776d..3b322abac 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -2806,6 +2806,8 @@ function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRad return nil end +--- Check if a target is detected. +-- @param Wrapper.Controllable#CONTROLLABLE self function CONTROLLABLE:IsTargetDetected( DCSObject, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) self:F2( self.ControllableName ) @@ -2851,7 +2853,7 @@ function CONTROLLABLE:OptionROEHoldFirePossible() return nil end ---- Holding weapons. +--- Weapons Hold: AI will hold fire under all circumstances. -- @param Wrapper.Controllable#CONTROLLABLE self -- @return Wrapper.Controllable#CONTROLLABLE self function CONTROLLABLE:OptionROEHoldFire() @@ -2893,7 +2895,7 @@ function CONTROLLABLE:OptionROEReturnFirePossible() return nil end ---- Return fire. +--- Return Fire: AI will only engage threats that shoot first. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self function CONTROLLABLE:OptionROEReturnFire() @@ -2935,7 +2937,7 @@ function CONTROLLABLE:OptionROEOpenFirePossible() return nil end ---- Openfire. +--- Open Fire (Only Designated): AI will engage only targets specified in its taskings. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self function CONTROLLABLE:OptionROEOpenFire() @@ -2959,6 +2961,45 @@ function CONTROLLABLE:OptionROEOpenFire() return nil end +--- Can the CONTROLLABLE attack priority designated targets? Only for AIR! +-- @param #CONTROLLABLE self +-- @return #boolean +function CONTROLLABLE:OptionROEOpenFireWeaponFreePossible() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + +--- Open Fire, Weapons Free (Priority Designated): AI will engage any enemy group it detects, but will prioritize targets specified in the groups tasking. +-- **Only for AIR units!** +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROEOpenFireWeaponFree() + 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.ROE, AI.Option.Air.val.ROE.OPEN_FIRE_WEAPON_FREE ) + end + + return self + end + + return nil +end + --- Can the CONTROLLABLE attack targets of opportunity? -- @param #CONTROLLABLE self -- @return #boolean @@ -3231,7 +3272,10 @@ end function CONTROLLABLE:OptionRTBBingoFuel( RTB ) --R2.2 self:F2( { self.ControllableName } ) - RTB = RTB or true + --RTB = RTB or true + if RTB==nil then + RTB=true + end local DCSControllable = self:GetDCSObject() if DCSControllable then From 6973e8c028e525a57bbe4e6a48d45a728dface1f Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 6 Jul 2019 20:23:07 +0200 Subject: [PATCH 315/485] CAP AI_A2A_PATROL: - Added optional race track pattern. AI_A2A_DISPATCHER: - Added option for CAP patrolling in a race track pattern. --- .../Moose/AI/AI_A2A_Dispatcher.lua | 97 ++++++++++++++- Moose Development/Moose/AI/AI_A2A_Patrol.lua | 117 ++++++++++++++---- 2 files changed, 187 insertions(+), 27 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 2114b1fce..ecc9005b6 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -643,6 +643,25 @@ do -- AI_A2A_DISPATCHER -- -- Setup the Refuelling for squadron "Gelend", at tanker (group) "TankerGelend" when the fuel in the tank of the CAP defenders is less than 80%. -- A2ADispatcher:SetSquadronFuelThreshold( "Gelend", 0.8 ) -- A2ADispatcher:SetSquadronTanker( "Gelend", "TankerGelend" ) + -- + -- ## 7.4 Set up race track pattern + -- + -- By default, flights patrol randomly within the CAP zone. It is also possible to let them fly a race track pattern using the + -- @{#AI_A2A_DISPATCHER.SetDefaultCapRacetrack}(*LeglengthMin*, *LeglengthMax*, *HeadingMin*, *HeadingMax*, *DurationMin*, *DurationMax*) or + -- @{#AI_A2A_DISPATCHER.SetSquadronCapRacetrack}(*SquadronName*, *LeglengthMin*, *LeglengthMax*, *HeadingMin*, *HeadingMax*, *DurationMin*, *DurationMax*) functions. + -- The first function enables this for all squadrons, the latter only for specific squadrons. For example, + -- + -- -- Enable race track pattern for CAP squadron "Mineralnye". + -- A2ADispatcher:SetSquadronCapRacetrack("Mineralnye", 10000, 20000, 90, 180, 10*60, 20*60) + -- + -- In this case the squadron "Mineralnye" will a race track pattern at a random point in the CAP zone. The leg length will be randomly selected between 10,000 and 20,000 meters. The heading + -- of the race track will randomly selected between 90 (West to East) and 180 (North to South) degrees. + -- After a random duration between 10 and 20 minutes, the flight will get a new random orbit location. + -- + -- Note that all parameters except the squadron name are optional. If not specified, default values are taken. Speed and altitude are taken from the + -- + -- Also note that the center of the race track pattern is chosen randomly within the patrol zone and can be close the the boarder of the zone. Hence, it cannot be guaranteed that the + -- whole pattern lies within the patrol zone. -- -- ## 8. Setup a squadron for GCI: -- @@ -852,6 +871,13 @@ do -- AI_A2A_DISPATCHER -- @field #table Table of template group names of the squadron. -- @field #table Spawn Table of spaws Core.Spawn#SPAWN. -- @field #table TemplatePrefixes + -- @field #boolean Racetrack If true, CAP flights will perform a racetrack pattern rather than randomly patrolling the zone. + -- @field #number RacetrackLengthMin Min Length of race track in meters. Default 10,000 m. + -- @field #number RacetrackLengthMax Max Length of race track in meters. Default 15,000 m. + -- @field #number RacetrackHeadingMin Min heading of race track in degrees. Default 0 deg, i.e. from South to North. + -- @field #number RacetrackHeadingMax Max heading of race track in degrees. Default 180 deg, i.e. from North to South. + -- @field #number RacetrackDurationMin Min duration in seconds before the CAP flight changes its orbit position. Default never. + -- @field #number RacetrackDurationMax Max duration in seconds before the CAP flight changes its orbit position. Default never. --- Enumerator for spawns at airbases -- @type AI_A2A_DISPATCHER.Takeoff @@ -1676,7 +1702,8 @@ do -- AI_A2A_DISPATCHER DefenderSquadron.Uncontrolled = true - for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do + for SpawnTemplate,_DefenderSpawn in pairs( self.DefenderSpawns ) do + local DefenderSpawn=_DefenderSpawn --Core.Spawn#SPAWN DefenderSpawn:InitUnControlled() end @@ -1842,7 +1869,7 @@ do -- AI_A2A_DISPATCHER --- Check if squadron can do CAP. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @return #table DefenderSquadron + -- @return #AI_A2A_DISPATCHER.Squadron DefenderSquadron function AI_A2A_DISPATCHER:CanCAP( SquadronName ) self:F({SquadronName = SquadronName}) @@ -1872,6 +1899,54 @@ do -- AI_A2A_DISPATCHER end + --- Set race track pattern as default when any squadron is performing CAP. + -- @param #AI_A2A_DISPATCHER self + -- @param #number LeglengthMin Min length of the race track leg in meters. Default 10,000 m. + -- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North. + -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from North to South. + -- @param #number DurationMin (Optional) Min duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetDefaultCapRacetrack(LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax) + + self.DefenderDefault.Racetrack=true + self.DefenderDefault.RacetrackLengthMin=LeglengthMin + self.DefenderDefault.RacetrackLengthMax=LeglengthMax + self.DefenderDefault.RacetrackHeadingMin=HeadingMin + self.DefenderDefault.RacetrackHeadingMax=HeadingMax + self.DefenderDefault.RacetrackDurationMin=DurationMin + self.DefenderDefault.RacetrackDurationMax=DurationMax + + return self + end + + --- Set race track pattern when squadron is performing CAP. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName Name of the squadron. + -- @param #number LeglengthMin Min length of the race track leg in meters. Default 10,000 m. + -- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North. + -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from North to South. + -- @param #number DurationMin (Optional) Min duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetSquadronCapRacetrack(SquadronName, LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + DefenderSquadron.Racetrack=true + DefenderSquadron.RacetrackLengthMin=LeglengthMin + DefenderSquadron.RacetrackLengthMax=LeglengthMax + DefenderSquadron.RacetrackHeadingMin=HeadingMin + DefenderSquadron.RacetrackHeadingMax=HeadingMax + DefenderSquadron.RacetrackDurationMin=DurationMin + DefenderSquadron.RacetrackDurationMax=DurationMax + end + + return self + end + + --- Check if squadron can do GCI. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -2927,6 +3002,14 @@ do -- AI_A2A_DISPATCHER Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) Fsm:SetDisengageRadius( self.DisengageRadius ) Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) + if DefenderSquadron.Racetrack or self.DefenderDefault.Racetrack then + Fsm:SetRaceTrackPattern(DefenderSquadron.RacetrackLengthMin or self.DefenderDefault.RacetrackLengthMin, + DefenderSquadron.RacetrackLengthMax or self.DefenderDefault.RacetrackLengthMax, + DefenderSquadron.RacetrackHeadingMin or self.DefenderDefault.RacetrackHeadingMin, + DefenderSquadron.RacetrackHeadingMax or self.DefenderDefault.RacetrackHeadingMax, + DefenderSquadron.RacetrackDurationMin or self.DefenderDefault.RacetrackDurationMin, + DefenderSquadron.RacetrackDurationMax or self.DefenderDefault.RacetrackDurationMax) + end Fsm:Start() self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", Fsm ) @@ -3328,8 +3411,10 @@ do -- AI_A2A_DISPATCHER 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(), + Report:Add( string.format( " - %s*%d/%d ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + Defender:GetSize(), + Defender:GetInitialSize(), DefenderTask.Type, DefenderTask.Fsm:GetState(), Defender:GetSize(), @@ -3354,8 +3439,10 @@ do -- AI_A2A_DISPATCHER 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", + Report:Add( string.format( " - %s*%d/%d ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", Defender:GetName(), + Defender:GetSize(), + Defender:GetInitialSize(), DefenderTask.Type, DefenderTask.Fsm:GetState(), Defender:GetSize(), diff --git a/Moose Development/Moose/AI/AI_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index 7e57c24d0..68baf0085 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -257,6 +257,31 @@ function AI_A2A_PATROL:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) self.PatrolCeilingAltitude = PatrolCeilingAltitude end +--- Set race track parameters. CAP flights will perform race track patterns rather than randomly patrolling the zone. +-- @param #AI_A2A_PATROL self +-- @param #number LegMin Min Length of the race track leg in meters. Default 10,000 m. +-- @param #number LegMax Max length of the race track leg in meters. Default 15,000 m. +-- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North. +-- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from South to North. +-- @param #number duration (Optional) Min duration before switching the orbit position. Default is keep same orbit until RTB or engage. +-- @param #number duration (Optional) Max duration before switching the orbit position. Default is keep same orbit until RTB or engage. +-- @return #AI_A2A_PATROL self +function AI_A2A_PATROL:SetRaceTrackPattern(LegMin, LegMax, HeadingMin, HeadingMax, DurationMin, DurationMax) + self:F2({leglength, duration}) + + self.racetrack=true + self.racetracklegmin=LegMin or 10000 + self.racetracklegmax=LegMax or 15000 + self.racetrackheadingmin=HeadingMin or 0 + self.racetrackheadingmax=HeadingMax or 180 + self.racetrackdurationmin=DurationMin + self.racetrackdurationmax=DurationMax + + if self.racetrackdurationmax and not self.racetrackdurationmin then + self.racetrackdurationmin=self.racetrackdurationmax + end +end + --- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. -- @param #AI_A2A_PATROL self @@ -312,7 +337,7 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) end - if AIPatrol:IsAlive() then + if AIPatrol and AIPatrol:IsAlive() then local PatrolRoute = {} @@ -320,31 +345,79 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) local CurrentCoord = AIPatrol:GetCoordinate() - local ToTargetCoord = self.PatrolZone:GetRandomPointVec2() - ToTargetCoord:SetAlt( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) ) - self:SetTargetDistance( ToTargetCoord ) -- For RTB status check + if self.racetrack then - 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 - ) + -- Random altitude. + local altitude=math.random(self.PatrolFloorAltitude, self.PatrolCeilingAltitude) + + -- Random speed in km/h. + local speedkmh = math.random(self.PatrolMinSpeed, self.PatrolMaxSpeed) + + -- Random heading. + local heading = math.random(self.racetrackheadingmin, self.racetrackheadingmax) + + -- Random leg length. + local leg=math.random(self.racetracklegmin, self.racetracklegmax) + + -- Random duration if any. + local duration = self.racetrackdurationmin + if self.racetrackdurationmax then + duration=math.random(self.racetrackdurationmin, self.racetrackdurationmax) + end + + -- Race track points. + local c1=self.PatrolZone:GetRandomCoordinate():SetAltitude(altitude) --Core.Point#COORDINATE + local c2=c1:Translate(leg, heading):SetAltitude(altitude) + + -- Debug: + self:T(string.format("Patrol zone race track: v=%.1f knots, h=%.1f ft, heading=%03d, leg=%d m, t=%s sec", UTILS.KmphToKnots(speedkmh), UTILS.MetersToFeet(altitude), heading, leg, tostring(duration))) + --c1:MarkToAll("Race track c1") + --c2:MarkToAll("Race track c2") - PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint - PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint - - local Tasks = {} - Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2A_PATROL.PatrolRoute", self ) - PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) - + -- Task to orbit. + local taskOrbit=AIPatrol:TaskOrbit(c1, altitude, UTILS.KmphToMps(speedkmh), c2) + + -- Task function to redo the patrol at other random position. + local taskPatrol=AIPatrol:TaskFunction("AI_A2A_PATROL.PatrolRoute", self) + + + local taskCond=AIPatrol:TaskCondition(nil, nil, nil, nil, duration, nil) + local taskCont=AIPatrol:TaskControlled(taskOrbit, taskCond) + + PatrolRoute[1]=CurrentCoord:WaypointAirTurningPoint(nil, speedkmh, {}, "Current") + PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskCont, taskPatrol}, "Orbit") + + else + + 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 + ) + + PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint + PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint + + local Tasks = {} + Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2A_PATROL.PatrolRoute", self ) + PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) + + end + + -- ROE AIPatrol:OptionROEReturnFire() AIPatrol:OptionROTEvadeFire() - + + -- Patrol. AIPatrol:Route( PatrolRoute, 0.5) end From 183c05bcf5dfe78fb65a0d751a1e96a6281954ec Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 6 Jul 2019 23:39:48 +0200 Subject: [PATCH 316/485] CAP AI_A2A_PATROL - Added CAP coordinates option. AI_A2A_DISPATCHER - Added CAP coordinates option. --- .../Moose/AI/AI_A2A_Dispatcher.lua | 25 ++++++++++++------- Moose Development/Moose/AI/AI_A2A_Patrol.lua | 20 +++++++++++---- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index ecc9005b6..cafbfd139 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -1902,12 +1902,14 @@ do -- AI_A2A_DISPATCHER --- Set race track pattern as default when any squadron is performing CAP. -- @param #AI_A2A_DISPATCHER self -- @param #number LeglengthMin Min length of the race track leg in meters. Default 10,000 m. - -- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North. - -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from North to South. + -- @param #number LeglengthMax Max length of the race track leg in meters. Default 15,000 m. + -- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. counter clockwise from South to North. + -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. counter clockwise from North to South. -- @param #number DurationMin (Optional) Min duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. -- @return #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:SetDefaultCapRacetrack(LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax) + function AI_A2A_DISPATCHER:SetDefaultCapRacetrack(LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) self.DefenderDefault.Racetrack=true self.DefenderDefault.RacetrackLengthMin=LeglengthMin @@ -1916,6 +1918,7 @@ do -- AI_A2A_DISPATCHER self.DefenderDefault.RacetrackHeadingMax=HeadingMax self.DefenderDefault.RacetrackDurationMin=DurationMin self.DefenderDefault.RacetrackDurationMax=DurationMax + self.DefenderDefault.RacetrackCoordinates=CapCoordinates return self end @@ -1924,12 +1927,14 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName Name of the squadron. -- @param #number LeglengthMin Min length of the race track leg in meters. Default 10,000 m. + -- @param #number LeglengthMax Max length of the race track leg in meters. Default 15,000 m. -- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North. -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from North to South. -- @param #number DurationMin (Optional) Min duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. -- @return #AI_A2A_DISPATCHER self - function AI_A2A_DISPATCHER:SetSquadronCapRacetrack(SquadronName, LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax) + function AI_A2A_DISPATCHER:SetSquadronCapRacetrack(SquadronName, LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -1941,6 +1946,7 @@ do -- AI_A2A_DISPATCHER DefenderSquadron.RacetrackHeadingMax=HeadingMax DefenderSquadron.RacetrackDurationMin=DurationMin DefenderSquadron.RacetrackDurationMax=DurationMax + DefenderSquadron.RacetrackCoordinates=CapCoordinates end return self @@ -3008,7 +3014,8 @@ do -- AI_A2A_DISPATCHER DefenderSquadron.RacetrackHeadingMin or self.DefenderDefault.RacetrackHeadingMin, DefenderSquadron.RacetrackHeadingMax or self.DefenderDefault.RacetrackHeadingMax, DefenderSquadron.RacetrackDurationMin or self.DefenderDefault.RacetrackDurationMin, - DefenderSquadron.RacetrackDurationMax or self.DefenderDefault.RacetrackDurationMax) + DefenderSquadron.RacetrackDurationMax or self.DefenderDefault.RacetrackDurationMax, + DefenderSquadron.RacetrackCoordinates or self.DefenderDefault.RacetrackCoordinates) end Fsm:Start() @@ -3366,7 +3373,7 @@ do -- AI_A2A_DISPATCHER end end - local Report = REPORT:New( "Tactical Overview" ) + local Report = REPORT:New( "Tactical Overviews" ) local DefenderGroupCount = 0 @@ -3403,7 +3410,7 @@ do -- AI_A2A_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- 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 @@ -3411,7 +3418,7 @@ do -- AI_A2A_DISPATCHER DefenderGroupCount = DefenderGroupCount + 1 local Fuel = Defender:GetFuelMin() * 100 local Damage = Defender:GetLife() / Defender:GetLife0() * 100 - Report:Add( string.format( " - %s*%d/%d ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", Defender:GetName(), Defender:GetSize(), Defender:GetInitialSize(), @@ -3439,7 +3446,7 @@ do -- AI_A2A_DISPATCHER local Fuel = Defender:GetFuelMin() * 100 local Damage = Defender:GetLife() / Defender:GetLife0() * 100 DefenderGroupCount = DefenderGroupCount + 1 - Report:Add( string.format( " - %s*%d/%d ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", Defender:GetName(), Defender:GetSize(), Defender:GetInitialSize(), diff --git a/Moose Development/Moose/AI/AI_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index 68baf0085..5071d1729 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -263,10 +263,11 @@ end -- @param #number LegMax Max length of the race track leg in meters. Default 15,000 m. -- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North. -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from South to North. --- @param #number duration (Optional) Min duration before switching the orbit position. Default is keep same orbit until RTB or engage. --- @param #number duration (Optional) Max duration before switching the orbit position. Default is keep same orbit until RTB or engage. +-- @param #number DurationMin (Optional) Min duration before switching the orbit position. Default is keep same orbit until RTB or engage. +-- @param #number DurationMax (Optional) Max duration before switching the orbit position. Default is keep same orbit until RTB or engage. +-- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. -- @return #AI_A2A_PATROL self -function AI_A2A_PATROL:SetRaceTrackPattern(LegMin, LegMax, HeadingMin, HeadingMax, DurationMin, DurationMax) +function AI_A2A_PATROL:SetRaceTrackPattern(LegMin, LegMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) self:F2({leglength, duration}) self.racetrack=true @@ -279,7 +280,10 @@ function AI_A2A_PATROL:SetRaceTrackPattern(LegMin, LegMax, HeadingMin, HeadingMa if self.racetrackdurationmax and not self.racetrackdurationmin then self.racetrackdurationmin=self.racetrackdurationmax - end + end + + self.racetrackcapcoordinates=CapCoordinates + end @@ -365,8 +369,14 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) duration=math.random(self.racetrackdurationmin, self.racetrackdurationmax) end + -- CAP coordinate. + local c0=self.PatrolZone:GetRandomCoordinate() + if self.racetrackcapcoordinates and #self.racetrackcapcoordinates>0 then + c0=self.racetrackcapcoordinates[math.random(#self.racetrackcapcoordinates)] + end + -- Race track points. - local c1=self.PatrolZone:GetRandomCoordinate():SetAltitude(altitude) --Core.Point#COORDINATE + local c1=c0:SetAltitude(altitude) --Core.Point#COORDINATE local c2=c1:Translate(leg, heading):SetAltitude(altitude) -- Debug: From c46028ff2df67093b6f66391e7cdd519229f6c2b Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 10 Jul 2019 22:29:49 +0200 Subject: [PATCH 317/485] FOX v0.6.0 FOX v0.6.0 - Missile target constantly updated. - Increased safety distance to 200 m. - Added safety distance for BIG missiles > 50 kg TNT as 400 m. - More output to dcs.log file. SPAWN - enabled uncontrolled UTILS - LLDMS accurracy fix. AI_A2A_DISPATCHER - Squadron visible improvements ZONE - Added MarkZone function for F10 zone markings. AI_PATROL_ZONE - Some WP code improvements. --- Moose Development/Moose/AI/AI_A2A_Cap.lua | 4 +- .../Moose/AI/AI_A2A_Dispatcher.lua | 96 +++++- Moose Development/Moose/AI/AI_A2A_Patrol.lua | 51 ++-- Moose Development/Moose/Core/Spawn.lua | 2 +- Moose Development/Moose/Core/Zone.lua | 27 ++ .../Moose/Functional/Detection.lua | 12 +- Moose Development/Moose/Functional/Fox.lua | 282 +++++++++++++++--- Moose Development/Moose/Utilities/Utils.lua | 12 +- 8 files changed, 397 insertions(+), 89 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Cap.lua b/Moose Development/Moose/AI/AI_A2A_Cap.lua index de9e184da..49b87b3c0 100644 --- a/Moose Development/Moose/AI/AI_A2A_Cap.lua +++ b/Moose Development/Moose/AI/AI_A2A_Cap.lua @@ -386,7 +386,7 @@ function AI_A2A_CAP:onafterEngage( AICap, From, Event, To, AttackSetUnit ) if FirstAttackUnit and FirstAttackUnit:IsAlive() then -- If there is no attacker anymore, stop the engagement. - if AICap:IsAlive() then + if AICap and AICap:IsAlive() then local EngageRoute = {} @@ -417,6 +417,8 @@ function AI_A2A_CAP:onafterEngage( AICap, From, Event, To, AttackSetUnit ) local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT self:T( { "Attacking Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) if AttackUnit:IsAlive() and AttackUnit:IsAir() then + -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition() + -- Maybe the detected set also contains AttackTasks[#AttackTasks+1] = AICap:TaskAttackUnit( AttackUnit ) end end diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index cafbfd139..c19b2fec7 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -1092,8 +1092,9 @@ do -- AI_A2A_DISPATCHER self:GetParent( self, AI_A2A_DISPATCHER ).onafterStart( self, From, Event, To ) -- Spawn the resources. - for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do - DefenderSquadron.Resource = {} + for SquadronName,_DefenderSquadron in pairs( self.DefenderSquadrons ) do + local DefenderSquadron=_DefenderSquadron --#AI_A2A_DISPATCHER.Squadron + DefenderSquadron.Resources = {} if DefenderSquadron.ResourceCount then for Resource = 1, DefenderSquadron.ResourceCount do self:ParkDefender( DefenderSquadron ) @@ -1107,18 +1108,39 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param #AI_A2A_DISPATCHER.Squadron DefenderSquadron The squadron. 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 + + local Grouping=DefenderSquadron.Grouping or self.DefenderDefault.Grouping + + Grouping=1 + + Spawn:InitGrouping(Grouping) + 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 + + self.uncontrolled=self.uncontrolled or {} + self.uncontrolled[DefenderSquadron.Name]=self.uncontrolled[DefenderSquadron.Name] or {} + + table.insert(self.uncontrolled[DefenderSquadron.Name], {group=SpawnGroup, name=GroupName, grouping=Grouping}) end + end @@ -1701,10 +1723,23 @@ do -- AI_A2A_DISPATCHER local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_DISPATCHER.Squadron DefenderSquadron.Uncontrolled = true + + -- For now, grouping is forced to 1 due to other parts of the class which would not work well with grouping>1. + DefenderSquadron.Grouping=1 + + -- Get free parking for fighter aircraft. + local nfreeparking=DefenderSquadron.Airbase:GetFreeParkingSpotsNumber(AIRBASE.TerminalType.FighterAircraft, true) + + -- Take number of free parking spots if no resource count was specifed. + DefenderSquadron.ResourceCount=DefenderSquadron.ResourceCount or nfreeparking + + -- Check that resource count is not larger than free parking spots. + DefenderSquadron.ResourceCount=math.min(DefenderSquadron.ResourceCount, nfreeparking) + -- Set uncontrolled spawning option. for SpawnTemplate,_DefenderSpawn in pairs( self.DefenderSpawns ) do local DefenderSpawn=_DefenderSpawn --Core.Spawn#SPAWN - DefenderSpawn:InitUnControlled() + DefenderSpawn:InitUnControlled(true) end end @@ -2751,7 +2786,7 @@ do -- AI_A2A_DISPATCHER --- Get squadron from defender. -- @param #AI_A2A_DISPATCHER self -- @param Wrapper.Group#GROUP Defender The defender group. - -- @return ? + -- @return #AI_A2A_DISPATCHER.Squadron Squadron The squadron. function AI_A2A_DISPATCHER:GetSquadronFromDefender( Defender ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() @@ -2800,8 +2835,9 @@ do -- AI_A2A_DISPATCHER if AIGroup and 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! - if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) - or DefenderTask.Fsm:Is( "Started" ) then + --env.info("FF fsm state "..tostring(DefenderTask.Fsm:GetState())) + if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) or DefenderTask.Fsm:Is( "Started" ) then + --env.info("FF capcount "..CapCount) CapCount = CapCount + 1 end end @@ -2908,16 +2944,48 @@ do -- AI_A2A_DISPATCHER 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 + --env.info(string.format("FF resource activate: Squadron=%s grouping=%d needed=%d visible=%s", SquadronName, DefenderGrouping, DefendersNeeded, tostring(self:IsSquadronVisible( SquadronName )))) + if self:IsSquadronVisible( SquadronName ) then + + local n=#self.uncontrolled[SquadronName] + + if n>0 then + -- Random number 1,...n + local id=math.random(n) + + -- Pick a random defender group. + local Defender=self.uncontrolled[SquadronName][id].group --Wrapper.Group#GROUP + + -- Start uncontrolled group. + Defender:StartUncontrolled() + + -- Get grouping. + DefenderGrouping=self.uncontrolled[SquadronName][id].grouping + + -- Add defender to squadron. + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + + -- Remove defender from uncontrolled table. + table.remove(self.uncontrolled[SquadronName], id) + + return Defender, DefenderGrouping + else + return nil,0 + end + -- 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 } ) @@ -2960,8 +3028,16 @@ do -- AI_A2A_DISPATCHER self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) return Defender, DefenderGrouping end + ]] + else + + ---------------------------- + --- Squadron not visible --- + ---------------------------- + local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN + if DefenderGrouping then Spawn:InitGrouping( DefenderGrouping ) else @@ -2969,8 +3045,11 @@ do -- AI_A2A_DISPATCHER 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 @@ -3261,6 +3340,7 @@ do -- AI_A2A_DISPATCHER Dispatcher:ParkDefender( Squadron ) end end + end -- if DefenderGCI then end -- while ( DefendersNeeded > 0 ) do end diff --git a/Moose Development/Moose/AI/AI_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index 5071d1729..6b1315440 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -349,13 +349,16 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) local CurrentCoord = AIPatrol:GetCoordinate() - if self.racetrack then - - -- Random altitude. - local altitude=math.random(self.PatrolFloorAltitude, self.PatrolCeilingAltitude) + -- Random altitude. + local altitude=math.random(self.PatrolFloorAltitude, self.PatrolCeilingAltitude) - -- Random speed in km/h. - local speedkmh = math.random(self.PatrolMinSpeed, self.PatrolMaxSpeed) + -- Random speed in km/h. + local speedkmh = math.random(self.PatrolMinSpeed, self.PatrolMaxSpeed) + + -- First waypoint is current position. + PatrolRoute[1]=CurrentCoord:WaypointAirTurningPoint(nil, speedkmh, {}, "Current") + + if self.racetrack then -- Random heading. local heading = math.random(self.racetrackheadingmin, self.racetrackheadingmax) @@ -379,6 +382,8 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) local c1=c0:SetAltitude(altitude) --Core.Point#COORDINATE local c2=c1:Translate(leg, heading):SetAltitude(altitude) + self:SetTargetDistance(c0) -- For RTB status check + -- Debug: self:T(string.format("Patrol zone race track: v=%.1f knots, h=%.1f ft, heading=%03d, leg=%d m, t=%s sec", UTILS.KmphToKnots(speedkmh), UTILS.MetersToFeet(altitude), heading, leg, tostring(duration))) --c1:MarkToAll("Race track c1") @@ -390,37 +395,25 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) -- Task function to redo the patrol at other random position. local taskPatrol=AIPatrol:TaskFunction("AI_A2A_PATROL.PatrolRoute", self) - + -- Controlled task with task condition. local taskCond=AIPatrol:TaskCondition(nil, nil, nil, nil, duration, nil) local taskCont=AIPatrol:TaskControlled(taskOrbit, taskCond) - PatrolRoute[1]=CurrentCoord:WaypointAirTurningPoint(nil, speedkmh, {}, "Current") - PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskCont, taskPatrol}, "Orbit") + -- Second waypoint + PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskCont, taskPatrol}, "CAP Orbit") else - - local ToTargetCoord = self.PatrolZone:GetRandomPointVec2() - ToTargetCoord:SetAlt( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) ) + + -- Target coordinate. + local ToTargetCoord=self.PatrolZone:GetRandomCoordinate() --Core.Point#COORDINATE + ToTargetCoord:SetAltitude(altitude) + 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 - ) - - PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint - PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint - - local Tasks = {} - Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2A_PATROL.PatrolRoute", self ) - PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) + local taskReRoute=AIPatrol:TaskFunction( "AI_A2A_PATROL.PatrolRoute", self ) + PatrolRoute[2]=ToTargetCoord:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskReRoute}, "Patrol Point") + end -- ROE diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index ff6387c5a..f9cd1b97b 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1816,7 +1816,7 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT SpawnTemplate.x = PointVec3.x SpawnTemplate.y = PointVec3.z - SpawnTemplate.uncontrolled = nil + SpawnTemplate.uncontrolled = self.SpawnUnControlled -- Spawn group. local GroupSpawned = self:SpawnWithIndex( self.SpawnIndex ) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 871404e96..52c27fd05 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -442,6 +442,33 @@ function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) return self end +--- Mark the zone with markers on the F10 map. +-- @param #ZONE_RADIUS self +-- @param #number Points (Optional) The amount of points in the circle. Default 360. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:MarkZone(Points) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, (360 / Points ) do + + local Radial = Angle * RadialBase / 360 + + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + + COORDINATE:NewFromVec2(Point):MarkToAll(self:GetName()) + + end + +end + --- Bounds the zone with tires. -- @param #ZONE_RADIUS self -- @param #number Points (optional) The amount of points in the circle. Default 360. diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 851977790..d711ad9b6 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -458,15 +458,18 @@ do -- DETECTION_BASE -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. + -- @param #table Units Table of detected units. --- Synchronous Event Trigger for Event Detected. -- @function [parent=#DETECTION_BASE] Detected -- @param #DETECTION_BASE self + -- @param #table Units Table of detected units. --- Asynchronous Event Trigger for Event Detected. -- @function [parent=#DETECTION_BASE] __Detected -- @param #DETECTION_BASE self -- @param #number Delay The delay in seconds. + -- @param #table Units Table of detected units. self:AddTransition( "Detecting", "DetectedItem", "Detecting" ) @@ -586,7 +589,7 @@ do -- DETECTION_BASE local HasDetectedObjects = false - if Detection:IsAlive() then + if Detection and Detection:IsAlive() then --self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) @@ -817,12 +820,17 @@ do -- DETECTION_BASE end self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method. + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:UpdateDetectedItemDetection( DetectedItem ) + self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list. + if DetectedItem then self:__DetectedItem( 0.1, DetectedItem ) end + end end @@ -834,7 +842,7 @@ do -- DETECTION_BASE do -- DetectionItems Creation - -- Clean the DetectedItem table. + --- Clean the DetectedItem table. -- @param #DETECTION_BASE self -- @return #DETECTION_BASE function DETECTION_BASE:CleanDetectionItem( DetectedItem, DetectedItemID ) diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index 69afa5761..e2ab614f3 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -40,7 +40,9 @@ -- @field #table launchzones Table of launch zones. -- @field Core.Set#SET_GROUP protectedset Set of protected groups. -- @field #number explosionpower Power of explostion when destroying the missile in kg TNT. Default 5 kg TNT. --- @field #number explosiondist Missile player distance in meters for destroying the missile. Default 100 m. +-- @field #number explosiondist Missile player distance in meters for destroying smaller missiles. Default 200 m. +-- @field #number explosiondist2 Missile player distance in meters for destroying big missiles. Default 400 m. +-- @field #number bigmissilemass Explosion power of big missiles. Default 50 kg TNT. Big missiles will be destroyed earlier. -- @field #number dt50 Time step [sec] for missile position updates if distance to target > 50 km. Default 5 sec. -- @field #number dt10 Time step [sec] for missile position updates if distance to target > 10 km and < 50 km. Default 1 sec. -- @field #number dt05 Time step [sec] for missile position updates if distance to target > 5 km and < 10 km. Default 0.5 sec. @@ -136,8 +138,10 @@ FOX = { safezones = {}, launchzones = {}, protectedset = nil, - explosionpower = 5, - explosiondist = 100, + explosionpower = 0.5, + explosiondist = 200, + explosiondist2 = 400, + bigmissilemass = 50, destroy = nil, dt50 = 5, dt10 = 1, @@ -169,14 +173,19 @@ FOX = { -- @field Wrapper.Unit#UNIT weapon Missile weapon unit. -- @field #boolean active If true the missile is active. -- @field #string missileType Type of missile. +-- @field #string missileName Name of missile. -- @field #number missileRange Range of missile in meters. +-- @field #number fuseDist Fuse distance in meters. +-- @field #number explosive Explosive mass in kg TNT. -- @field Wrapper.Unit#UNIT shooterUnit Unit that shot the missile. -- @field Wrapper.Group#GROUP shooterGroup Group that shot the missile. -- @field #number shooterCoalition Coalition side of the shooter. -- @field #string shooterName Name of the shooter unit. --- @field #number shotTime Abs mission time in seconds the missile was fired. +-- @field #number shotTime Abs. mission time in seconds the missile was fired. -- @field Core.Point#COORDINATE shotCoord Coordinate where the missile was fired. -- @field Wrapper.Unit#UNIT targetUnit Unit that was targeted. +-- @field #string targetName Name of the target unit or "unknown". +-- @field #string targetOrig Name of the "original" target, i.e. the one right after launched. -- @field #FOX.PlayerData targetPlayer Player that was targeted or nil. --- Main radio menu on group level. @@ -189,7 +198,7 @@ FOX.MenuF10Root=nil --- FOX class version. -- @field #string version -FOX.version="0.5.1" +FOX.version="0.6.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -217,6 +226,10 @@ function FOX:New() self:SetDefaultMissileDestruction(true) self:SetDefaultLaunchAlerts(true) self:SetDefaultLaunchMarks(true) + + -- Explosion/destruction defaults. + self:SetExplosionDistance() + self:SetExplosionDistanceBigMissiles() self:SetExplosionPower() -- Start State. @@ -358,12 +371,14 @@ function FOX:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Shot) + --self:HandleEvent(EVENTS.Hit) + if self.Debug then self:TraceClass(self.ClassName) self:TraceLevel(2) end - self:__Status(-10) + self:__Status(-20) end --- On after Stop event. Stops the missile trainer and unhandles events. @@ -378,8 +393,10 @@ function FOX:onafterStop(From, Event, To) env.info(text) -- Handle events: - self:UnhandleEvent(EVENTS.Birth) - self:UnhandleEvent(EVENTS.Shot) + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Shot) + + --self:UnhandleEvent(EVENTS.Hit) end @@ -433,28 +450,42 @@ function FOX:AddProtectedGroup(group) return self end ---- Set explosion power. +--- Set explosion power. This is an "artificial" explosion generated when the missile is destroyed. Just for the visual effect. +-- Don't set the explosion power too big or it will harm the aircraft in the vicinity. -- @param #FOX self --- @param #number power Explosion power in kg TNT. Default 5. +-- @param #number power Explosion power in kg TNT. Default 0.5 kg. -- @return #FOX self function FOX:SetExplosionPower(power) - self.explosionpower=power or 5 + self.explosionpower=power or 0.5 return self end --- Set missile-player distance when missile is destroyed. -- @param #FOX self --- @param #number distance Distance in meters. Default 100 m. +-- @param #number distance Distance in meters. Default 200 m. -- @return #FOX self function FOX:SetExplosionDistance(distance) - self.explosiondist=distance or 100 + self.explosiondist=distance or 200 return self end +--- Set missile-player distance when BIG missiles are destroyed. +-- @param #FOX self +-- @param #number distance Distance in meters. Default 400 m. +-- @param #number explosivemass Explosive mass of missile in kg TNT. Default 50 kg. +-- @return #FOX self +function FOX:SetExplosionDistanceBigMissiles(distance, explosivemass) + + self.explosiondist2=distance or 400 + + self.bigmissilemass=explosivemass or 50 + + return self +end --- Disable F10 menu for all players. -- @param #FOX self @@ -558,8 +589,11 @@ function FOX:onafterStatus(From, Event, To) -- Get FSM state. local fsmstate=self:GetState() + local time=timer.getAbsTime() + local clock=UTILS.SecondsToClock(time) + -- Status. - self:I(self.lid..string.format("Missile trainer status: %s", fsmstate)) + self:I(self.lid..string.format("Missile trainer status %s: %s", clock, fsmstate)) -- Check missile status. self:_CheckMissileStatus() @@ -664,6 +698,9 @@ function FOX:_CheckMissileStatus() text=text..string.format("\n[%d] %s: active=%s, range=%.1f NM, heading=%03d, target=%s, player=%s, missilename=%s", i, mtype, active, range, heading, targetname, playername, missile.missileName) end + if #self.missiles==0 then + text=text.." none" + end self:I(self.lid..text) -- Remove inactive missiles. @@ -722,7 +759,7 @@ end function FOX:onafterMissileLaunch(From, Event, To, missile) -- Tracking info and init of last bomb position. - self:I(FOX.lid..string.format("FOX: Tracking %s - %s.", missile.missileType, missile.missileName)) + self:I(FOX.lid..string.format("FOX: Tracking %s - %s - target %s", missile.missileType, missile.missileName, tostring(missile.targetName))) -- Loop over players. for _,_player in pairs(self.players) do @@ -803,6 +840,9 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) -- Missile velocity in m/s. local missileVelocity=UTILS.VecNorm(_ordnance:getVelocity()) + -- Update missile target if necessary. + self:GetMissileTarget(missile) + if missile.targetUnit then ----------------------------------- @@ -825,7 +865,31 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) ------------------------------------ -- Missile has NO specific target -- - ------------------------------------ + ------------------------------------ + + -- TODO: This might cause a problem with wingman. Even if the shooter itself is excluded from the check, it's wingmen are not. + -- That would trigger the distance check right after missile launch if things to wrong. + -- + -- Possible solutions: + -- * Time check: enable this check after X seconds after missile was fired. What is X? + -- * Coalition check. But would not work in training situations where blue on blue is valid! + -- * At least enable it for surface-to-air missiles. + + --[[ + local function _GetTarget(_unit) + local unit=_unit --Wrapper.Unit#UNIT + + -- Player position. + local playerCoord=unit:GetCoordinate() + + -- Distance. + local dist=missileCoord:Get3DDistance(playerCoord) + + -- Update mindist if necessary. Only include players in range of missile + 50% safety margin. + if dist<=self.explosiondist then + return unit + end + end -- Distance to closest player. local mindist=nil @@ -843,17 +907,59 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) -- Distance. local dist=missileCoord:Get3DDistance(playerCoord) - -- Maxrange from launch point to player. - local maxrange=playerCoord:Get3DDistance(missile.shotCoord) + -- Distance from shooter to player. + local Dshooter2player=playerCoord:Get3DDistance(missile.shotCoord) - -- Update mindist if necessary. Only include players in range of missile. - if (mindist==nil or dist=self.bigmissilemass + end - -- If missile is 100 m from target ==> destroy missile if in safe zone. - if distance<=self.explosiondist and self:_CheckCoordSafe(targetCoord)then + -- If missile is 150 m from target ==> destroy missile if in safe zone. + if destroymissile and self:_CheckCoordSafe(targetCoord) then -- Destroy missile. - self:T(self.lid..string.format("Destroying missile at distance %.1f m", distance)) + self:I(self.lid..string.format("Destroying missile %s(%s) fired by %s aimed at %s [player=%s] at distance %.1f m", + missile.missileType, missile.missileName, missile.shooterName, target:GetName(), tostring(missile.targetPlayer~=nil), distance)) _ordnance:destroy() -- Missile is not active any more. missile.active=false + -- Debug smoke. + missileCoord:SmokeRed() + targetCoord:SmokeGreen() + -- Create event. self:MissileDestroyed(missile) -- Little explosion for the visual effect. - if self.explosionpower>0 then + if self.explosionpower>0 and distance>=50 then missileCoord:Explosion(self.explosionpower) end + -- Message to target. local text=string.format("Destroying missile. %s", self:_DeadText()) MESSAGE:New(text, 10):ToGroup(target:GetGroup()) @@ -898,6 +1023,7 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) -- Terminate timer. return nil + else -- Time step. @@ -922,10 +1048,15 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) -- Check again in dt seconds. return timer.getTime()+dt end + else + -- Destroy missile. + self:I(self.lid..string.format("Missile %s(%s) fired by %s has no current target. Checking back in 0.1 sec.", missile.missileType, missile.missileName, missile.shooterName)) + return timer.getTime()+0.1 + -- No target ==> terminate timer. - return nil + --return nil end else @@ -945,7 +1076,7 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) MESSAGE:New(text, 10):ToClient(player.client) -- Increase defeated counter. - player.defeated=player.defeated+1 + player.defeated=player.defeated+1 end end @@ -1041,11 +1172,52 @@ function FOX:OnEventBirth(EventData) end end +--- Get missile target. +-- @param #FOX self +-- @param #FOX.MissileData missile The missile data table. +function FOX:GetMissileTarget(missile) + + local target=nil + local targetName="unknown" + local targetUnit=nil --Wrapper.Unit#UNIT + + if missile.weapon and missile.weapon:isExist() then + + -- Get target of missile. + target=missile.weapon:getTarget() + + -- Get the target unit. Note if if _target is not nil, the unit can sometimes not be found! + if target then + self:T2({missiletarget=target}) + + -- Get target unit. + targetUnit=UNIT:Find(target) + + if targetUnit then + targetName=targetUnit:GetName() + + missile.targetUnit=targetUnit + missile.targetPlayer=self:_GetPlayerFromUnit(missile.targetUnit) + end + + end + end + + -- Missile got new target. + if missile.targetName and missile.targetName~=targetName then + self:I(self.lid..string.format("Missile %s(%s) changed target to %s. Previous target was %s.", missile.missileType, missile.missileName, targetName, missile.targetName)) + end + + -- Set target name. + missile.targetName=targetName + +end + --- FOX event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). -- @param #FOX self -- @param Core.Event#EVENTDATA EventData function FOX:OnEventShot(EventData) - self:I({eventshot = EventData}) + self:T2({eventshot=EventData}) if EventData.Weapon==nil then return @@ -1062,7 +1234,7 @@ function FOX:OnEventShot(EventData) -- Weapon descriptor. local desc=EventData.Weapon:getDesc() - self:E({desc=desc}) + self:T2({desc=desc}) -- Weapon category: 0=Shell, 1=Missile, 2=Rocket, 3=BOMB local weaponcategory=desc.category @@ -1091,15 +1263,6 @@ function FOX:OnEventShot(EventData) return end - -- Get the target unit. Note if if _target is not nil, the unit can sometimes not be found! - if _target then - self:E({target=_target}) - --_targetName=Unit.getName(_target) - --_targetUnit=UNIT:FindByName(_targetName) - _targetUnit=UNIT:Find(_target) - end - self:E(FOX.lid..string.format("EVENT SHOT: Target name = %s", tostring(_targetName))) - -- Track missiles of type AAM=1, SAM=2 or OTHER=6 local _track = weaponcategory==1 and missilecategory and (missilecategory==1 or missilecategory==2 or missilecategory==6) @@ -1119,8 +1282,13 @@ function FOX:OnEventShot(EventData) missile.shooterName=EventData.IniUnitName missile.shotTime=timer.getAbsTime() missile.shotCoord=EventData.IniUnit:GetCoordinate() - missile.targetUnit=_targetUnit - missile.targetPlayer=self:_GetPlayerFromUnit(missile.targetUnit) + missile.fuseDist=desc.fuseDist + missile.explosive=desc.warhead.explosiveMass or desc.warhead.shapedExplosiveMass + + -- Set missile target name, unit and player. + self:GetMissileTarget(missile) + + self:I(FOX.lid..string.format("EVENT SHOT: Target name = %s, fuse dist=%s, explosive=%s", tostring(missile.targetName), tostring(missile.fuseDist), tostring(missile.explosive))) -- Only track if target was a player or target is protected. if missile.targetPlayer or self:_IsProtected(missile.targetUnit) then @@ -1137,6 +1305,36 @@ function FOX:OnEventShot(EventData) end +--- FOX event handler for event hit. +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventHit(EventData) + self:T({eventhit = EventData}) + + -- Nil checks. + if EventData.Weapon==nil then + return + end + if EventData.IniUnit==nil then + return + end + if EventData.TgtUnit==nil then + return + end + + local weapon=EventData.Weapon + local weaponname=weapon:getName() + + for i,_missile in pairs(self.missiles) do + local missile=_missile --#FOX.MissileData + if missile.missileName==weaponname then + self:I(self.lid..string.format("WARNING: Missile %s (%s) hit target %s. Missile trainer target was %s.", missile.missileType, missile.missileName, EventData.TgtUnitName, missile.targetName)) + self:I({missile=missile}) + return + end + end + +end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- RADIO MENU Functions diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 873b1b4b3..f32caa744 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -429,12 +429,12 @@ UTILS.tostringLL = function( lat, lon, acc, DMS) local secFrmtStr -- create the formatting string for the seconds place secFrmtStr = '%02d' --- if acc <= 0 then -- no decimal place. --- secFrmtStr = '%02d' --- else --- local width = 3 + acc -- 01.310 - that's a width of 6, for example. --- secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' --- end + if acc <= 0 then -- no decimal place. + secFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end return string.format('%03d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' .. string.format('%03d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi From 03042c8282d44016e9b0aa93cd02eb2a3fb40855 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 12 Jul 2019 09:41:13 +0200 Subject: [PATCH 318/485] AIRBOSS v1.0.4 One stack per flight group in CASE II/III --- Moose Development/Moose/Functional/Fox.lua | 2 +- Moose Development/Moose/Ops/Airboss.lua | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index e2ab614f3..91518375e 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -476,7 +476,7 @@ end --- Set missile-player distance when BIG missiles are destroyed. -- @param #FOX self -- @param #number distance Distance in meters. Default 400 m. --- @param #number explosivemass Explosive mass of missile in kg TNT. Default 50 kg. +-- @param #number explosivemass Explosive mass of missile threshold in kg TNT. Default 50 kg. -- @return #FOX self function FOX:SetExplosionDistanceBigMissiles(distance, explosivemass) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 74b3778a3..82a088c08 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1681,12 +1681,13 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.0.3" +AIRBOSS.version="1.0.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - + +-- TODO: Handle tanker and AWACS. Put them into pattern. -- TODO: Handle cases where AI crashes on carrier deck ==> Clean up deck. -- TODO: Player eject and crash debrief "gradings". -- TODO: PWO during case 2/3. @@ -6862,8 +6863,8 @@ function AIRBOSS:_GetFreeStack(ai, case, empty) local n=flight.flag if n>0 then - if flight.ai then - stack[n]=0 -- AI get one stack on their own. + if flight.ai or flight.case>1 then + stack[n]=0 -- AI get one stack on their own. Also CASE II/III get one stack each. else stack[n]=stack[n]-1 end @@ -6878,7 +6879,7 @@ function AIRBOSS:_GetFreeStack(ai, case, empty) local nfree=nil for i=1,nmaxstacks do self:T2(self.lid..string.format("FF Stack[%d]=%d", i, stack[i])) - if ai or empty then + if ai or empty or case>1 then -- AI need the whole stack. if stack[i]==self.NmaxStack then nfree=i From 5caab9c6f37a3335fc9af691220cf3ae15bf6e3a Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 12 Jul 2019 18:38:13 +0200 Subject: [PATCH 319/485] FOX - adjustment of destruction distance - tracking missiles with "unknown" target --- Moose Development/Moose/Functional/Fox.lua | 45 ++++++++++++---------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index 91518375e..99c838a06 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -41,7 +41,7 @@ -- @field Core.Set#SET_GROUP protectedset Set of protected groups. -- @field #number explosionpower Power of explostion when destroying the missile in kg TNT. Default 5 kg TNT. -- @field #number explosiondist Missile player distance in meters for destroying smaller missiles. Default 200 m. --- @field #number explosiondist2 Missile player distance in meters for destroying big missiles. Default 400 m. +-- @field #number explosiondist2 Missile player distance in meters for destroying big missiles. Default 500 m. -- @field #number bigmissilemass Explosion power of big missiles. Default 50 kg TNT. Big missiles will be destroyed earlier. -- @field #number dt50 Time step [sec] for missile position updates if distance to target > 50 km. Default 5 sec. -- @field #number dt10 Time step [sec] for missile position updates if distance to target > 10 km and < 50 km. Default 1 sec. @@ -138,9 +138,9 @@ FOX = { safezones = {}, launchzones = {}, protectedset = nil, - explosionpower = 0.5, + explosionpower = 0.1, explosiondist = 200, - explosiondist2 = 400, + explosiondist2 = 500, bigmissilemass = 50, destroy = nil, dt50 = 5, @@ -371,7 +371,9 @@ function FOX:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Shot) - --self:HandleEvent(EVENTS.Hit) + if self.Debug then + self:HandleEvent(EVENTS.Hit) + end if self.Debug then self:TraceClass(self.ClassName) @@ -396,7 +398,9 @@ function FOX:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.Shot) - --self:UnhandleEvent(EVENTS.Hit) + if self.Debug then + self:UnhandleEvent(EVENTS.Hit) + end end @@ -453,11 +457,11 @@ end --- Set explosion power. This is an "artificial" explosion generated when the missile is destroyed. Just for the visual effect. -- Don't set the explosion power too big or it will harm the aircraft in the vicinity. -- @param #FOX self --- @param #number power Explosion power in kg TNT. Default 0.5 kg. +-- @param #number power Explosion power in kg TNT. Default 0.1 kg. -- @return #FOX self function FOX:SetExplosionPower(power) - self.explosionpower=power or 0.5 + self.explosionpower=power or 0.1 return self end @@ -475,12 +479,12 @@ end --- Set missile-player distance when BIG missiles are destroyed. -- @param #FOX self --- @param #number distance Distance in meters. Default 400 m. +-- @param #number distance Distance in meters. Default 500 m. -- @param #number explosivemass Explosive mass of missile threshold in kg TNT. Default 50 kg. -- @return #FOX self function FOX:SetExplosionDistanceBigMissiles(distance, explosivemass) - self.explosiondist2=distance or 400 + self.explosiondist2=distance or 500 self.bigmissilemass=explosivemass or 50 @@ -875,7 +879,6 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) -- * Coalition check. But would not work in training situations where blue on blue is valid! -- * At least enable it for surface-to-air missiles. - --[[ local function _GetTarget(_unit) local unit=_unit --Wrapper.Unit#UNIT @@ -920,8 +923,8 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) if self.protectedset then - -- Distance to closest player. - local mindist=nil + -- Distance to closest protected unit. + mindist=nil for _,_group in pairs(self.protectedset:GetSet()) do local group=_group --Wrapper.Group#GROUP @@ -955,10 +958,8 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) end if target then - self:I(self.lid..string.format("Missile %s with NO explicit target got closest unit to missile as target %s.", missile.missileType, target:GetName())) + self:I(self.lid..string.format("Missile %s with NO explicit target got closest unit to missile as target %s. Dist=%s m", missile.missileType, target:GetName(), tostring(mindist))) end - - --]] end @@ -1001,8 +1002,10 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) missile.active=false -- Debug smoke. - missileCoord:SmokeRed() - targetCoord:SmokeGreen() + if self.Debug then + missileCoord:SmokeRed() + targetCoord:SmokeGreen() + end -- Create event. self:MissileDestroyed(missile) @@ -1284,14 +1287,16 @@ function FOX:OnEventShot(EventData) missile.shotCoord=EventData.IniUnit:GetCoordinate() missile.fuseDist=desc.fuseDist missile.explosive=desc.warhead.explosiveMass or desc.warhead.shapedExplosiveMass + missile.targetOrig=missile.targetName -- Set missile target name, unit and player. self:GetMissileTarget(missile) - self:I(FOX.lid..string.format("EVENT SHOT: Target name = %s, fuse dist=%s, explosive=%s", tostring(missile.targetName), tostring(missile.fuseDist), tostring(missile.explosive))) + self:I(FOX.lid..string.format("EVENT SHOT: Shooter=%s %s(%s) ==> Target=%s, fuse dist=%s, explosive=%s", + tostring(missile.shooterName), tostring(missile.missileType), tostring(missile.missileName), tostring(missile.targetName), tostring(missile.fuseDist), tostring(missile.explosive))) - -- Only track if target was a player or target is protected. - if missile.targetPlayer or self:_IsProtected(missile.targetUnit) then + -- Only track if target was a player or target is protected. Saw the 9M311 missiles have no target! + if missile.targetPlayer or self:_IsProtected(missile.targetUnit) or missile.targetName=="unknown" then -- Add missile table. table.insert(self.missiles, missile) From 7fdc049079a40f77a22ad0bd55c3bea46665a10f Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 12 Jul 2019 20:39:07 +0200 Subject: [PATCH 320/485] Update Fox.lua --- Moose Development/Moose/Functional/Fox.lua | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index 99c838a06..153c2c1b3 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -972,6 +972,12 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) -- Distance from missile to target. local distance=missileCoord:Get3DDistance(targetCoord) + -- Distance missile to shooter. + local distShooter=nil + if missile.shooterUnit and missile.shooterUnit:IsAlive() then + distShooter=missileCoord:Get3DDistance(missile.shooterUnit:GetCoordinate()) + end + -- Debug output. if self.Debug then @@ -1011,16 +1017,18 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) self:MissileDestroyed(missile) -- Little explosion for the visual effect. - if self.explosionpower>0 and distance>=50 then + if self.explosionpower>0 and distance>50 and (distShooter==nil or (distShooter and distShooter>50)) then missileCoord:Explosion(self.explosionpower) end - - -- Message to target. - local text=string.format("Destroying missile. %s", self:_DeadText()) - MESSAGE:New(text, 10):ToGroup(target:GetGroup()) - - -- Increase dead counter. + + -- Target was a player. if missile.targetPlayer then + + -- Message to target. + local text=string.format("Destroying missile. %s", self:_DeadText()) + MESSAGE:New(text, 10):ToGroup(target:GetGroup()) + + -- Increase dead counter. missile.targetPlayer.dead=missile.targetPlayer.dead+1 end From 345b0055f320b018c89fd92737d233f34935b5f1 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 12 Jul 2019 21:53:52 +0200 Subject: [PATCH 321/485] Update Fox.lua --- Moose Development/Moose/Functional/Fox.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index 153c2c1b3..47faf90bf 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -763,7 +763,9 @@ end function FOX:onafterMissileLaunch(From, Event, To, missile) -- Tracking info and init of last bomb position. - self:I(FOX.lid..string.format("FOX: Tracking %s - %s - target %s", missile.missileType, missile.missileName, tostring(missile.targetName))) + local text=string.format("FOX: Tracking missile %s(%s) - target %s - shooter %s", missile.missileType, missile.missileName, tostring(missile.targetName), missile.shooterName) + self:I(FOX.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) -- Loop over players. for _,_player in pairs(self.players) do From d4b9fc9e40bc7a9d8ae4fb6ae7f45e782cddd63e Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 21 Jul 2019 23:35:09 +0200 Subject: [PATCH 322/485] Updates --- Moose Development/Moose/Core/Spawn.lua | 98 ++++++++++-- Moose Development/Moose/Functional/Sead.lua | 45 +++++- Moose Development/Moose/Ops/Airboss.lua | 86 +++++++---- .../Moose/Ops/RecoveryTanker.lua | 146 ++++++++++++++++-- Moose Development/Moose/Ops/RescueHelo.lua | 87 ++++------- .../Moose/Utilities/Routines.lua | 5 + Moose Development/Moose/Wrapper/Airbase.lua | 31 +++- Moose Development/Moose/Wrapper/Unit.lua | 34 +++- 8 files changed, 407 insertions(+), 125 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index f9cd1b97b..867a4c3f8 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -319,7 +319,7 @@ function SPAWN:New( SpawnTemplatePrefix ) self.SpawnUnControlled = false self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. self.DelayOnOff = false -- No intial delay when spawning the first group. - self.Grouping = nil -- No grouping. + self.SpawnGrouping = nil -- No grouping. self.SpawnInitLivery = nil -- No special livery. self.SpawnInitSkill = nil -- No special skill. self.SpawnInitFreq = nil -- No special frequency. @@ -371,7 +371,7 @@ function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) self.SpawnUnControlled = false self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. self.DelayOnOff = false -- No intial delay when spawning the first group. - self.Grouping = nil -- No grouping. + self.SpawnGrouping = nil -- No grouping. self.SpawnInitLivery = nil -- No special livery. self.SpawnInitSkill = nil -- No special skill. self.SpawnInitFreq = nil -- No special frequency. @@ -549,7 +549,7 @@ end --- Sets the country of the spawn group. Note that the country determins the coalition of the group depending on which country is defined to be on which side for each specific mission! -- @param #SPAWN self --- @param #DCS.country Country Country id as number or enumerator: +-- @param #number Country Country id as number or enumerator: -- -- * @{DCS#country.id.RUSSIA} -- * @{DCS#county.id.USA} @@ -1438,7 +1438,7 @@ end -- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}. -- @param #boolean EmergencyAirSpawn (optional) If true (default), groups are spawned in air if there is no parking spot at the airbase. If false, nothing is spawned if no parking spot is available. -- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactily these spots! --- @return Wrapper.Group#GROUP that was spawned or nil when nothing was spawned. +-- @return Wrapper.Group#GROUP The group that was spawned or nil when nothing was spawned. -- @usage -- Spawn_Plane = SPAWN:New( "Plane" ) -- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Cold ) @@ -1498,11 +1498,7 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) local TemplateUnit=TemplateGroup:GetUnit(1) - --local ishelo=TemplateUnit:HasAttribute("Helicopters") - --local isbomber=TemplateUnit:HasAttribute("Bombers") - --local istransport=TemplateUnit:HasAttribute("Transports") - --local isfighter=TemplateUnit:HasAttribute("Battleplanes") - + -- General category of spawned group. local group=TemplateGroup local istransport=group:HasAttribute("Transports") and group:HasAttribute("Planes") local isawacs=group:HasAttribute("AWACS") @@ -1577,8 +1573,17 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT -- Set terminal type. local termtype=TerminalType - if spawnonrunway then - termtype=AIRBASE.TerminalType.Runway + if spawnonrunway then + if spawnonship then + -- Looks like there are no runway spawn spots on the stennis! + if ishelo then + termtype=AIRBASE.TerminalType.HelicopterUsable + else + termtype=AIRBASE.TerminalType.OpenMedOrBig + end + else + termtype=AIRBASE.TerminalType.Runway + end end -- Scan options. Might make that input somehow. @@ -1647,9 +1652,9 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT -- Get parking data. local parkingdata=SpawnAirbase:GetParkingSpotsTable(termtype) - self:T2(string.format("Parking at %s, terminal type %s:", SpawnAirbase:GetName(), tostring(termtype))) + self:T(string.format("Parking at %s, terminal type %s:", SpawnAirbase:GetName(), tostring(termtype))) for _,_spot in pairs(parkingdata) do - self:T2(string.format("%s, Termin Index = %3d, Term Type = %03d, Free = %5s, TOAC = %5s, Term ID0 = %3d, Dist2Rwy = %4d", + self:T(string.format("%s, Termin Index = %3d, Term Type = %03d, Free = %5s, TOAC = %5s, Term ID0 = %3d, Dist2Rwy = %4d", SpawnAirbase:GetName(), _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy)) end self:T(string.format("%s at %s: free parking spots = %d - number of units = %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), nfree, nunits)) @@ -1802,8 +1807,8 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT end -- Debug output. - self:T2(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) - self:T2(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) + self:T(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) + self:T(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) end end @@ -1840,6 +1845,66 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT return nil end +--- Spawn a group on an @{Wrapper.Airbase} at a specific parking spot. +-- @param #SPAWN self +-- @param Wrapper.Airbase#AIRBASE Airbase The @{Wrapper.Airbase} where to spawn the group. +-- @param #table Spots Table of parking spot IDs. Note that these in general are different from the numbering in the mission editor! +-- @param #SPAWN.Takeoff Takeoff (Optional) Takeoff type, i.e. either SPAWN.Takeoff.Cold or SPAWN.Takeoff.Hot. Default is Hot. +-- @return Wrapper.Group#GROUP The group that was spawned or nil when nothing was spawned. +function SPAWN:SpawnAtParkingSpot(Airbase, Spots, Takeoff) -- R2.5 + self:F({Airbase=Airbase, Spots=Spots, Takeoff=Takeoff}) + + -- Ensure that Spots parameter is a table. + if type(Spots)~="table" then + Spots={Spots} + end + + -- Get template group. + local group=GROUP:FindByName(self.SpawnTemplatePrefix) + + -- Get number of units in group. + local nunits=self.SpawnGrouping or #group:GetUnits() + + -- Quick check. + if nunits then + + -- Check that number of provided parking spots is large enough. + if #Spots=nunits then + return self:SpawnAtAirbase(Airbase, Takeoff, nil, nil, nil, Parkingdata) + else + self:E("ERROR: Could not find enough free parking spots!") + end + + + else + self:E("ERROR: Could not get number of units in group!") + end + + return nil +end --- Will park a group at an @{Wrapper.Airbase}. -- @@ -3023,6 +3088,9 @@ function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, Spa end --- Get the next index of the groups to be spawned. This method is complicated, as it is used at several spaces. +-- @param #SPAWN self +-- @param #number SpawnIndex Spawn index. +-- @return #number self.SpawnIndex function SPAWN:_GetSpawnIndex( SpawnIndex ) self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) diff --git a/Moose Development/Moose/Functional/Sead.lua b/Moose Development/Moose/Functional/Sead.lua index 55a791023..6edb99312 100644 --- a/Moose Development/Moose/Functional/Sead.lua +++ b/Moose Development/Moose/Functional/Sead.lua @@ -57,8 +57,10 @@ SEAD = { -- -- Defends the Russian SA installations from SEAD attacks. -- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) function SEAD:New( SEADGroupPrefixes ) + local self = BASE:Inherit( self, BASE:New() ) - self:F( SEADGroupPrefixes ) + self:F( SEADGroupPrefixes ) + if type( SEADGroupPrefixes ) == 'table' then for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix @@ -85,7 +87,29 @@ function SEAD:OnEventShot( EventData ) local SEADWeaponName = EventData.WeaponName -- return weapon type -- Start of the 2nd loop self:T( "Missile Launched = " .. SEADWeaponName ) - if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD + + --if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD + if SEADWeaponName == "weapons.missiles.X_58" --Kh-58U anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.Kh25MP_PRGS1VP" --Kh-25MP anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.X_25MP" --Kh-25MPU anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.X_28" --Kh-28 anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.X_31P" --Kh-31P anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.AGM_45A" --AGM-45A anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.AGM_45" --AGM-45B anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.AGM_88" --AGM-88C anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.AGM_122" --AGM-122 Sidearm anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.ALARM" --ALARM anti-radiation missiles fired + then + local _evade = math.random (1,100) -- random number for chance of evading action local _targetMim = EventData.Weapon:getTarget() -- Identify target local _targetMimname = Unit.getName(_targetMim) @@ -111,47 +135,62 @@ function SEAD:OnEventShot( EventData ) self:T( _targetskill ) if self.TargetSkill[_targetskill] then if (_evade > self.TargetSkill[_targetskill].Evade) then + self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) + local _targetMim = Weapon.getTarget(SEADWeapon) local _targetMimname = Unit.getName(_targetMim) local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) local _targetMimcont= _targetMimgroup:getController() + routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly + local SuppressedGroups1 = {} -- unit suppressed radar off for a random time + local function SuppressionEnd1(id) id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) SuppressedGroups1[id.groupName] = nil end + local id = { groupName = _targetMimgroup, ctrl = _targetMimcont } + local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) + if SuppressedGroups1[id.groupName] == nil then + SuppressedGroups1[id.groupName] = { SuppressionEndTime1 = timer.getTime() + delay1, SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function - } + } + Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) end local SuppressedGroups = {} + local function SuppressionEnd(id) id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) SuppressedGroups[id.groupName] = nil end + local id = { groupName = _targetMimgroup, ctrl = _targetMimcont } + local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) + if SuppressedGroups[id.groupName] == nil then SuppressedGroups[id.groupName] = { SuppressionEndTime = timer.getTime() + delay, SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function } + timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) end diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 82a088c08..da29434fe 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -205,7 +205,7 @@ -- @field Core.Set#SET_GROUP excludesetAI AI groups in this set will be explicitly excluded from handling by the airboss and not forced into the Marshal pattern. -- @field #boolean menusingle If true, menu is optimized for a single carrier. -- @field #number collisiondist Distance up to which collision checks are done. --- @field #nubmer holdtimestamp Timestamp when the carrier first came to an unexpected hold. +-- @field #number holdtimestamp Timestamp when the carrier first came to an unexpected hold. -- @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. -- @field #string soundfolderLSO Folder withing the mission (miz) file where LSO sound files are stored. @@ -1559,7 +1559,7 @@ AIRBOSS.Difficulty={ -- @field #number Time Time in seconds. -- @field #number Rho Distance in meters. -- @field #number X Distance in meters. --- @field #nubmer Z Distance in meters. +-- @field #number Z Distance in meters. -- @field #number AoA Angle of Attack. -- @field #number Alt Altitude in meters. -- @field #number GSE Glideslope error in degrees. @@ -1681,7 +1681,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.0.4" +AIRBOSS.version="1.0.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -2128,6 +2128,13 @@ function AIRBOSS:New(carriername, alias) -- @param #AIRBOSS self -- @param #number delay Delay in seconds. + --- On after "RecoveryStop" user function. Called when recovery of aircraft is stopped. + -- @function [parent=#AIRBOSS] OnAfterRecoveryStop + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + --- Triggers the FSM event "RecoveryPause" that pauses the recovery of aircraft. -- @function [parent=#AIRBOSS] RecoveryPause @@ -3063,6 +3070,15 @@ function AIRBOSS:SetRecoveryTanker(recoverytanker) return self end +--- Define an AWACS associated with the carrier. +-- @param #AIRBOSS self +-- @param Ops.RecoveryTanker#RECOVERYTANKER awacs AWACS (recovery tanker) object. +-- @return #AIRBOSS self +function AIRBOSS:SetAWACS(awacs) + self.awacs=awacs + return self +end + --- Set default player skill. New players will be initialized with this skill. -- -- * "Flight Student" = @{#AIRBOSS.Difficulty.Easy} @@ -5914,38 +5930,48 @@ 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 5 and has initial flag value. - if closein>UTILS.NMToMeters(5) and knownflight.flag==-100 and iscarriersquad then + -- Is this group the tanker? + local istanker=self.tanker and self.tanker.tanker:GetName()==groupname - -- Check that we do not add a recovery tanker for marshaling. - if self.tanker and self.tanker.tanker:GetName()==groupname then + -- Is this group the AWACS? + local isawacs=self.awacs and self.awacs.tanker:GetName()==groupname + + -- Send tanker to marshal stack? + local tanker2marshal = istanker and self.tanker:IsReturning() and self.tanker.airbase:GetName()==self.airbase:GetName() and knownflight.flag==-100 + + -- Send AWACS to marhsal stack? + local awacs2marshal = isawacs and self.awacs:IsReturning() and self.awacs.airbase:GetName()==self.airbase:GetName() and knownflight.flag==-100 + + -- Put flight into Marshal. + local putintomarshal=closein>UTILS.NMToMeters(5) and knownflight.flag==-100 and iscarriersquad and istanker==false and isawacs==false + + -- Send AI flight to marshal stack if group closes in more than 5 and has initial flag value. + if putintomarshal or tanker2marshal or awacs2marshal then + + -- Get the next free stack for current recovery case. + local stack=self:_GetFreeStack(knownflight.ai) + + -- Repawn. + local respawn=self.respawnAI --or tanker2marshal - -- Don't touch the recovery tanker! + if stack then + + -- Send AI to marshal stack. We respawn the group to clean possible departure and destination airbases. + self:_MarshalAI(knownflight, stack, respawn) else - -- Get the next free stack for current recovery case. - local stack=self:_GetFreeStack(knownflight.ai) - - if stack then - - -- Send AI to marshal stack. We respawn the group to clean possible departure and destination airbases. - self:_MarshalAI(knownflight, stack, self.respawnAI) - - 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, self.respawnAI) -- Group is respawned to clear any attached airfields. - end - + -- 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, respawn) -- Group is respawned to clear any attached airfields. end - - -- Break the loop to not have all flights at once! Spams the message screen. - break - - end -- Tanker - end -- Closed in + + end + + -- Break the loop to not have all flights at once! Spams the message screen. + break + + end -- Closed in or tanker/AWACS end -- AI else @@ -14329,7 +14355,7 @@ end -- @param #number delay Delay in seconds, before the message is broadcasted. -- @param #number interval Interval in seconds after the last sound has been played. -- @param #boolean click If true, play radio click at the end. --- @param #booelan pilotcall If true, it's a pilot call. +-- @param #boolean pilotcall If true, it's a pilot call. function AIRBOSS:RadioTransmission(radio, call, loud, delay, interval, click, pilotcall) self:F2({radio=radio, call=call, loud=loud, delay=delay, interval=interval, click=click}) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 3c4d781dc..a4ef844df 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -60,6 +60,8 @@ -- @field #number callsignname Number for the callsign name. -- @field #number callsignnumber Number of the callsign name. -- @field #string modex Tail number of the tanker. +-- @field #boolean eplrs If true, enable data link, e.g. if used as AWACS. +-- @field #boolean recovery If true, tanker will recover using the AIRBOSS marshal pattern. -- @extends Core.Fsm#FSM --- Recovery Tanker. @@ -295,15 +297,16 @@ RECOVERYTANKER = { callsignnumber = nil, modex = nil, eplrs = nil, + recovery = nil, } --- Unique ID (global). -- @field #number UID Unique ID (global). -RECOVERYTANKER.UID=0 +_RECOVERYTANKERID=0 --- Class version. -- @field #string version -RECOVERYTANKER.version="1.0.7" +RECOVERYTANKER.version="1.0.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -349,16 +352,16 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self.tankergroupname=tankergroupname -- Increase unique ID. - RECOVERYTANKER.UID=RECOVERYTANKER.UID+1 + _RECOVERYTANKERID=_RECOVERYTANKERID+1 -- Unique ID of this tanker. - self.uid=RECOVERYTANKER.UID + self.uid=_RECOVERYTANKERID -- 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, _RECOVERYTANKERID) -- Log ID. self.lid=string.format("RECOVERYTANKER %s | ", self.alias) @@ -377,6 +380,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetPatternUpdateHeading() self:SetPatternUpdateInterval() self:SetAWACS(false) + self:SetRecoveryAirboss(false) -- Debug trace. if false then @@ -399,6 +403,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) 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("Returning", "Returned", "Returned") -- Tanker has returned to its airbase (i.e. landed). self:AddTransition("*", "Status", "*") -- Status update. self:AddTransition("Running", "PatternUpdate", "*") -- Update pattern wrt to carrier. self:AddTransition("*", "Stop", "Stopped") -- Stop the FSM. @@ -472,6 +477,26 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + --- Triggers the FSM event "Returned" after the tanker has landed. + -- @function [parent=#RECOVERYTANKER] Returned + -- @param #RECOVERYTANKER self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed. + + --- Triggers the delayed FSM event "Returned" after the tanker has landed. + -- @function [parent=#RECOVERYTANKER] __Returned + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed. + + --- On after "Returned" event user function. Called when a the the tanker has landed at an airbase. + -- @function [parent=#RECOVERYTANKER] OnAfterReturned + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed. + + --- Triggers the FSM event "Status" that updates the tanker status. -- @function [parent=#RECOVERYTANKER] Status -- @param #RECOVERYTANKER self @@ -596,6 +621,19 @@ function RECOVERYTANKER:SetHomeBase(airbase) return self end +--- Activate recovery by the AIRBOSS class. Tanker will get a Marshal stack and perform a CASE I, II or III recovery when RTB. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true or nil, recovery is done by AIRBOSS. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRecoveryAirboss(switch) + if switch==true or switch==nil then + self.recovery=true + else + self.recovery=false + end + 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. @@ -830,6 +868,7 @@ function RECOVERYTANKER:onafterStart(From, Event, To) -- Handle events. self:HandleEvent(EVENTS.EngineShutdown) + self:HandleEvent(EVENTS.Land) 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) @@ -865,7 +904,7 @@ function RECOVERYTANKER:onafterStart(From, Event, To) else -- Check if an uncontrolled tanker group was requested. - if self.useuncontrolled then + if self.uncontrolledac then -- Use an uncontrolled aircraft group. self.tanker=GROUP:FindByName(self.tankergroupname) @@ -884,14 +923,14 @@ function RECOVERYTANKER:onafterStart(From, Event, To) else -- Spawn tanker at airbase. - self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, AIRBASE.TerminalType.OpenMedOrBig) end end -- Initialize route. self.distStern<0! - SCHEDULER:New(nil, self._InitRoute, {self, -self.distStern+UTILS.NMToMeters(3)}, 1) + self:ScheduleOnce(1, self._InitRoute, self, -self.distStern+UTILS.NMToMeters(3)) -- Create tanker beacon. if self.TACANon then @@ -929,7 +968,7 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) -- Get current time. local time=timer.getTime() - if self.tanker:IsAlive() then + if self.tanker and self.tanker:IsAlive() then --------------------- -- TANKER is ALIVE -- @@ -937,8 +976,14 @@ 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) + local life=self.tanker:GetUnit(1):GetLife() + local life0=self.tanker:GetUnit(1):GetLife0() + local lifeR=self.tanker:GetUnit(1):GetLifeRelative() + + -- Report fuel and life. + local text=string.format("Recovery tanker %s: state=%s fuel=%.1f, life=%.1f/%.1f=%d", self.tanker:GetName(), self:GetState(), fuel, life, life0, lifeR*100) self:T(self.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) -- Check if tanker is running and not RTBing or refueling. if self:IsRunning() then @@ -947,7 +992,7 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) if fuel0 then if (allowTOAC and allowTOAC==true) or _spot.TO_AC==false then local _coord=COORDINATE:NewFromVec3(_spot.vTerminalPos) table.insert(freespots, {Coordinate=_coord, TerminalID=_spot.Term_Index, TerminalType=_spot.Term_Type, TOAC=_spot.TO_AC, Free=true, TerminalID0=_spot.Term_Index_0, DistToRwy=_spot.fDistToRW}) @@ -589,6 +589,31 @@ function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC) return freespots end +--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase. +-- @param #AIRBASE self +-- @param #number TerminalID The terminal ID of the parking spot. +-- @return #AIRBASE.ParkingSpot Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". +function AIRBASE:GetParkingSpotData(TerminalID) + self:F({TerminalID=TerminalID}) + + -- Get parking data. + local parkingdata=self:GetParkingSpotsTable() + + -- Debug output. + self:T2({parkingdata=parkingdata}) + + for _,_spot in pairs(parkingdata) do + local spot=_spot --#AIRBASE.ParkingSpot + self:E({TerminalID=spot.TerminalID,TerminalType=spot.TerminalType}) + if TerminalID==spot.TerminalID then + return spot + end + end + + self:E("ERROR: Could not find spot with Terminal ID="..tostring(TerminalID)) + return nil +end + --- Place markers of parking spots on the F10 map. -- @param #AIRBASE self -- @param #AIRBASE.TerminalType termtype Terminal type for which marks should be placed. @@ -893,7 +918,7 @@ function AIRBASE:CheckOnRunWay(group, radius, despawn) end --- Get category of airbase. --- @param #WAREHOUSE self +-- @param #AIRBASE self -- @return #number Category of airbase from GetDesc().category. function AIRBASE:GetAirbaseCategory() return self:GetDesc().category diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 1ef55065d..6e28f3323 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -612,8 +612,7 @@ end --- Returns the unit's health. Dead units has health <= 1.0. -- @param #UNIT self --- @return #number The Unit's health value. --- @return #nil The DCS Unit is not existing or alive. +-- @return #number The Unit's health value or -1 if unit does not exist any more. function UNIT:GetLife() self:F2( self.UnitName ) @@ -629,8 +628,7 @@ end --- Returns the Unit's initial health. -- @param #UNIT self --- @return #number The Unit's initial health value. --- @return #nil The DCS Unit is not existing or alive. +-- @return #number The Unit's initial health value or 0 if unit does not exist any more. function UNIT:GetLife0() self:F2( self.UnitName ) @@ -644,6 +642,34 @@ function UNIT:GetLife0() return 0 end +--- Returns the unit's relative health. +-- @param #UNIT self +-- @return #number The Unit's relative health value, i.e. a number in [0,1] or -1 if unit does not exist any more. +function UNIT:GetLifeRelative() + self:F2(self.UnitName) + + if self and self:IsAlive() then + local life0=self:GetLife0() + local lifeN=self:GetLife() + return lifeN/life0 + end + + return -1 +end + +--- Returns the unit's relative damage, i.e. 1-life. +-- @param #UNIT self +-- @return #number The Unit's relative health value, i.e. a number in [0,1] or 1 if unit does not exist any more. +function UNIT:GetDamageRelative() + self:F2(self.UnitName) + + if self and self:IsAlive() then + return 1-self:GetLifeRelative() + end + + return 1 +end + --- Returns the category name of the #UNIT. -- @param #UNIT self -- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship From e17e6357103bd512e183c384f83fe47059f71708 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 21 Jul 2019 23:59:05 +0200 Subject: [PATCH 323/485] Updates --- Moose Development/Moose/Ops/RecoveryTanker.lua | 3 +-- Moose Development/Moose/Ops/RescueHelo.lua | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index a4ef844df..2007d3b60 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -1312,8 +1312,7 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) group:InitRadioModulation(self.RadioModu) group:InitModex(self.modex) - -- Respawn tanker. - -- Delaying respawn due to DCS bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + -- Respawn tanker. 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. diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 41751fb58..d033713b5 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -1178,7 +1178,7 @@ 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))) + self:I(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 From d264c6f6a5f78a72696e3744a9a635fa9668ab49 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 21 Jul 2019 23:59:35 +0200 Subject: [PATCH 324/485] Update RecoveryTanker.lua --- Moose Development/Moose/Ops/RecoveryTanker.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 2007d3b60..10e132e73 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -1361,8 +1361,7 @@ function RECOVERYTANKER:_RefuelingStart(EventData) self:T(self.lid..text) -- FMS state "Refueling". - self:RefuelStart(receiver) - + self:RefuelStart(receiver) end end From 96fe9d51d6c60af28214dc772ae9529d1e3d833e Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 22 Jul 2019 16:55:17 +0200 Subject: [PATCH 325/485] Typo bug fix --- Moose Development/Moose/Ops/RecoveryTanker.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index 10e132e73..bd2adab50 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -1267,7 +1267,7 @@ function RECOVERYTANKER:OnEventLand(EventData) local airbase=nil --Wrapper.Airbase#AIRBASE local airbasename="unknown" if EventData.Place then - airbase=EventData.Plase + airbase=EventData.Place airbasename=airbase:GetName() end From f931af2ee99296bf2d8277fbc5c8454829a05f09 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 22 Jul 2019 21:06:12 +0200 Subject: [PATCH 326/485] Updates --- Moose Development/Moose/Core/Radio.lua | 2 +- Moose Development/Moose/Ops/Airboss.lua | 37 +++++++++++-------- .../Moose/Ops/RecoveryTanker.lua | 29 +++++++++++++-- Moose Development/Moose/Wrapper/Group.lua | 3 +- 4 files changed, 50 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 412e18430..b6c13185c 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -517,7 +517,7 @@ end -- local myUnit = UNIT:FindByName("MyUnit") -- local myBeacon = myUnit:GetBeacon() -- Creates the beacon -- --- myBeacon:TACAN(20, "Y", "TEXACO", true) -- Activate the beacon +-- myBeacon:ActivateTACAN(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}) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index da29434fe..966afb94d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -8395,28 +8395,35 @@ function AIRBOSS:OnEventEngineShutdown(EventData) -- Debug message. self:T(self.lid..string.format("AI unit %s shut down its engines!", _unitName)) - if self.despawnshutdown then + -- Get flight. + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + + -- Only AI flights. + if flight and flight.ai then - -- Get flight. - local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) - - -- Only AI flights. - if flight and flight.ai then + -- Check if all elements were recovered. + local recovered=self:_CheckSectionRecovered(flight) - -- Check if all elements were recovered. - local recovered=self:_CheckSectionRecovered(flight) + -- Despawn group and completely remove flight. + if recovered then + self:T(self.lid..string.format("AI group %s completely recovered. Despawning group after engine shutdown event as requested in 5 seconds.", tostring(EventData.IniGroupName))) - -- Despawn group and completely remove flight. - if recovered then - self:T(self.lid..string.format("AI group %s completely recovered. Despawning group after engine shutdown event as requested in 5 seconds.", tostring(EventData.IniGroupName))) + -- Remove flight. + self:_RemoveFlight(flight) + + -- Check if this is a tanker or AWACS associated with the carrier. + local istanker=self.tanker and self.tanker.tanker:GetName()==EventData.IniGroupName + local isawacs=self.awacs and self.awacs.tanker:GetName()==EventData.IniGroupName + + -- Destroy group if desired. Recovery tankers have their own logic for despawning. + if self.despawnshutdown and not (istanker or isawacs) then EventData.IniGroup:Destroy(nil, 5) - self:_RemoveFlight(flight) end + end + end - - end - + end end --- Airboss event handler for event that a unit takes off. diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index bd2adab50..e77f91ad2 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -813,6 +813,13 @@ function RECOVERYTANKER:IsReturning() return self:is("Returning") end +--- Check if tanker has returned to base. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker has returned to base. +function RECOVERYTANKER:IsReturned() + return self:is("Returned") +end + --- Check if tanker is currently operating. -- @param #RECOVERYTANKER self -- @return #boolean If true, tanker is operating. @@ -1058,6 +1065,16 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) end end + elseif self:IsReturning() then + + -- Tanker is returning to its base. + self:T2(self.lid.."Tanker is returning.") + + elseif self:IsReturned() then + + -- Tanker landed. Waiting for engine shutdown... + self:T2(self.lid.."Tanker returned. waiting for engine shutdown.") + end -- Call status again in 30 seconds. @@ -1084,6 +1101,7 @@ function RECOVERYTANKER:onafterStatus(From, Event, To) end end + end end @@ -1313,7 +1331,8 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) group:InitModex(self.modex) -- Respawn tanker. Delaying respawn due to DCS bug https://github.com/FlightControl-Master/MOOSE/issues/1076 - SCHEDULER:New(nil , group.RespawnAtCurrentAirbase, {group}, 1) + --SCHEDULER:New(nil , group.RespawnAtCurrentAirbase, {group}, 1) + self:ScheduleOnce(1, GROUP.RespawnAtCurrentAirbase, group) -- Create tanker beacon and activate TACAN. if self.TACANon then @@ -1331,7 +1350,8 @@ function RECOVERYTANKER:OnEventEngineShutdown(EventData) end -- Initial route. - SCHEDULER:New(nil, self._InitRoute, {self, -self.distStern+UTILS.NMToMeters(3)}, 2) + --SCHEDULER:New(nil, self._InitRoute, {self, -self.distStern+UTILS.NMToMeters(3)}, 2) + self:ScheduleOnce(2, RECOVERYTANKER._InitRoute, self, -self.distStern+UTILS.NMToMeters(3)) end end @@ -1593,7 +1613,8 @@ 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) + self:ScheduleOnce(delay, RECOVERYTANKER._ActivateTACAN, self) else @@ -1604,7 +1625,7 @@ function RECOVERYTANKER:_ActivateTACAN(delay) if unit and unit:IsAlive() then -- Debug message. - local text=string.format("Activating recovery tanker TACAN beacon: channel=%d mode=%s, morse=%s.", self.TACANchannel, self.TACANmode, self.TACANmorse) + local text=string.format("Activating 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) diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 5d1ff66c5..a6f372e1a 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -349,7 +349,8 @@ function GROUP:Destroy( GenerateEvent, delay ) self:F2( self.GroupName ) if delay and delay>0 then - SCHEDULER:New(nil, GROUP.Destroy, {self, GenerateEvent}, delay) + --SCHEDULER:New(nil, GROUP.Destroy, {self, GenerateEvent}, delay) + self:ScheduleOnce(delay, GROUP.Destroy, self, GenerateEvent) else local DCSGroup = self:GetDCSObject() From 20cf69c182683602fe6e7262d8afe557cf737138 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 22 Jul 2019 21:20:05 +0200 Subject: [PATCH 327/485] Update Airboss.lua --- Moose Development/Moose/Ops/Airboss.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 966afb94d..9f4c8494c 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -5937,10 +5937,10 @@ function AIRBOSS:_ScanCarrierZone() local isawacs=self.awacs and self.awacs.tanker:GetName()==groupname -- Send tanker to marshal stack? - local tanker2marshal = istanker and self.tanker:IsReturning() and self.tanker.airbase:GetName()==self.airbase:GetName() and knownflight.flag==-100 + local tanker2marshal = istanker and self.tanker:IsReturning() and self.tanker.airbase:GetName()==self.airbase:GetName() and knownflight.flag==-100 and self.tanker.recovery==true -- Send AWACS to marhsal stack? - local awacs2marshal = isawacs and self.awacs:IsReturning() and self.awacs.airbase:GetName()==self.airbase:GetName() and knownflight.flag==-100 + local awacs2marshal = isawacs and self.awacs:IsReturning() and self.awacs.airbase:GetName()==self.airbase:GetName() and knownflight.flag==-100 and self.awacs.recovery==true -- Put flight into Marshal. local putintomarshal=closein>UTILS.NMToMeters(5) and knownflight.flag==-100 and iscarriersquad and istanker==false and isawacs==false From d08d8db29868ed2ef09154bbe1e4e38d2b554783 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 28 Jul 2019 22:43:10 +0200 Subject: [PATCH 328/485] AIRBOSS v1.0.6 - Added Marshal radial to skipper menu. - Adjusted grading and groove time start for LUL issue. --- Moose Development/Moose/Ops/Airboss.lua | 96 +++++++++++++++++++++---- 1 file changed, 82 insertions(+), 14 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 9f4c8494c..04f531bd1 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -231,6 +231,7 @@ -- @field #number skipperSpeed Speed in knots for manual recovery start. -- @field #number skipperCase Manual recovery case. -- @field #boolean skipperUturn U-turn on/off via menu. +-- @field #number skipperOffset Holding offset angle in degrees for Case II/III manual recoveries. -- @field #number skipperTime Recovery time in min for manual recovery. -- @extends Core.Fsm#FSM @@ -1234,6 +1235,7 @@ AIRBOSS = { skipperMenu = nil, skipperSpeed = nil, skipperTime = nil, + skipperOffset = nil, skipperUturn = nil, } @@ -1681,7 +1683,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.0.5" +AIRBOSS.version="1.0.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1951,6 +1953,7 @@ function AIRBOSS:New(carriername, alias) local case=2 self.holdingoffset=30 self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red, 5) + self:_GetZoneLineup():SmokeZone(SMOKECOLOR.Green, 5) self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) @@ -2340,12 +2343,15 @@ end -- @param #number duration Default duration of the recovery in minutes. Default 30 min. -- @param #number windondeck Default wind on deck in knots. Default 25 knots. -- @param #boolean uturn U-turn after recovery window closes on=true or off=false/nil. Default off. +-- @param #number offset Relative Marshal radial in degrees for Case II/III recoveries. Default 30°. -- @return #AIRBOSS self -function AIRBOSS:SetMenuRecovery(duration, windondeck, uturn) +function AIRBOSS:SetMenuRecovery(duration, windondeck, uturn, offset) self.skipperMenu=true self.skipperTime=duration or 30 self.skipperSpeed=windondeck or 25 + self.skipperOffset=offset or 30 + if uturn then self.skipperUturn=true else @@ -9534,7 +9540,7 @@ function AIRBOSS:_Final(playerData, nocheck) local inzone=playerData.unit:IsInZone(zone) -- Check. - if inzone then + if inzone then --and math.abs(groovedata.Roll)<5 then -- Hint for player about altitude, AoA etc. Sound is off. self:_PlayerHint(playerData, nil, true) @@ -9593,12 +9599,11 @@ function AIRBOSS:_Groove(playerData) local glideslopeError=groovedata.GSE local AoA=groovedata.AoA - -- Start time in groove when "wings are level", i.e. <= 5°. - if playerData.TIG0==nil and math.abs(groovedata.Roll)<=5.0 then - playerData.TIG0=timer.getTime() - end - if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX then + if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX and (math.abs(groovedata.Roll)<=4.0 or playerData.unit:IsInZone(self:_GetZoneLineup())) then + + -- Start time in groove + playerData.TIG0=timer.getTime() -- LSO "Call the ball" call. self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true) @@ -9806,7 +9811,7 @@ function AIRBOSS:_Groove(playerData) -- Wait until player passed the 0.75 NM distance. local _advice=true - if rho>RXX and playerData.difficulty~=AIRBOSS.Difficulty.EASY then + if playerData.TIG0==nil and playerData.difficulty~=AIRBOSS.Difficulty.EASY then --rho>RXX _advice=false end @@ -10317,15 +10322,45 @@ function AIRBOSS:_GetZoneInitial(case) return zone end +--- Get lineup groove zone. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON_BASE Lineup zone. +function AIRBOSS:_GetZoneLineup() + + -- Get radial, i.e. inverse of BRC. + local fbi=self:GetRadial(1, false, false) + + -- Stern coordinate. + local st=self:_GetOptLandingCoordinate() + + -- Zone points. + local c1=st + local c2=st:Translate(UTILS.NMToMeters(0.50), fbi+15) + local c3=st:Translate(UTILS.NMToMeters(0.50), fbi+self.lue._max-0.05) + local c4=st:Translate(UTILS.NMToMeters(0.77), fbi+self.lue._max-0.05) + local c5=c4:Translate(UTILS.NMToMeters(0.25), fbi-90) + + -- Vec2 array. + local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()} + + -- Polygon zone. + local zone=ZONE_POLYGON_BASE:New("Zone Lineup", vec2) + + return zone +end + + --- Get groove zone. -- @param #AIRBOSS self -- @param #number l Length of the groove in NM. Default 1.5 NM. -- @param #number w Width of the groove in NM. Default 0.25 NM. --- @return Core.Zone#ZONE_POLYGON_BASE Initial zone. -function AIRBOSS:_GetZoneGroove(l, w) +-- @param #number b Width of the beginning in NM. Default 0.10 NM. +-- @return Core.Zone#ZONE_POLYGON_BASE Groove zone. +function AIRBOSS:_GetZoneGroove(l, w, b) l=l or 1.50 w=w or 0.25 + b=b or 0.10 -- Get radial, i.e. inverse of BRC. local fbi=self:GetRadial(1, false, false) @@ -10336,9 +10371,9 @@ function AIRBOSS:_GetZoneGroove(l, w) -- Zone points. local c1=st:Translate(self.carrierparam.totwidthstarboard, fbi-90) local c2=st:Translate(UTILS.NMToMeters(0.10), fbi-90):Translate(UTILS.NMToMeters(0.3), fbi) - local c3=st:Translate(UTILS.NMToMeters(w/2), fbi-90):Translate(UTILS.NMToMeters(l), fbi) + local c3=st:Translate(UTILS.NMToMeters(0.25), fbi-90):Translate(UTILS.NMToMeters(l), fbi) local c4=st:Translate(UTILS.NMToMeters(w/2), fbi+90):Translate(UTILS.NMToMeters(l), fbi) - local c5=st:Translate(UTILS.NMToMeters(0.10), fbi+90):Translate(UTILS.NMToMeters(0.3), fbi) + local c5=st:Translate(UTILS.NMToMeters(b), fbi+90):Translate(UTILS.NMToMeters(0.3), fbi) local c6=st:Translate(self.carrierparam.totwidthport, fbi+90) -- Vec2 array. @@ -15505,6 +15540,12 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(gid, "45 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 45) missionCommands.addCommandForGroup(gid, "60 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 60) missionCommands.addCommandForGroup(gid, "90 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 90) + local _menusetrtime=missionCommands.addSubMenuForGroup(gid, "Set Marshal Radial", _skipperPath) + missionCommands.addCommandForGroup(gid, "+30°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 30) + missionCommands.addCommandForGroup(gid, "+15°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 15) + missionCommands.addCommandForGroup(gid, "0°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 0) + missionCommands.addCommandForGroup(gid, "-15°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, -15) + missionCommands.addCommandForGroup(gid, "-30°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, -30) missionCommands.addCommandForGroup(gid, "U-turn On/Off", _skipperPath, self._SkipperRecoveryUturn, self, _unitName) missionCommands.addCommandForGroup(gid, "Start CASE I", _skipperPath, self._SkipperStartRecovery, self, _unitName, 1) missionCommands.addCommandForGroup(gid, "Start CASE II", _skipperPath, self._SkipperStartRecovery, self, _unitName, 2) @@ -15560,6 +15601,9 @@ function AIRBOSS:_SkipperStartRecovery(_unitName, case) -- Inform player. local text=string.format("affirm, Case %d recovery will start in 5 min for %d min. Wind on deck %d knots. U-turn=%s.", case, self.skipperTime, self.skipperSpeed, tostring(self.skipperUturn)) + if case>1 then + text=text..string.format(" Marshal radial %d°.", self.skipperOffset) + end if self:IsRecovering() then text="negative, carrier is already recovering." self:MessageToPlayer(playerData, text, "AIRBOSS") @@ -15574,7 +15618,7 @@ function AIRBOSS:_SkipperStartRecovery(_unitName, case) local C9=UTILS.SecondsToClock(t9) -- Carrier will turn into the wind. Wind on deck 25 knots. U-turn on. - self:AddRecoveryWindow(C0, C9, case, 30, true, self.skipperSpeed, self.skipperUturn) + self:AddRecoveryWindow(C0, C9, case, self.skipperOffset, true, self.skipperSpeed, self.skipperUturn) end end @@ -15608,6 +15652,30 @@ function AIRBOSS:_SkipperStopRecovery(_unitName) end end +--- Skipper set recovery offset angle. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +-- @param #number offset Recovery holding offset angle in degrees for Case II/III. +function AIRBOSS:_SkipperRecoveryOffset(_unitName, offset) + + -- 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 + + -- Inform player. + local text=string.format("roger, relative CASE II/III Marshal radial set to %d°.", offset) + self:MessageToPlayer(playerData, text, "AIRBOSS") + + self.skipperOffset=offset + end + end +end + --- Skipper set recovery time. -- @param #AIRBOSS self -- @param #string _unitName Name fo the player unit. From 59447999f09660a00df01cf4b2ad107baad4c8c4 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 1 Aug 2019 18:29:57 +0300 Subject: [PATCH 329/485] Updates --- Moose Development/Moose/AI/AI_Escort.lua | 25 +++++- .../Moose/AI/AI_Escort_Dispatcher.lua | 77 +++++++++++++------ .../Moose/AI/AI_Escort_Dispatcher_Request.lua | 30 ++------ 3 files changed, 82 insertions(+), 50 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 1e2807e5a..d64732c8e 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -351,9 +351,6 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) self.Detection:__Start( 30 ) - self:HandleEvent( EVENTS.Dead, OnEventDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, OnEventDeadOrCrash ) - self.MainMenu = MENU_GROUP:New( self.PlayerGroup, self.EscortName ) self.FlightMenu = MENU_GROUP:New( self.PlayerGroup, "Flight", self.MainMenu ) @@ -373,6 +370,27 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) end +--- @param #AI_ESCORT self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT:onafterStop( EscortGroupSet ) + + self:F() + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + EscortGroup:WayPointInitialize() + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEOpenFire() + end + ) + + self.Detection:Stop() + + self.MainMenu:Remove() + +end --- Set a Detection method for the EscortUnit to be reported upon. -- Detection methods are based on the derived classes from DETECTION_BASE. -- @param #AI_ESCORT self @@ -707,6 +725,7 @@ function AI_ESCORT:SetEscortMenuJoinUp( EscortGroup ) end + --- Defines --- Defines a menu slot to let the escort to join formation. -- @param #AI_ESCORT self -- @return #AI_ESCORT diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua index a4000ccf8..8615573fb 100644 --- a/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua @@ -1,21 +1,8 @@ ---- **AI** -- (R2.4) - Models the assignment of AI escorts to player flights. +--- **AI** -- (R2.5) - Models the automatic assignment of AI escorts to player flights. -- -- ## Features: -- -- --- * Provides the facilities to trigger escorts when players join flight units. --- * Provide different means how escorts can be triggered: --- * Directly when a player joins a plane. --- * Through the menu. --- --- === --- --- ## Test Missions: --- --- Test missions can be located on the main GITHUB site. --- --- [FlightControl-Master/MOOSE_MISSIONS] --- --- === +-- * Provides the facilities to trigger escorts when players join flight slots. -- -- === -- @@ -28,14 +15,10 @@ --- @type AI_ESCORT_DISPATCHER --- @field Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. --- @field Core.Set#SET_GROUP EscortGroupSet The set of group AI escorting the EscortUnit. --- @field #string EscortName Name of the escort. --- @field #string EscortBriefing A text showing the AI_ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. -- @extends Core.Fsm#FSM ---- A dynamic cargo handling capability for AI groups. +--- Models the automatic assignment of AI escorts to player flights. -- -- === -- @@ -50,12 +33,34 @@ AI_ESCORT_DISPATCHER.AI_Escorts = {} --- Creates a new AI_ESCORT_DISPATCHER object. -- @param #AI_ESCORT_DISPATCHER self --- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. +-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers for which escorts are spawned in. -- @param Core.Spawn#SPAWN EscortSpawn The spawn object that will spawn in the Escorts. -- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where the escorts are spawned. --- @param #string EscortName Name of the escort. --- @param #string EscortBriefing A text showing the AI_ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @param #string EscortName Name of the escort, which will also be the name of the escort menu. +-- @param #string EscortBriefing A text showing the briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. -- @return #AI_ESCORT_DISPATCHER +-- @usage +-- +-- -- Create a new escort when a player joins an SU-25T plane. +-- Create a carrier set, which contains the player slots that can be joined by the players, for which escorts will be defined. +-- local Red_SU25T_CarrierSet = SET_GROUP:New():FilterPrefixes( "Red A2G Player Su-25T" ):FilterStart() +-- +-- -- Create a spawn object that will spawn in the escorts, once the player has joined the player slot. +-- local Red_SU25T_EscortSpawn = SPAWN:NewWithAlias( "Red A2G Su-25 Escort", "Red AI A2G SU-25 Escort" ):InitLimit( 10, 10 ) +-- +-- -- Create an airbase object, where the escorts will be spawned. +-- local Red_SU25T_Airbase = AIRBASE:FindByName( AIRBASE.Caucasus.Maykop_Khanskaya ) +-- +-- -- Park the airplanes at the airbase, visible before start. +-- Red_SU25T_EscortSpawn:ParkAtAirbase( Red_SU25T_Airbase, AIRBASE.TerminalType.OpenMedOrBig ) +-- +-- -- New create the escort dispatcher, using the carrier set, the escort spawn object at the escort airbase. +-- -- Provide a name of the escort, which will be also the name appearing on the radio menu for the group. +-- -- And a briefing to appear when the player joins the player slot. +-- Red_SU25T_EscortDispatcher = AI_ESCORT_DISPATCHER:New( Red_SU25T_CarrierSet, Red_SU25T_EscortSpawn, Red_SU25T_Airbase, "Escort Su-25", "You Su-25T is escorted by one Su-25. Use the radio menu to control the escorts." ) +-- +-- -- The dispatcher needs to be started using the :Start() method. +-- Red_SU25T_EscortDispatcher:Start() function AI_ESCORT_DISPATCHER:New( CarrierSet, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) local self = BASE:Inherit( self, FSM:New() ) -- #AI_ESCORT_DISPATCHER @@ -85,10 +90,34 @@ function AI_ESCORT_DISPATCHER:onafterStart( From, Event, To ) self:HandleEvent( EVENTS.Birth ) - self:HandleEvent( EVENTS.PlayerLeaveUnit ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventExit ) + self:HandleEvent( EVENTS.Crash, self.OnEventExit ) + self:HandleEvent( EVENTS.Dead, self.OnEventExit ) end +--- @param #AI_ESCORT_DISPATCHER self +-- @param Core.Event#EVENTDATA EventData +function AI_ESCORT_DISPATCHER:OnEventExit( EventData ) + + local PlayerGroupName = EventData.IniGroupName + local PlayerGroup = EventData.IniGroup + local PlayerUnit = EventData.IniUnit + + self:I({EscortAirbase= self.EscortAirbase } ) + self:I({PlayerGroupName = PlayerGroupName } ) + self:I({PlayerGroup = PlayerGroup}) + self:I({FirstGroup = self.CarrierSet:GetFirst()}) + self:I({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) + + if self.CarrierSet:FindGroup( PlayerGroupName ) then + if self.AI_Escorts[PlayerGroupName] then + self.AI_Escorts[PlayerGroupName]:Stop() + self.AI_Escorts[PlayerGroupName] = nil + end + end + +end --- @param #AI_ESCORT_DISPATCHER self -- @param Core.Event#EVENTDATA EventData diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua index 05f503fe7..546f2baf9 100644 --- a/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua @@ -1,21 +1,9 @@ ---- **AI** -- (R2.4) - Models the assignment of AI escorts to player flights upon request using the radio menu. +--- **AI** -- (R2.5) - Models the assignment of AI escorts to player flights upon request using the radio menu. -- -- ## Features: --- -- +-- -- * Provides the facilities to trigger escorts when players join flight units. --- * Provide different means how escorts can be triggered: --- * Directly when a player joins a plane. --- * Through the menu. --- --- === --- --- ## Test Missions: --- --- Test missions can be located on the main GITHUB site. --- --- [FlightControl-Master/MOOSE_MISSIONS] --- --- === +-- * Provide a menu for which escorts can be requested. -- -- === -- @@ -28,14 +16,10 @@ --- @type AI_ESCORT_DISPATCHER_REQUEST --- @field Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. --- @field Core.Set#SET_GROUP EscortGroupSet The set of group AI escorting the EscortUnit. --- @field #string EscortName Name of the escort. --- @field #string EscortBriefing A text showing the AI_ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. -- @extends Core.Fsm#FSM ---- A dynamic cargo handling capability for AI groups. +--- Models the assignment of AI escorts to player flights upon request using the radio menu. -- -- === -- @@ -50,11 +34,11 @@ AI_ESCORT_DISPATCHER_REQUEST.AI_Escorts = {} --- Creates a new AI_ESCORT_DISPATCHER_REQUEST object. -- @param #AI_ESCORT_DISPATCHER_REQUEST self --- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. +-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers for which escorts are requested. -- @param Core.Spawn#SPAWN EscortSpawn The spawn object that will spawn in the Escorts. -- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where the escorts are spawned. --- @param #string EscortName Name of the escort. --- @param #string EscortBriefing A text showing the AI_ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @param #string EscortName Name of the escort, which will also be the name of the escort menu. +-- @param #string EscortBriefing A text showing the briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. -- @return #AI_ESCORT_DISPATCHER_REQUEST function AI_ESCORT_DISPATCHER_REQUEST:New( CarrierSet, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) From 115743263b872f8692fa9a1971a501210f66e076 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 1 Aug 2019 21:16:01 +0300 Subject: [PATCH 330/485] Fixed nasty bug where the same plane would be chosen all the time for request spawns. Root cause was the inherit. self variables were also copied during inherit of the chile object, resulting in a bad bad bad behaviour. --- Moose Development/Moose/AI/AI_Escort.lua | 1 + .../Moose/AI/AI_Escort_Dispatcher_Request.lua | 27 ++++++++++++++++- .../Moose/AI/AI_Escort_Request.lua | 29 +++++++++++++++++-- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index d64732c8e..dc9e699b4 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -391,6 +391,7 @@ function AI_ESCORT:onafterStop( EscortGroupSet ) self.MainMenu:Remove() end + --- Set a Detection method for the EscortUnit to be reported upon. -- Detection methods are based on the derived classes from DETECTION_BASE. -- @param #AI_ESCORT self diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua index 546f2baf9..0bf3d1a6b 100644 --- a/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua @@ -69,10 +69,35 @@ function AI_ESCORT_DISPATCHER_REQUEST:onafterStart( From, Event, To ) self:HandleEvent( EVENTS.Birth ) - self:HandleEvent( EVENTS.PlayerLeaveUnit ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventExit ) + self:HandleEvent( EVENTS.Crash, self.OnEventExit ) + self:HandleEvent( EVENTS.Dead, self.OnEventExit ) end +--- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param Core.Event#EVENTDATA EventData +function AI_ESCORT_DISPATCHER_REQUEST:OnEventExit( EventData ) + + local PlayerGroupName = EventData.IniGroupName + local PlayerGroup = EventData.IniGroup + local PlayerUnit = EventData.IniUnit + + self.CarrierSet:Flush(self) + self:I({EscortAirbase= self.EscortAirbase } ) + self:I({PlayerGroupName = PlayerGroupName } ) + self:I({PlayerGroup = PlayerGroup}) + self:I({FirstGroup = self.CarrierSet:GetFirst()}) + self:I({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) + + if self.CarrierSet:FindGroup( PlayerGroupName ) then + if self.AI_Escorts[PlayerGroupName] then + self.AI_Escorts[PlayerGroupName]:Stop() + self.AI_Escorts[PlayerGroupName] = nil + end + end + +end --- @param #AI_ESCORT_DISPATCHER_REQUEST self -- @param Core.Event#EVENTDATA EventData diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index 5eb274278..0994a143c 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -206,12 +206,13 @@ AI_ESCORT_REQUEST = { -- Escort:__Start( 5 ) function AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) - self.EscortGroupSet = SET_GROUP:New():FilterDeads():FilterCrashes() + local EscortGroupSet = SET_GROUP:New():FilterDeads():FilterCrashes() + local self = BASE:Inherit( self, AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT_REQUEST + + self.EscortGroupSet = EscortGroupSet self.EscortSpawn = EscortSpawn self.EscortAirbase = EscortAirbase - local self = BASE:Inherit( self, AI_ESCORT:New( EscortUnit, self.EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT_REQUEST - self.LeaderGroup = self.PlayerUnit:GetGroup() self.Detection = DETECTION_AREAS:New( self.EscortGroupSet, 5000 ) @@ -284,6 +285,28 @@ function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) end +--- @param #AI_ESCORT_REQUEST self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT_REQUEST:onafterStop( EscortGroupSet ) + + self:F() + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + EscortGroup:WayPointInitialize() + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEOpenFire() + end + ) + + self.Detection:Stop() + + self.MainMenu:Remove() + +end + --- Set the spawn mode to be mission execution. -- @param #AI_ESCORT_REQUEST self function AI_ESCORT_REQUEST:SetEscortSpawnMission() From 5a97f6877868a7c48fc369a30b353e767f8e8251 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 2 Aug 2019 21:10:56 +0300 Subject: [PATCH 331/485] Escort menus are deleted once the escort crashes. --- Moose Development/Moose/AI/AI_Escort.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index dc9e699b4..815f8dc21 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -364,6 +364,17 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) self:_InitEscortRoute( EscortGroup ) self:SetFlightModeFormation( EscortGroup ) + + --- @param #AI_ESCORT self + -- @param Core.Event#EVENTDATA EventData + function EscortGroup:OnEventDeadOrCrash( EventData ) + self:F( { "EventDead", EventData } ) + self.EscortMenu:Remove() + end + + EscortGroup:HandleEvent( EVENTS.Dead, EscortGroup.OnEventDeadOrCrash ) + EscortGroup:HandleEvent( EVENTS.Crash, EscortGroup.OnEventDeadOrCrash ) + end ) From debf784d0c99affcc956d1fb252bf89005743401 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 5 Aug 2019 11:15:45 +0200 Subject: [PATCH 332/485] SWAPR v0.0.1 --- Moose Development/Moose/Functional/SWAPR.lua | 614 +++++++++++++++++++ Moose Development/Moose/Modules.lua | 1 + Moose Development/Moose/Wrapper/Static.lua | 1 + Moose Setup/Moose.files | 1 + 4 files changed, 617 insertions(+) create mode 100644 Moose Development/Moose/Functional/SWAPR.lua diff --git a/Moose Development/Moose/Functional/SWAPR.lua b/Moose Development/Moose/Functional/SWAPR.lua new file mode 100644 index 000000000..34a3094a4 --- /dev/null +++ b/Moose Development/Moose/Functional/SWAPR.lua @@ -0,0 +1,614 @@ +--- **Functional** - (R2.5) - Replace client aircraft by statics until a player enters. +-- +-- Make the DCS world a bit more lively! +-- +-- **Main Features:** +-- +-- * Easy! +-- +-- ## Known (DCS) Issues +-- +-- * Does not support clients on ships. +-- * Does not support Harriers and helicopters on parking spots below a shelter. +-- +-- === +-- +-- ### Author: **Hardcard** (aka Goreuncle on the MOOSE discord) +-- ### Contributions: funkyfranky +-- +-- @module Functional.Swapr +-- @image Functional_SWAPR.png + +--- SWAPR class. +-- @type SWAPR +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode on/off. +-- @field #string lid Log debug id text. +-- @field Core.Set#SET_CLIENT clientset Set of clients to be replaced. +-- @field #table statics Table of static objects. +-- @field #table statictemplate Table of static template +-- @extends Core.Fsm#FSM + +--- Swap clients and statics +-- +-- === +-- +-- ![Banner Image](..\Presentations\SWAPR\SWAPR_Main.png) +-- +-- # SWAPR Concept +-- +-- SWAPR will enable you to easily spawn static aircraft on client slots. When a player enters a client slot, the static object is removed and the player aircraft spawned. +-- This makes the airbases look a lot more alive. +-- +-- # Simple Script +-- +-- The basic script is very simple and consists of only two lines: +-- +-- local clientset=SET_CLIENT:New():FilterActive(false):FilterOnce() +-- swapr=SWAPR:New(clientset) +-- +-- The first line defines a set of clients (here all) that will be replaced by statics. +-- The second lines initiates the SWAPR script. That's all. +-- +-- **Note** that Harrier and helicopter clients are automatically removed from the client set if they are placed on a sheltered parking spot. Otherwise the statics would be spawned +-- on top of the shelter roof. +-- +-- Similarly, clients on ships are removed as these would be spawned at sea level and not on the ship itself. +-- +-- All these are *DCS side restriction* when spawning statics. +-- +-- # 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 @{#SWAPR} class should have the string "SWAPR" 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("SWAPR") +-- +-- 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 @{#SWAPR.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 #SWAPR +SWAPR = { + ClassName = "SWAPR", + Debug = false, + lid = nil, + clientset = nil, + statics = {}, + statictemplate = {}, +} + +--- Class version. +-- @field #string version +SWAPR.version="0.0.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Check for clients on ships ==> get airdrome id from first route point. +-- TODO: Check that harrier and helo clients are not spawned in shelters ==> get parking spot type for these units in _Prepare() +-- TODO: Check what happens if statics are destroyed. +-- TODO: Check what happens if clients eject, crash or are shot down. +-- TODO: Check that parking spot is not blocked by other aircraft or statics when spawning a static replacement. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new SWAPR object. +-- @param #SWAPR self +-- @param Core.Set#SET_CLIENT clientset Set of clients to be replaced. +-- @return #SWAPR SWAPR object. +function SWAPR:New(clientset) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #SWAPR + + -- Carrier type. + self.clientset=clientset + + -- Log ID. + self.lid=string.format("SWAPR | ") + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + -- Events are handled directly by DCS. + self:T(self.lid.."Events are handled directly by DCS.") + world.addEventHandler(self) + + self:HandleEvent(EVENTS.RemoveUnit) + + -- Prepare stuff by temporarity spawning aircraft to determine the heading. + self:_Prepare() + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "Stopped") + + + --- Triggers the FSM event "Start" that starts the rescue helo. Initializes parameters and starts event handlers. + -- @function [parent=#SWAPR] Start + -- @param #SWAPR self + + --- Triggers the FSM event "Start" that starts the rescue helo after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#SWAPR] __Start + -- @param #SWAPR self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status" that updates the helo status. + -- @function [parent=#SWAPR] Status + -- @param #SWAPR self + + --- Triggers the delayed FSM event "Status" that updates the helo status. + -- @function [parent=#SWAPR] __Status + -- @param #SWAPR self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. + -- @function [parent=#SWAPR] Stop + -- @param #SWAPR self + + --- Triggers the FSM event "Stop" that stops the rescue helo after a delay. Event handlers are stopped. + -- @function [parent=#SWAPR] __Stop + -- @param #SWAPR self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event handler +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + +--- General event handler. +-- @param #SWAPR self +-- @param #table Event DCS event table. +function SWAPR:onEvent(Event) + self:F3(Event) + + if Event == nil or Event.initiator == nil then + self:T3("Skipping onEvent. Event or Event.initiator unknown.") + return true + end + + if Unit.getByName(Event.initiator:getName()) == nil then + self:T3("Skipping onEvent. Initiator unit name unknown.") + --return true + end + + -- Get unit name and category. + local IniUnitName = Event.initiator:getName() + local IniCategory = Event.initiator:getCategory() + + -- Get client. + local client=self.clientset:FindClient(IniUnitName) + + -- This is not an event involving a client in the defined set. + if not client then + env.info("FF event not associated with client aircraft!") + return + end + + if Event.id==EVENTS.Birth then + + ----------------- + -- BIRTH EVENT -- + ----------------- + + if IniCategory==1 then + + --------------- + -- UNIT BORN -- + --------------- + + local IniDCSGroup = Event.initiator:getGroup() + local IniGroupName = Event.initiator:getGroup():getName() + + -- Debug info. + env.info(string.format("FF Event birth for unit %s of group %s", tostring(IniUnitName), tostring(IniGroupName))) + + -- Get unit. + local unit=UNIT:FindByName(IniUnitName) + + if unit then + + --unit:SmokeGreen() + + -- Group and name. + local group=unit:GetGroup() + local groupname=group:GetName() + + -- Check if this is prepare step to determine the heading. + if string.find(groupname, "_SWAPR") then + --unit:SmokeBlue() + + -- Get info necessary for the static template. + local heading=unit:GetHeading() + local coord=unit:GetCoordinate() + local actype=unit:GetTypeName() + local livery=self:_GetLiveryFromTemplate(IniUnitName) + + -- Add static template to table. + local statictemplate=self:_AddStaticTemplate(IniUnitName, actype, coord.x, coord.z, heading, unit:GetCountry(), livery) + + -- Destroy unit ==> triggers a remove unit event. + unit:Destroy() + + -- Replace aircraft by static. + --self:_Aircraft2Static(unit) + + else + env.info("FF client spawned!") + + -- Get static that is in place of the spawned client. + local static=self.statics[IniUnitName] --Wrapper.Static#STATIC + + -- Remove static. + if static then + env.info("FF destroying static!") + + -- Looks like the MOOSE Destroy function is not fast enough! + static:destroy() + self.statics[IniUnitName]=nil + else + env.info("FF no static to destroy!") + end + + end + + end + + elseif IniCategory==3 then + + ----------------- + -- STATIC BORN -- + ----------------- + + env.info(string.format("FF Event birth for static %s", tostring(IniUnitName))) + + -- WORKS! + local static=STATIC:FindByName(IniUnitName, true) + + -- Add spawned static to table. + --self.statics[IniUnitName]=static + self.statics[IniUnitName]=Event.initiator + + end + + elseif Event.id==EVENTS.PlayerLeaveUnit then + + ----------------- + -- PLAYER LEFT -- + ----------------- + + env.info(string.format("FF Event player leave unit for unit %s", IniUnitName)) + + -- Spawn static. Needs to be delayed a tad or DCS crashes to desktop. + local statictemplate=self.statictemplate[IniUnitName] + if statictemplate then + self:ScheduleOnce(0.1, SWAPR._SpawnStaticAircraft, self, statictemplate) + end + end + +end + +--- General event handler. +-- @param #SWAPR self +-- @param Core.Event#EVENTDATA EventData Event data table. +function SWAPR:OnEventRemoveUnit(EventData) + self:I(EventData) + + if EventData and EventData.IniUnitName then + + -- Debug info. + env.info(string.format("FF Event removed unit %s!", EventData.IniUnitName)) + + -- Spawn static aircraft. + local statictemplate=self.statictemplate[EventData.IniUnitName] + if statictemplate then + self:ScheduleOnce(0.1, SWAPR._SpawnStaticAircraft, self, statictemplate) + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Spawn functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add template to static table. +-- @param #SWAPR self +-- @param #string name Name of the static. +-- @param #string actype Type of the aircraft. +-- @param #number x X coordinate of spawn place. +-- @param #number y Y coordinate of spawn place. +-- @param #number heading Heading of static. +-- @param #number country Country ID of static. +-- @param #string livery Livery ID of the static. +-- @return #table Static template. +function SWAPR:_AddStaticTemplate(name, actype, x, y, heading, country, livery) + + -- Heading is in rad not degrees! + local headingrad=0 + if heading then + headingrad=math.rad(heading) + end + + -- Static template table. + local static={ + livery_id=livery, + heading=headingrad, + type=actype, + name=name, + y=y , + x=x , + CountryID=country, + } + + -- Debug info. + self:I({statictemplate=static}) + + self.statictemplate[name]=static + + return static +end + +--- General event handler. +-- @param #SWAPR self +-- @param #table template The static template. +function SWAPR:_SpawnStaticAircraft(template) + self:I({statictemplate=template}) + + if template and not self.statics[template.name] then + + -- Spawn static. + local static=coalition.addStaticObject(template.CountryID, template) + + -- Debug info. + self:I({spawnedstatic=static}) + env.info(string.format("FF spawned static name = %s", template.name)) + + else + self:T3(self.lid.."WARNING: Static template is nil!") + end + +end + +--- Replace a whole aircraft group by statics. +-- @param #SWAPR self +-- @param Wrapper.Group#GROUP group +function SWAPR:_AircraftGroup2Statics(group) + + -- Get the group template. + local grouptemplate=group:GetTemplate() + + -- Debug info. + self:I({groupname=grouptemplate}) + + for i,_unit in pairs(group:GetUnits()) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Get unit name. + local unitname=unit:GetName() + + local statictemplate=self.statictemplate[unitname] + local static=self.statics[unitname] + + if statictemplate and not static then + + -- Destroy the unit. + unit:Destroy() + + -- Spawn static aircraft instead. + self:_SpawnStaticAircraft(statictemplate) + end + + end +end + +--- Replace a single aircraft unit by static. +-- @param #SWAPR self +-- @param Wrapper.Unit#UNIT unit The unit to be replaced. +function SWAPR:_Aircraft2Static(unit) + + if unit and unit:IsAlive() then + + -- Get the group template. + local grouptemplate=unit:GetGroup():GetTemplate() + + -- Debug info. + self:I({groupname=grouptemplate}) + + -- Get unit name. + local unitname=unit:GetName() + + -- Get the static template. + local statictemplate=self.statictemplate[unitname] + + -- Get the static to check if there already is one. + local static=self.statics[unitname] + + if statictemplate and not static then + + -- Destroy the unit ==> triggers a RemoveUnit event. + unit:Destroy() + + -- Spawn static aircraft instead. + self:_SpawnStaticAircraft(statictemplate) + end + + end +end + + +--- Temporarily spawn uncontrolled aircraft at all client spots to get the correct headings. +-- @param #SWAPR self +function SWAPR:_Prepare() + + for _,_client in pairs(self.clientset:GetSet()) do + local client=_client --Wrapper.Client#CLIENT + + -- Unit name + local unitname=client.ClientName + + if true then + + -- Client group name. + local groupname=_DATABASE.Templates.Units[unitname].GroupName + + -- Client group template copy. + local grouptemplate=UTILS.DeepCopy(_DATABASE:GetGroupTemplate(groupname)) + + -- Nillify the group ID. + grouptemplate.groupId=nil + + -- Set skill. + for i=1,#grouptemplate.units do + local unit=grouptemplate.units[i] + unit.skill="Good" + -- Nillify the unit ID. + unit.unitId=nil + end + + -- Uncontrolled + grouptemplate.uncontrolled=true + + -- Add _SWAPR to the group name so that we find it in birth event. + grouptemplate.name=string.format("%s_SWAPR", groupname) + + -- Debug info. + self:I({grouptemplate=grouptemplate}) + + -- Spawn group. + local group=_DATABASE:Spawn(grouptemplate) + + else + + local livery=self:_GetLiveryFromTemplate(unitname) + local x,y=self:_GetPositionFromTemplate(unitname) + local actype=self:_GetTypeFromTemplate(unitname) + local heading=self:_GetHeadingFromTemplate(unitname) + + local template=self:_AddStaticTemplate(unitname, actype, x, y, heading, 1, livery) + + self:_SpawnStaticAircraft(template) + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get livery from unit. +-- @param #SWAPR self +-- @param #string unitname Name of the unit. +-- @return #string Livery ID. +function SWAPR:_GetLiveryFromTemplate(unitname) + + local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) + + for _,unit in pairs(grouptemplate.units) do + if unit.name==unitname then + return tostring(unit.livery_id) + end + end + + return nil +end + +--- Get livery from unit. +-- @param #SWAPR self +-- @param #string unitname Name of the unit. +-- @return #number X coordinate. +-- @return #number Y coordinate. +function SWAPR:_GetPositionFromTemplate(unitname) + + local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) + + for _,unit in pairs(grouptemplate.units) do + if unit.name==unitname then + return tonumber(unit.x), tonumber(unit.y) + end + end + + return nil, nil +end + + +--- Get livery from unit. +-- @param #SWAPR self +-- @param #string unitname Name of the unit. +-- @return #string Aircraft type. +function SWAPR:_GetTypeFromTemplate(unitname) + + local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) + + for _,unit in pairs(grouptemplate.units) do + if unit.name==unitname then + return tostring(unit.type) + end + end + + return nil +end + +--- Get livery from unit. +-- @param #SWAPR self +-- @param #string unitname Name of the unit. +-- @return #number Heading in degrees. +function SWAPR:_GetHeadingFromTemplate(unitname) + + local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) + + for _,unit in pairs(grouptemplate.units) do + if unit.name==unitname then + return tonumber(unit.heading) + end + end + + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index a5e12f901..a082a82a1 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -62,6 +62,7 @@ __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/Functional/Fox.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/SWAPR.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' ) __Moose.Include( 'Scripts/Moose/Ops/RecoveryTanker.lua' ) diff --git a/Moose Development/Moose/Wrapper/Static.lua b/Moose Development/Moose/Wrapper/Static.lua index 1f2568a73..d26aca44c 100644 --- a/Moose Development/Moose/Wrapper/Static.lua +++ b/Moose Development/Moose/Wrapper/Static.lua @@ -136,6 +136,7 @@ function STATIC:Destroy( GenerateEvent ) end DCSObject:destroy() + return true end return nil diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 16b0fd7c0..6f8c5fd79 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -59,6 +59,7 @@ Functional/Suppression.lua Functional/PseudoATC.lua Functional/Warehouse.lua Functional/Fox.lua +Functional/SWAPR.lua Ops/Airboss.lua Ops/RecoveryTanker.lua From 27a1401ae152c8972cd070b23d99426aeb58e5ee Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 5 Aug 2019 18:04:59 +0200 Subject: [PATCH 333/485] SWAPR v0.0.2 --- Moose Development/Moose/Functional/SWAPR.lua | 213 ++++++++++++++----- Moose Development/Moose/Wrapper/Airbase.lua | 22 +- 2 files changed, 179 insertions(+), 56 deletions(-) diff --git a/Moose Development/Moose/Functional/SWAPR.lua b/Moose Development/Moose/Functional/SWAPR.lua index 34a3094a4..117eedb2e 100644 --- a/Moose Development/Moose/Functional/SWAPR.lua +++ b/Moose Development/Moose/Functional/SWAPR.lua @@ -90,14 +90,14 @@ SWAPR = { --- Class version. -- @field #string version -SWAPR.version="0.0.1" +SWAPR.version="0.0.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Check for clients on ships ==> get airdrome id from first route point. --- TODO: Check that harrier and helo clients are not spawned in shelters ==> get parking spot type for these units in _Prepare() +-- DONE: Check for clients on ships ==> get airdrome id from first route point. +-- DONE: Check that harrier and helo clients are not spawned in shelters ==> get parking spot type for these units in _Prepare() -- TODO: Check what happens if statics are destroyed. -- TODO: Check what happens if clients eject, crash or are shot down. -- TODO: Check that parking spot is not blocked by other aircraft or statics when spawning a static replacement. @@ -207,7 +207,7 @@ function SWAPR:onEvent(Event) if Unit.getByName(Event.initiator:getName()) == nil then self:T3("Skipping onEvent. Initiator unit name unknown.") - --return true + return true end -- Get unit name and category. @@ -219,7 +219,7 @@ function SWAPR:onEvent(Event) -- This is not an event involving a client in the defined set. if not client then - env.info("FF event not associated with client aircraft!") + self:T3(self.lid.."Event not associated with client aircraft!") return end @@ -239,7 +239,7 @@ function SWAPR:onEvent(Event) local IniGroupName = Event.initiator:getGroup():getName() -- Debug info. - env.info(string.format("FF Event birth for unit %s of group %s", tostring(IniUnitName), tostring(IniGroupName))) + self:T(self.lid..string.format("Event birth of unit %s of group %s", tostring(IniUnitName), tostring(IniGroupName))) -- Get unit. local unit=UNIT:FindByName(IniUnitName) @@ -261,6 +261,19 @@ function SWAPR:onEvent(Event) local coord=unit:GetCoordinate() local actype=unit:GetTypeName() local livery=self:_GetLiveryFromTemplate(IniUnitName) + local airbase=self:_GetAirbaseFromTemplate(IniUnitName) + + -- FARPS suck! + if airbase:GetAirbaseCategory()==Airbase.Category.HELIPAD then + local parkingid=self:_GetParkingFromTemplate(IniUnitName) + self:T2(self.lid..string.format("FARP parking id=%s", tostring(parkingid))) + if parkingid then + local spot=airbase:GetParkingSpotData(parkingid) + coord=spot.Coordinate + coord.z=coord.z+5 + coord.x=coord.x+5 + end + end -- Add static template to table. local statictemplate=self:_AddStaticTemplate(IniUnitName, actype, coord.x, coord.z, heading, unit:GetCountry(), livery) @@ -272,20 +285,20 @@ function SWAPR:onEvent(Event) --self:_Aircraft2Static(unit) else - env.info("FF client spawned!") + self:I(self.lid..string.format("Client %s spawned!", IniUnitName)) -- Get static that is in place of the spawned client. local static=self.statics[IniUnitName] --Wrapper.Static#STATIC -- Remove static. if static then - env.info("FF destroying static!") + self:I(self.lid..string.format("Destroying static %s!", IniUnitName)) -- Looks like the MOOSE Destroy function is not fast enough! static:destroy() self.statics[IniUnitName]=nil else - env.info("FF no static to destroy!") + self:E(self.lid..string.format("WARNING: No static %s to destroy!", IniUnitName)) end end @@ -298,7 +311,7 @@ function SWAPR:onEvent(Event) -- STATIC BORN -- ----------------- - env.info(string.format("FF Event birth for static %s", tostring(IniUnitName))) + self:I(self.lid..string.format("Event birth of static %s", tostring(IniUnitName))) -- WORKS! local static=STATIC:FindByName(IniUnitName, true) @@ -315,7 +328,7 @@ function SWAPR:onEvent(Event) -- PLAYER LEFT -- ----------------- - env.info(string.format("FF Event player leave unit for unit %s", IniUnitName)) + self:I(self.lid..string.format("Event player leave unit %s", IniUnitName)) -- Spawn static. Needs to be delayed a tad or DCS crashes to desktop. local statictemplate=self.statictemplate[IniUnitName] @@ -335,7 +348,7 @@ function SWAPR:OnEventRemoveUnit(EventData) if EventData and EventData.IniUnitName then -- Debug info. - env.info(string.format("FF Event removed unit %s!", EventData.IniUnitName)) + self:I(self.lid..string.format("Event removed unit %s!", EventData.IniUnitName)) -- Spawn static aircraft. local statictemplate=self.statictemplate[EventData.IniUnitName] @@ -381,7 +394,7 @@ function SWAPR:_AddStaticTemplate(name, actype, x, y, heading, country, livery) } -- Debug info. - self:I({statictemplate=static}) + self:T2({statictemplate=static}) self.statictemplate[name]=static @@ -400,8 +413,8 @@ function SWAPR:_SpawnStaticAircraft(template) local static=coalition.addStaticObject(template.CountryID, template) -- Debug info. - self:I({spawnedstatic=static}) - env.info(string.format("FF spawned static name = %s", template.name)) + self:T2({spawnedstatic=static}) + self:T(self.lid..string.format("Spawned static %s", template.name)) else self:T3(self.lid.."WARNING: Static template is nil!") @@ -418,7 +431,7 @@ function SWAPR:_AircraftGroup2Statics(group) local grouptemplate=group:GetTemplate() -- Debug info. - self:I({groupname=grouptemplate}) + self:T3({grouptemplate=grouptemplate}) for i,_unit in pairs(group:GetUnits()) do local unit=_unit --Wrapper.Unit#UNIT @@ -452,7 +465,7 @@ function SWAPR:_Aircraft2Static(unit) local grouptemplate=unit:GetGroup():GetTemplate() -- Debug info. - self:I({groupname=grouptemplate}) + self:T3({grouptemplate=grouptemplate}) -- Get unit name. local unitname=unit:GetName() @@ -480,57 +493,98 @@ end -- @param #SWAPR self function SWAPR:_Prepare() + local remove={} for _,_client in pairs(self.clientset:GetSet()) do local client=_client --Wrapper.Client#CLIENT -- Unit name local unitname=client.ClientName + + -- Get airbase if any. + local airbase=self:_GetAirbaseFromTemplate(unitname) + + -- First check that this is not a cliened spawned in air or on a ship. + if airbase==nil then + -- Spawned in air ==> remove! + self:I(self.lid..string.format("Removing client %s because of air start.", unitname)) + table.insert(remove, client) + elseif airbase:GetAirbaseCategory()==Airbase.Category.SHIP then + self:I(self.lid..string.format("Removing client %s because spawned on ship.", unitname)) + table.insert(remove, client) + else - if true then - - -- Client group name. - local groupname=_DATABASE.Templates.Units[unitname].GroupName + if true then - -- Client group template copy. - local grouptemplate=UTILS.DeepCopy(_DATABASE:GetGroupTemplate(groupname)) - - -- Nillify the group ID. - grouptemplate.groupId=nil + -- Check that harriers are not spawned in shelters because they would appear on top of them. + local _continue=true + if self:_GetTypeFromTemplate(unitname)=="AV8BNA" then + local parkingid=self:_GetParkingFromTemplate(unitname) + if parkingid then + env.info(string.format("Harrier parking spot id %d", parkingid)) + local spot=airbase:GetParkingSpotData(parkingid) + if spot and spot.TerminalType==AIRBASE.TerminalType.Shelter then + _continue=false + table.insert(remove, client) + end + end + end + + if _continue then + + -- Client group name. + local groupname=_DATABASE.Templates.Units[unitname].GroupName + + -- Client group template copy. + local grouptemplate=UTILS.DeepCopy(_DATABASE:GetGroupTemplate(groupname)) + + -- Nillify the group ID. + grouptemplate.groupId=nil + + -- Set skill. + for i=1,#grouptemplate.units do + local unit=grouptemplate.units[i] + unit.skill="Good" + -- Nillify the unit ID. + unit.unitId=nil + end + + -- Uncontrolled + grouptemplate.uncontrolled=true + + -- Add _SWAPR to the group name so that we find it in birth event. + grouptemplate.name=string.format("%s_SWAPR", groupname) + + -- Debug info. + self:I({grouptemplate=grouptemplate}) + + -- Spawn group. + local group=_DATABASE:Spawn(grouptemplate) + + end - -- Set skill. - for i=1,#grouptemplate.units do - local unit=grouptemplate.units[i] - unit.skill="Good" - -- Nillify the unit ID. - unit.unitId=nil + else + + local livery=self:_GetLiveryFromTemplate(unitname) + local x,y=self:_GetPositionFromTemplate(unitname) + local actype=self:_GetTypeFromTemplate(unitname) + local heading=self:_GetHeadingFromTemplate(unitname) + + local template=self:_AddStaticTemplate(unitname, actype, x, y, heading, 1, livery) + + self:_SpawnStaticAircraft(template) + end - -- Uncontrolled - grouptemplate.uncontrolled=true - - -- Add _SWAPR to the group name so that we find it in birth event. - grouptemplate.name=string.format("%s_SWAPR", groupname) - - -- Debug info. - self:I({grouptemplate=grouptemplate}) - - -- Spawn group. - local group=_DATABASE:Spawn(grouptemplate) - - else - - local livery=self:_GetLiveryFromTemplate(unitname) - local x,y=self:_GetPositionFromTemplate(unitname) - local actype=self:_GetTypeFromTemplate(unitname) - local heading=self:_GetHeadingFromTemplate(unitname) - - local template=self:_AddStaticTemplate(unitname, actype, x, y, heading, 1, livery) - - self:_SpawnStaticAircraft(template) - end end + + for _,_client in pairs(remove) do + local client=_client --Wrapper.Client#CLIENT + self.clientset:RemoveClientsByName(client.ClientName) + end + + self:I(self.lid..string.format("Number of clients left after prepare = %d", self.clientset:Count())) end @@ -608,6 +662,55 @@ function SWAPR:_GetHeadingFromTemplate(unitname) return nil end +--- Get airbase from template. +-- @param #SWAPR self +-- @param #string unitname Name of the unit. +-- @return Wrapper.Airbase#AIRBASE The airbase object or nil. +function SWAPR:_GetAirbaseFromTemplate(unitname) + + local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) + + -- First waypoint. + local wp=grouptemplate.route.points[1] + + local airbase=nil --Wrapper.Airbase#AIRBASE + local id=-666 + if wp.airdromeId then + id=tonumber(wp.airdromeId) + elseif wp.helipadId then + id=tonumber(wp.helipadId) + end + + -- Find airbase by its id. + airbase=AIRBASE:FindByID(id) + + -- Debug info. + if airbase then + self:T3(self.lid..string.format("Found airbase %s for unit %s, id=%d", airbase:GetName(), unitname, id)) + else + self:T3(self.lid..string.format("Found NO airbase for unit %s, id=%d", unitname,id)) + end + + return airbase +end + +--- Get parking id from template. +-- @param #SWAPR self +-- @param #string unitname Name of the unit. +-- @return #number Parking id or nil. +function SWAPR:_GetParkingFromTemplate(unitname) + + local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) + + for _,unit in pairs(grouptemplate.units) do + if unit.name==unitname then + return tonumber(unit.parking) + end + end + + return nil +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 5d157b821..89b2a38f2 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -361,6 +361,26 @@ function AIRBASE:FindByName( AirbaseName ) return AirbaseFound end +--- Find a AIRBASE in the _DATABASE by its ID. +-- @param #AIRBASE self +-- @param #number id Airbase ID. +-- @return #AIRBASE self +function AIRBASE:FindByID(id) + + for name,_airbase in pairs(_DATABASE.AIRBASES) do + local airbase=_airbase --#AIRBASE + + local aid=tonumber(airbase:GetID()) + + if aid==id then + return airbase + end + + end + + return nil +end + --- Get the DCS object of an airbase -- @param #AIRBASE self -- @return DCS#Airbase DCS airbase object. @@ -604,7 +624,7 @@ function AIRBASE:GetParkingSpotData(TerminalID) for _,_spot in pairs(parkingdata) do local spot=_spot --#AIRBASE.ParkingSpot - self:E({TerminalID=spot.TerminalID,TerminalType=spot.TerminalType}) + self:T({TerminalID=spot.TerminalID,TerminalType=spot.TerminalType}) if TerminalID==spot.TerminalID then return spot end From 6f170db3445915c88416d46cfec43de1c21e62c0 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 5 Aug 2019 21:37:28 +0200 Subject: [PATCH 334/485] Update SWAPR.lua --- Moose Development/Moose/Functional/SWAPR.lua | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/SWAPR.lua b/Moose Development/Moose/Functional/SWAPR.lua index 117eedb2e..e13043acd 100644 --- a/Moose Development/Moose/Functional/SWAPR.lua +++ b/Moose Development/Moose/Functional/SWAPR.lua @@ -101,6 +101,9 @@ SWAPR.version="0.0.2" -- TODO: Check what happens if statics are destroyed. -- TODO: Check what happens if clients eject, crash or are shot down. -- TODO: Check that parking spot is not blocked by other aircraft or statics when spawning a static replacement. +-- TODO: Add FSM events, e.g. static spawned, static destroyed etc. +-- TODO: Add user functions, e.g. for defining the static FARP offset. +-- TODO: Safe/load static templates to/from disk. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -108,7 +111,7 @@ SWAPR.version="0.0.2" --- Create a new SWAPR object. -- @param #SWAPR self --- @param Core.Set#SET_CLIENT clientset Set of clients to be replaced. +-- @param Core.Set#SET_CLIENT clientset (Optional) Set of clients to be replaced. Default all. -- @return #SWAPR SWAPR object. function SWAPR:New(clientset) @@ -116,7 +119,7 @@ function SWAPR:New(clientset) local self = BASE:Inherit(self, FSM:New()) -- #SWAPR -- Carrier type. - self.clientset=clientset + self.clientset=clientset or SET_CLIENT:New():FilterActive(false):FilterOnce() -- Log ID. self.lid=string.format("SWAPR | ") @@ -514,8 +517,13 @@ function SWAPR:_Prepare() else if true then + + --- + -- Spawn a group to get parameters in particular the heading on the parking spot as this is not correct in the template. + --- -- Check that harriers are not spawned in shelters because they would appear on top of them. + -- TODO: Need to do the same of helos? local _continue=true if self:_GetTypeFromTemplate(unitname)=="AV8BNA" then local parkingid=self:_GetParkingFromTemplate(unitname) @@ -564,11 +572,16 @@ function SWAPR:_Prepare() else + --- + -- Get all info from template. Unfortunately, heading is always 0 in the template, i.e. all statics would face due North! + --- + local livery=self:_GetLiveryFromTemplate(unitname) local x,y=self:_GetPositionFromTemplate(unitname) local actype=self:_GetTypeFromTemplate(unitname) local heading=self:_GetHeadingFromTemplate(unitname) + -- TODO: country! local template=self:_AddStaticTemplate(unitname, actype, x, y, heading, 1, livery) self:_SpawnStaticAircraft(template) From fbb32a9e455a774b0792aca5b681393a85049461 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 7 Aug 2019 16:41:21 +0200 Subject: [PATCH 335/485] Removed the escorts to detect targets using DLINK. Now escorts will only report targets that were detected by its own onboard detection methods. --- Moose Development/Moose/AI/AI_Escort.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 815f8dc21..254af3768 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -349,6 +349,15 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) self.Detection = DETECTION_AREAS:New( EscortGroupSet, 5000 ) + -- This only makes the escort report detections made by the escort, not through DLINK. + -- These must be enquired using other facilities. + -- In this way, the escort will report the target areas that are relevant for the mission. + self.Detection:InitDetectVisual( true ) + self.Detection:InitDetectIRST( true ) + self.Detection:InitDetectOptical( true ) + self.Detection:InitDetectRadar( true ) + self.Detection:InitDetectRWR( true ) + self.Detection:__Start( 30 ) self.MainMenu = MENU_GROUP:New( self.PlayerGroup, self.EscortName ) From 4982cabcfbc1405b46790f45be3d2793a945a41c Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 7 Aug 2019 22:17:16 +0200 Subject: [PATCH 336/485] AIRBOSS v1.0.7 Corrected foul deck wave off. --- Moose Development/Moose/Ops/Airboss.lua | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 04f531bd1..04e1a741d 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1683,7 +1683,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.0.6" +AIRBOSS.version="1.0.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -9944,15 +9944,9 @@ function AIRBOSS:_CheckFoulDeck(playerData) -- Assume no check necessary. local check=false - - -- Case I/II: Check is done, when AC is at the wake according to NATOPS. At the wake we switch to final. - if playerData.case<3 and playerData.step==AIRBOSS.PatternStep.FINAL then - check=true - end - - -- Case III: Check is done at 3/4 NM according to NATOPS. - if playerData.step==AIRBOSS.PatternStep.GROOVE_XX or - playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + + -- Check if player is IM or IC. + if playerData.step==AIRBOSS.PatternStep.GROOVE_IM or playerData.step==AIRBOSS.PatternStep.GROOVE_IC then check=true end From f934c8cb7701b0072664e153a1daa4323113f939 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 8 Aug 2019 23:27:45 +0200 Subject: [PATCH 337/485] Update Warehouse.lua --- .../Moose/Functional/Warehouse.lua | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 527138506..087852348 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -5029,12 +5029,7 @@ function WAREHOUSE:onafterDestroyed(From, Event, To) for k,_ in pairs(self.stock) do self.stock[k]=nil end - - --self.queue=nil - --self.queue={} - - --self.stock=nil - --self.stock={} + end end @@ -5814,11 +5809,9 @@ end -- @param Wrapper.Group#GROUP group The group that arrived. function WAREHOUSE:_Arrived(group) self:_DebugMessage(string.format("Group %s arrived!", tostring(group:GetName()))) - --self:E(string.format("Group %s arrived!", tostring(group:GetName()))) if group then --Trigger "Arrived event. - --group:SmokeBlue() self:__Arrived(1, group) end @@ -5830,14 +5823,10 @@ end -- @param #number n Waypoint passed. -- @param #number N Final waypoint. function WAREHOUSE:_PassingWaypoint(group,n,N) - self:T(string.format("Group %s passing waypoint %d of %d!", tostring(group:GetName()), n, N)) - - if group then - --group:SmokeGreen() - end + self:T(self.wid..string.format("Group %s passing waypoint %d of %d!", tostring(group:GetName()), n, N)) + -- Final waypoint reached. if n==N then - --group:SmokeBlue() self:__Arrived(1, group) end From d6b1018700798472a8888ed9c942703cc999d9a3 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 10 Aug 2019 15:41:34 +0200 Subject: [PATCH 338/485] - Implemented attack nearest target. - Implemented attack nearest ground target. - Implemented attack nearest air target. - Report all targets. - Report only ground targets. - Report only ground targets with radar. - Report only air targets. - Sort targest from nearest to furthest. - Improved menu system. - Improved detection of ground targets and air targets and reporting. - Added the task TASK_CAPTURE as part of the reporting framework. - Improved the detection of targets. Now targets with specific detection methods are correctly detected. f.e. disabling DLINK will now work correctly! All these fixes are great improvements to the AI_ESCORT framework! --- Moose Development/Moose/AI/AI_Escort.lua | 291 +++++++++++++----- Moose Development/Moose/AI/AI_Formation.lua | 11 + Moose Development/Moose/Core/Menu.lua | 80 +++-- Moose Development/Moose/Core/Point.lua | 19 +- Moose Development/Moose/Core/Set.lua | 17 + .../Moose/Functional/Detection.lua | 14 +- .../Moose/Wrapper/Controllable.lua | 41 ++- 7 files changed, 355 insertions(+), 118 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 254af3768..5f1dd66f1 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -224,7 +224,6 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) self.PlayerGroup = self.FollowUnit:GetGroup() -- Wrapper.Group#GROUP self.EscortName = EscortName - self.EscortGroupSet = EscortGroupSet self.EscortGroupSet:SetSomeIteratorLimit( 8 ) @@ -266,7 +265,9 @@ function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) end end ) - + + self:SetFlightReportType( self.__Enum.ReportType.All ) + return self end @@ -293,6 +294,7 @@ function AI_ESCORT:_InitFlightMenus() self:SetFlightMenuROT() self:SetFlightMenuTargets() + self:SetFlightMenuReportType() end @@ -357,6 +359,8 @@ function AI_ESCORT:onafterStart( EscortGroupSet ) self.Detection:InitDetectOptical( true ) self.Detection:InitDetectRadar( true ) self.Detection:InitDetectRWR( true ) + + self.Detection:SetAcceptRange( 100000 ) self.Detection:__Start( 30 ) @@ -1115,20 +1119,57 @@ function AI_ESCORT:MenuSmoke( MenuTextFormat ) return self end +function AI_ESCORT:SetFlightMenuReportType() + + local FlightMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", self.FlightMenu ) + local MenuStamp = FlightMenuReportTargets:GetStamp() + + local FlightReportType = self:GetFlightReportType() + + if FlightReportType ~= self.__Enum.ReportType.All then + local FlightMenuReportTargetsAll = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report all targets", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeAll, self ) + :SetTag( "ReportType" ) + :SetStamp( MenuStamp ) + end + + if FlightReportType == self.__Enum.ReportType.All or FlightReportType ~= self.__Enum.ReportType.Airborne then + local FlightMenuReportTargetsAirborne = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report airborne targets", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeAirborne, self ) + :SetTag( "ReportType" ) + :SetStamp( MenuStamp ) + end + + if FlightReportType == self.__Enum.ReportType.All or FlightReportType ~= self.__Enum.ReportType.GroundRadar then + local FlightMenuReportTargetsGroundRadar = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report gound radar targets", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeGroundRadar, self ) + :SetTag( "ReportType" ) + :SetStamp( MenuStamp ) + end + if FlightReportType == self.__Enum.ReportType.All or FlightReportType ~= self.__Enum.ReportType.Ground then + local FlightMenuReportTargetsGround = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report ground targets", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeGround, self ) + :SetTag( "ReportType" ) + :SetStamp( MenuStamp ) + end + + FlightMenuReportTargets:RemoveSubMenus( MenuStamp, "ReportType" ) + +end + function AI_ESCORT:SetFlightMenuTargets() + local FlightMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", self.FlightMenu ) + + -- Report Targets + local FlightMenuReportTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", FlightMenuReportTargets, AI_ESCORT._FlightReportNearbyTargetsNow, self ) + local FlightMenuReportTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, true ) + local FlightMenuReportTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, false ) + + -- Attack Targets + self.FlightMenuAttack = MENU_GROUP:New( self.PlayerGroup, "Attack targets", self.FlightMenu ) + local FlightMenuAttackNearby = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Attack nearest targets", self.FlightMenuAttack, AI_ESCORT._FlightAttackNearestTarget, self ):SetTag( "Attack" ) + local FlightMenuAttackNearbyAir = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Attack nearest airborne targets", self.FlightMenuAttack, AI_ESCORT._FlightAttackNearestTarget, self, self.__Enum.ReportType.Air ):SetTag( "Attack" ) + local FlightMenuAttackNearbyGround = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Attack nearest ground targets", self.FlightMenuAttack, AI_ESCORT._FlightAttackNearestTarget, self, self.__Enum.ReportType.Ground ):SetTag( "Attack" ) + for _, MenuTargets in pairs( self.Menu.Targets) do - local FlightMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", self.FlightMenu ) - - -- Report Targets - local FlightMenuReportTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", FlightMenuReportTargets, AI_ESCORT._FlightReportNearbyTargetsNow, self ) - local FlightMenuReportTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, true ) - local FlightMenuReportTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, false ) - - -- Attack Targets - local FlightMenuAttackNearbyTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", self.FlightMenu ) - MenuTargets.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, MenuTargets.Interval, MenuTargets.Interval ) end @@ -1542,22 +1583,6 @@ function AI_ESCORT:_FlightReportNearbyTargetsNow() end -function AI_ESCORT:_SwitchReportNearbyTargets( EscortGroup, ReportTargets ) - - local EscortUnit = self.PlayerUnit - - self.ReportTargets = ReportTargets - - if self.ReportTargets then - if not EscortGroup.ReportTargetsScheduler then - EscortGroup.ReportTargetsScheduler:Schedule( self, self._ReportTargetsScheduler, {}, 1, 30 ) - end - else - routines.removeFunction( EscortGroup.ReportTargetsScheduler ) - EscortGroup.ReportTargetsScheduler = nil - end -end - function AI_ESCORT:_FlightSwitchReportNearbyTargets( ReportTargets ) @@ -1565,13 +1590,65 @@ function AI_ESCORT:_FlightSwitchReportNearbyTargets( ReportTargets ) --- @param Core.Group#GROUP EscortGroup function( EscortGroup ) if EscortGroup:IsAir() then - self:_SwitchReportNearbyTargets( EscortGroup, ReportTargets ) + self:_EscortSwitchReportNearbyTargets( EscortGroup, ReportTargets ) end end ) end +function AI_ESCORT:SetFlightReportType( ReportType ) + + self.FlightReportType = ReportType + +end + +function AI_ESCORT:GetFlightReportType() + + return self.FlightReportType + +end + +function AI_ESCORT:_FlightSwitchReportTypeAll() + + self:SetFlightReportType( self.__Enum.ReportType.All ) + self:SetFlightMenuReportType() + + local EscortGroup = self.EscortGroupSet:GetFirst() + EscortGroup:MessageTypeToGroup( "Reporting all targets.", MESSAGE.Type.Information, self.PlayerGroup ) + +end + +function AI_ESCORT:_FlightSwitchReportTypeAirborne() + + self:SetFlightReportType( self.__Enum.ReportType.Airborne ) + self:SetFlightMenuReportType() + + local EscortGroup = self.EscortGroupSet:GetFirst() + EscortGroup:MessageTypeToGroup( "Reporting airborne targets.", MESSAGE.Type.Information, self.PlayerGroup ) + +end + +function AI_ESCORT:_FlightSwitchReportTypeGroundRadar() + + self:SetFlightReportType( self.__Enum.ReportType.Ground ) + self:SetFlightMenuReportType() + + local EscortGroup = self.EscortGroupSet:GetFirst() + EscortGroup:MessageTypeToGroup( "Reporting ground radar targets.", MESSAGE.Type.Information, self.PlayerGroup ) + +end + +function AI_ESCORT:_FlightSwitchReportTypeGround() + + self:SetFlightReportType( self.__Enum.ReportType.Ground ) + self:SetFlightMenuReportType() + + local EscortGroup = self.EscortGroupSet:GetFirst() + EscortGroup:MessageTypeToGroup( "Reporting ground targets.", MESSAGE.Type.Information, self.PlayerGroup ) + +end + function AI_ESCORT:_ScanTargets( ScanDuration ) @@ -1724,6 +1801,35 @@ function AI_ESCORT:_FlightAttackTarget( DetectedItem ) end +function AI_ESCORT:_FlightAttackNearestTarget( TargetType ) + + local AttackDetectedItem = nil + local DetectedItems = self.Detection:GetDetectedItems() + + for DetectedItemIndex, DetectedItem in UTILS.spairs( DetectedItems, function( t, a, b ) return self:Distance( self.PlayerUnit, t[a] ) < self:Distance( self.PlayerUnit, t[b] ) end ) do + + local DetectedItemSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local HasGround = DetectedItemSet:HasGroundUnits() > 0 + local HasAir = DetectedItemSet:HasAirUnits() > 0 + + local FlightReportType = self:GetFlightReportType() + + if ( TargetType and TargetType == self.__Enum.ReportType.Ground and HasGround ) or + ( TargetType and TargetType == self.__Enum.ReportType.Air and HasAir ) or + ( TargetType == nil ) then + AttackDetectedItem = DetectedItem + break + end + end + + if AttackDetectedItem then + self:_FlightAttackTarget( AttackDetectedItem ) + end + +end + + --- --- @param #AI_ESCORT self -- @param Wrapper.Group#GROUP EscortGroup The escort group that will attack the detected item. @@ -1884,6 +1990,17 @@ function AI_ESCORT:_ResumeScheduler( EscortGroup ) end +--- Measure distance between coordinate player and coordinate detected item. +-- @param #AI_ESCORT self +function AI_ESCORT:Distance( PlayerUnit, DetectedItem ) + + local DetectedCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + local PlayerCoordinate = PlayerUnit:GetCoordinate() + + return DetectedCoordinate:Get3DDistance( PlayerCoordinate ) + +end + --- Report Targets Scheduler. -- @param #AI_ESCORT self -- @param Wrapper.Group#GROUP EscortGroup @@ -1910,40 +2027,56 @@ function AI_ESCORT:_ReportTargetsScheduler( EscortGroup, Report ) local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) local DetectedTargets = false - for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do - DetectedTargets = true + + for DetectedItemIndex, DetectedItem in UTILS.spairs( DetectedItems, function( t, a, b ) return self:Distance( self.PlayerUnit, t[a] ) < self:Distance( self.PlayerUnit, t[b] ) end ) do + --for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do - local DetectedMenu = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ):Text("\n") - - local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) - local ReportSummary = DetectedItemReportSummary:Text(", ") - DetectedTargetsReport:AddIndent( ReportSummary, "-" ) + local DetectedItemSet = self.Detection:GetDetectedItemSet( DetectedItem ) - if EscortGroup:IsAir() then + local HasGround = DetectedItemSet:HasGroundUnits() > 0 + local HasGroundRadar = HasGround and DetectedItemSet:HasRadar() > 0 + local HasAir = DetectedItemSet:HasAirUnits() > 0 + + local FlightReportType = self:GetFlightReportType() + - MENU_GROUP_COMMAND:New( self.PlayerGroup, - DetectedMenu, - EscortMenuAttackTargets, - AI_ESCORT._AttackTarget, - self, - EscortGroup, - DetectedItem - ):SetTag( "Escort" ):SetTime( TimeUpdate ) - else - if self.EscortMenuTargetAssistance then - local MenuTargetAssistance = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, EscortGroup.EscortMenuTargetAssistance ) + if ( FlightReportType == self.__Enum.ReportType.All ) or + ( FlightReportType == self.__Enum.ReportType.Airborne and HasAir ) or + ( FlightReportType == self.__Enum.ReportType.Ground and HasGround ) or + ( FlightReportType == self.__Enum.ReportType.GroundRadar and HasGroundRadar ) then + + DetectedTargets = true + + local DetectedMenu = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ):Text("\n") + + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportSummary = DetectedItemReportSummary:Text(", ") + DetectedTargetsReport:AddIndent( ReportSummary, "-" ) + + if EscortGroup:IsAir() then + MENU_GROUP_COMMAND:New( self.PlayerGroup, DetectedMenu, - MenuTargetAssistance, - AI_ESCORT._AssistTarget, + EscortMenuAttackTargets, + AI_ESCORT._AttackTarget, self, EscortGroup, DetectedItem - ) + ):SetTag( "Escort" ):SetTime( TimeUpdate ) + else + if self.EscortMenuTargetAssistance then + local MenuTargetAssistance = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, EscortGroup.EscortMenuTargetAssistance ) + MENU_GROUP_COMMAND:New( self.PlayerGroup, + DetectedMenu, + MenuTargetAssistance, + AI_ESCORT._AssistTarget, + self, + EscortGroup, + DetectedItem + ) + end end - end - end EscortMenuAttackTargets:RemoveSubMenus( TimeUpdate, "Escort" ) @@ -1975,43 +2108,53 @@ function AI_ESCORT:_FlightReportTargetsScheduler() if EscortGroup and ( self.PlayerUnit:IsAlive() and EscortGroup:IsAlive() ) then - local ClientGroup = self.PlayerGroup - local TimeUpdate = timer.getTime() - local DetectedItems = self.Detection:GetDetectedItems() local DetectedTargets = false local ClientEscortTargets = self.Detection - local FlightMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", self.FlightMenu ) - - for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do + for DetectedItemIndex, DetectedItem in UTILS.spairs( DetectedItems, function( t, a, b ) return self:Distance( self.PlayerUnit, t[a] ) < self:Distance( self.PlayerUnit, t[b] ) end ) do self:F("FlightReportTargetScheduler Targets") - - DetectedTargets = true -- There are detected targets, when the content of the for loop is executed. We use it to display a message. + local DetectedItemSet = self.Detection:GetDetectedItemSet( DetectedItem ) - local DetectedItemReportMenu = self.Detection:DetectedItemReportMenu( DetectedItem, ClientGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) - local ReportMenuText = DetectedItemReportMenu:Text(", ") + local HasGround = DetectedItemSet:HasGroundUnits() > 0 + local HasGroundRadar = HasGround and DetectedItemSet:HasRadar() > 0 + local HasAir = DetectedItemSet:HasAirUnits() > 0 - MENU_GROUP_COMMAND:New( self.PlayerGroup, - ReportMenuText, - FlightMenuAttackTargets, - AI_ESCORT._FlightAttackTarget, - self, - DetectedItem - ):SetTag( "Flight" ):SetTime( TimeUpdate ) + local FlightReportType = self:GetFlightReportType() + - local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, ClientGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) - local ReportSummary = DetectedItemReportSummary:Text(", ") - DetectedTargetsReport:AddIndent( ReportSummary, "-" ) + if ( FlightReportType == self.__Enum.ReportType.All ) or + ( FlightReportType == self.__Enum.ReportType.Airborne and HasAir ) or + ( FlightReportType == self.__Enum.ReportType.Ground and HasGround ) or + ( FlightReportType == self.__Enum.ReportType.GroundRadar and HasGroundRadar ) then + + + DetectedTargets = true -- There are detected targets, when the content of the for loop is executed. We use it to display a message. + + local DetectedItemReportMenu = self.Detection:DetectedItemReportMenu( DetectedItem, self.PlayerGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportMenuText = DetectedItemReportMenu:Text(", ") + + MENU_GROUP_COMMAND:New( self.PlayerGroup, + ReportMenuText, + self.FlightMenuAttack, + AI_ESCORT._FlightAttackTarget, + self, + DetectedItem + ):SetTag( "Flight" ):SetTime( TimeUpdate ) + + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, self.PlayerGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportSummary = DetectedItemReportSummary:Text(", ") + DetectedTargetsReport:AddIndent( ReportSummary, "-" ) + end end - FlightMenuAttackTargets:RemoveSubMenus( TimeUpdate, "Flight" ) + self.FlightMenuAttack:RemoveSubMenus( TimeUpdate, "Flight" ) if DetectedTargets then EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.PlayerGroup ) diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 21482e930..2ecca0958 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -147,6 +147,17 @@ AI_FORMATION.__Enum.Mode = { Reconnaissance = "R", } +--- @type AI_FORMATION.__Enum.ReportType +-- @field #number All +-- @field #number Airborne +-- @field #number GroundRadar +-- @field #number Ground +AI_FORMATION.__Enum.ReportType = { + Airborne = "*", + Airborne = "A", + GroundRadar = "R", + Ground = "G", +} diff --git a/Moose Development/Moose/Core/Menu.lua b/Moose Development/Moose/Core/Menu.lua index fa7ab6a72..e968b9350 100644 --- a/Moose Development/Moose/Core/Menu.lua +++ b/Moose Development/Moose/Core/Menu.lua @@ -238,7 +238,7 @@ do -- MENU_BASE self.Path = ( self.ParentMenu and "@" .. table.concat( self.MenuParentPath or {}, "@" ) or "" ) .. "@" .. self.MenuText self.Menus = {} self.MenuCount = 0 - self.MenuTime = timer.getTime() + self.MenuStamp = timer.getTime() self.MenuRemoveParent = false if self.ParentMenu then @@ -285,13 +285,31 @@ do -- MENU_BASE function MENU_BASE:GetMenu( MenuText ) return self.Menus[MenuText] end + + --- Sets a menu stamp for later prevention of menu removal. + -- @param #MENU_BASE self + -- @param MenuStamp + -- @return #MENU_BASE + function MENU_BASE:SetStamp( MenuStamp ) + self.MenuStamp = MenuStamp + return self + end + + + --- Gets a menu stamp for later prevention of menu removal. + -- @param #MENU_BASE self + -- @return MenuStamp + function MENU_BASE:GetStamp() + return timer.getTime() + end + --- Sets a time stamp for later prevention of menu removal. -- @param #MENU_BASE self - -- @param MenuTime + -- @param MenuStamp -- @return #MENU_BASE - function MENU_BASE:SetTime( MenuTime ) - self.MenuTime = MenuTime + function MENU_BASE:SetTime( MenuStamp ) + self.MenuStamp = MenuStamp return self end @@ -443,7 +461,7 @@ do -- MENU_MISSION --- Removes the main menu and the sub menus recursively of this MENU_MISSION. -- @param #MENU_MISSION self -- @return #nil - function MENU_MISSION:Remove( MenuTime, MenuTag ) + function MENU_MISSION:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareMission() local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) @@ -451,7 +469,7 @@ do -- MENU_MISSION if MissionMenu == self then self:RemoveSubMenus() - if not MenuTime or self.MenuTime ~= MenuTime then + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then self:F( { Text = self.MenuText, Path = self.MenuPath } ) if self.MenuPath ~= nil then @@ -537,7 +555,7 @@ do -- MENU_MISSION_COMMAND local MissionMenu = MENU_INDEX:HasMissionMenu( Path ) if MissionMenu == self then - if not MenuTime or self.MenuTime ~= MenuTime then + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then self:F( { Text = self.MenuText, Path = self.MenuPath } ) if self.MenuPath ~= nil then @@ -666,7 +684,7 @@ do -- MENU_COALITION --- Removes the main menu and the sub menus recursively of this MENU_COALITION. -- @param #MENU_COALITION self -- @return #nil - function MENU_COALITION:Remove( MenuTime, MenuTag ) + function MENU_COALITION:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareCoalition( self.Coalition ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) @@ -674,7 +692,7 @@ do -- MENU_COALITION if CoalitionMenu == self then self:RemoveSubMenus() - if not MenuTime or self.MenuTime ~= MenuTime then + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then self:F( { Coalition = self.Coalition, Text = self.MenuText, Path = self.MenuPath } ) if self.MenuPath ~= nil then @@ -758,14 +776,14 @@ do -- MENU_COALITION_COMMAND --- Removes a radio command item for a coalition -- @param #MENU_COALITION_COMMAND self -- @return #nil - function MENU_COALITION_COMMAND:Remove( MenuTime, MenuTag ) + function MENU_COALITION_COMMAND:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareCoalition( self.Coalition ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( self.Coalition, Path ) if CoalitionMenu == self then - if not MenuTime or self.MenuTime ~= MenuTime then + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then self:F( { Coalition = self.Coalition, Text = self.MenuText, Path = self.MenuPath } ) if self.MenuPath ~= nil then @@ -907,13 +925,13 @@ do --- Removes the sub menus recursively of this MENU_GROUP. -- @param #MENU_GROUP self - -- @param MenuTime + -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #MENU_GROUP self - function MENU_GROUP:RemoveSubMenus( MenuTime, MenuTag ) + function MENU_GROUP:RemoveSubMenus( MenuStamp, MenuTag ) for MenuText, Menu in pairs( self.Menus or {} ) do - Menu:Remove( MenuTime, MenuTag ) + Menu:Remove( MenuStamp, MenuTag ) end self.Menus = nil @@ -923,18 +941,18 @@ do --- Removes the main menu and sub menus recursively of this MENU_GROUP. -- @param #MENU_GROUP self - -- @param MenuTime + -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #nil - function MENU_GROUP:Remove( MenuTime, MenuTag ) + function MENU_GROUP:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareGroup( self.Group ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) if GroupMenu == self then - self:RemoveSubMenus( MenuTime, MenuTag ) - if not MenuTime or self.MenuTime ~= MenuTime then + self:RemoveSubMenus( MenuStamp, MenuTag ) + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then if self.MenuPath ~= nil then self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) @@ -1014,17 +1032,17 @@ do --- Removes a menu structure for a group. -- @param #MENU_GROUP_COMMAND self - -- @param MenuTime + -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #nil - function MENU_GROUP_COMMAND:Remove( MenuTime, MenuTag ) + function MENU_GROUP_COMMAND:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareGroup( self.Group ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) if GroupMenu == self then - if not MenuTime or self.MenuTime ~= MenuTime then + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then if self.MenuPath ~= nil then self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) @@ -1136,13 +1154,13 @@ do --- Removes the sub menus recursively of this MENU_GROUP_DELAYED. -- @param #MENU_GROUP_DELAYED self - -- @param MenuTime + -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #MENU_GROUP_DELAYED self - function MENU_GROUP_DELAYED:RemoveSubMenus( MenuTime, MenuTag ) + function MENU_GROUP_DELAYED:RemoveSubMenus( MenuStamp, MenuTag ) for MenuText, Menu in pairs( self.Menus or {} ) do - Menu:Remove( MenuTime, MenuTag ) + Menu:Remove( MenuStamp, MenuTag ) end self.Menus = nil @@ -1152,18 +1170,18 @@ do --- Removes the main menu and sub menus recursively of this MENU_GROUP. -- @param #MENU_GROUP_DELAYED self - -- @param MenuTime + -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #nil - function MENU_GROUP_DELAYED:Remove( MenuTime, MenuTag ) + function MENU_GROUP_DELAYED:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareGroup( self.Group ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) if GroupMenu == self then - self:RemoveSubMenus( MenuTime, MenuTag ) - if not MenuTime or self.MenuTime ~= MenuTime then + self:RemoveSubMenus( MenuStamp, MenuTag ) + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then if self.MenuPath ~= nil then self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) @@ -1263,17 +1281,17 @@ do --- Removes a menu structure for a group. -- @param #MENU_GROUP_COMMAND_DELAYED self - -- @param MenuTime + -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #nil - function MENU_GROUP_COMMAND_DELAYED:Remove( MenuTime, MenuTag ) + function MENU_GROUP_COMMAND_DELAYED:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareGroup( self.Group ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) if GroupMenu == self then - if not MenuTime or self.MenuTime ~= MenuTime then + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then if self.MenuPath ~= nil then self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index a2e7fe2bc..e866bf807 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -2065,31 +2065,32 @@ do -- COORDINATE -- @return #string The coordinate Text in the configured coordinate system. function COORDINATE:ToString( Controllable, Settings, Task ) - self:F2( { Controllable = Controllable and Controllable:GetName() } ) + self:E( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS - local ModeA2A = false - self:E('A2A false') + local ModeA2A = nil if Task then - self:E('Task ' .. Task.ClassName ) if Task:IsInstanceOf( TASK_A2A ) then ModeA2A = true - self:E('A2A true') else if Task:IsInstanceOf( TASK_A2G ) then ModeA2A = false else if Task:IsInstanceOf( TASK_CARGO ) then ModeA2A = false - else - ModeA2A = false end + if Task:IsInstanceOf( TASK_CAPTURE_ZONE ) then + ModeA2A = false + end end end - else - local IsAir = Controllable and Controllable:IsAirPlane() or false + end + + + if ModeA2A == nil then + local IsAir = Controllable and ( Controllable:IsAirPlane() or Controllable:IsHelicopter() ) or false if IsAir then ModeA2A = true else diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 23b148ad3..8834dcb0b 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -2624,6 +2624,23 @@ do -- SET_UNIT return GroundUnitCount end + --- Returns if the @{Set} has air targets. + -- @param #SET_UNIT self + -- @return #number The amount of air targets in the Set. + function SET_UNIT:HasAirUnits() + self:F2() + + local AirUnitCount = 0 + for UnitID, UnitData in pairs( self:GetSet() ) do + local UnitTest = UnitData -- Wrapper.Unit#UNIT + if UnitTest:IsAir() then + AirUnitCount = AirUnitCount + 1 + end + end + + return AirUnitCount + end + --- Returns if the @{Set} has friendly ground units. -- @param #SET_UNIT self -- @return #number The amount of ground targets in the Set. diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index d711ad9b6..72289d084 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -2560,9 +2560,17 @@ do -- DETECTION_AREAS local DetectedSet = self:GetDetectedItemSet( DetectedItem ) local ReportSummaryItem - local DetectedZone = self:GetDetectedItemZone( DetectedItem ) - local DetectedItemCoordinate = DetectedZone:GetCoordinate() - local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) + --local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedItem ) + local DetectedAir = DetectedSet:HasAirUnits() + local DetectedAltitude = self:GetDetectedItemCoordinate( DetectedItem ) + local DetectedItemCoordText = "" + if DetectedAir > 0 then + DetectedItemCoordText = DetectedItemCoordinate:ToStringA2A( AttackGroup, Settings ) + else + DetectedItemCoordText = DetectedItemCoordinate:ToStringA2G( AttackGroup, Settings ) + end + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) local DetectedItemsCount = DetectedSet:Count() diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 3b322abac..d23d00070 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -2798,9 +2798,31 @@ function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRad local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil + + local Params = {} + if DetectionVisual then + Params[#Params+1] = DetectionVisual + end + if DetectionOptical then + Params[#Params+1] = DetectionOptical + end + if DetectionRadar then + Params[#Params+1] = DetectionRadar + end + if DetectionIRST then + Params[#Params+1] = DetectionIRST + end + if DetectionRWR then + Params[#Params+1] = DetectionRWR + end + if DetectionDLINK then + Params[#Params+1] = DetectionDLINK + end + + self:T2( { DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK } ) - return self:_GetController():getDetectedTargets( DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK ) + return self:_GetController():getDetectedTargets( Params[1], Params[2], Params[3], Params[4], Params[5], Params[6] ) end return nil @@ -3447,3 +3469,20 @@ function CONTROLLABLE:IsAirPlane() return nil end + +--- Returns if the Controllable contains Helicopters. +-- @param #CONTROLLABLE self +-- @return #boolean true if Controllable contains Helicopters. +function CONTROLLABLE:IsHelicopter() + self:F2() + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local Category = DCSObject:getDesc().category + return Category == Unit.Category.HELICOPTER + end + + return nil +end + From d3c76da7a2f72fc5d1f94cdef18e6bdeb664b4e1 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 11 Aug 2019 08:32:34 +0200 Subject: [PATCH 339/485] Fixes issue with SetDetectionLimit --- .../Moose/AI/AI_A2G_Dispatcher.lua | 11 +++++++-- .../Moose/Functional/Detection.lua | 22 +++++++++++++++-- .../Moose/Functional/DetectionZones.lua | 24 +++++++++++++++---- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index e0484ee64..89fbfb31e 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -4056,6 +4056,7 @@ do -- AI_A2G_DISPATCHER local TaskReport = REPORT:New() + local DefenseTotal = 0 for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do local DefenderGroup = DefenderGroup -- Wrapper.Group#GROUP @@ -4089,12 +4090,15 @@ do -- AI_A2G_DISPATCHER end end +-- for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do +-- DefenseTotal = DefenseTotal + 1 +-- end + local Report = REPORT:New( "\nTactical Overview" ) local DefenderGroupCount = 0 local DefendersTotal = 0 - local DefenseTotal = 0 -- Now that all obsolete tasks are removed, loop through the detected targets. --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do @@ -4149,7 +4153,10 @@ do -- AI_A2G_DISPATCHER end end - if EngageCoordinate and ( ( self.DefenseLimit and DefenseTotal < self.DefenseLimit ) or true ) then + -- There needs to be an EngageCoordinate. + -- If self.DefenseLimit is set (thus limit the amount of defenses to one zone), then only start a new defense if the maximum has not been reached. + -- If self.DefenseLimit has not been set, there is an unlimited amount of zones to be defended. + if ( EngageCoordinate and ( self.DefenseLimit and DefenseTotal < self.DefenseLimit ) or not self.DefenseLimit ) then do local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged... if DefendersMissing > 0 then diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 72289d084..8c8bdcc0a 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -562,9 +562,9 @@ do -- DETECTION_BASE end -- Count alive(!) groups only. Solves issue #1173 https://github.com/FlightControl-Master/MOOSE/issues/1173 - self.DetectionCount = self.DetectionSet:CountAlive() + self.DetectionCount = self:CountAliveRecce() - self.DetectionSet:ForEachGroupAlive( + self:ForEachAliveRecce( function( DetectionGroup ) self:__Detection( DetectDelay, DetectionGroup, DetectionTimeStamp ) -- Process each detection asynchronously. DetectDelay = DetectDelay + 1 @@ -574,6 +574,24 @@ do -- DETECTION_BASE self:__Detect( -self.RefreshTimeInterval ) end + + --- @param #DETECTION_BASE self + -- @param #number The amount of alive recce. + function DETECTION_BASE:CountAliveRecce() + + return self.DetectionSet:CountAlive() + + end + + --- @param #DETECTION_BASE self + function DETECTION_BASE:ForEachAliveRecce( IteratorFunction, ... ) + self:F2( arg ) + + self.DetectionSet:ForEachGroupAlive( IteratorFunction, arg ) + + return self + end + --- @param #DETECTION_BASE self -- @param #string From The From State string. diff --git a/Moose Development/Moose/Functional/DetectionZones.lua b/Moose Development/Moose/Functional/DetectionZones.lua index c44ef9efe..5d9d69750 100644 --- a/Moose Development/Moose/Functional/DetectionZones.lua +++ b/Moose Development/Moose/Functional/DetectionZones.lua @@ -44,15 +44,15 @@ do -- DETECTION_ZONES --- DETECTION_ZONES constructor. -- @param #DETECTION_ZONES self - -- @param Core.Set#SET_ZONE_RADIUS DetectionSetZone The @{Set} of ZONE_RADIUS. + -- @param Core.Set#SET_ZONE DetectionSetZone The @{Set} of ZONE_RADIUS. -- @param DCS#Coalition.side DetectionCoalition The coalition of the detection. -- @return #DETECTION_ZONES function DETECTION_ZONES:New( DetectionSetZone, DetectionCoalition ) -- Inherits from DETECTION_BASE - local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetZone ) ) + local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetZone ) ) -- #DETECTION_ZONES - self.DetectionSetZone = DetectionSetZone + self.DetectionSetZone = DetectionSetZone -- Core.Set#SET_ZONE self.DetectionCoalition = DetectionCoalition self._SmokeDetectedUnits = false @@ -64,6 +64,22 @@ do -- DETECTION_ZONES return self end + --- @param #DETECTION_ZONES self + -- @param #number The amount of alive recce. + function DETECTION_ZONES:CountAliveRecce() + + return self.DetectionSetZone:Count() + + end + + --- @param #DETECTION_ZONES self + function DETECTION_ZONES:ForEachAliveRecce( IteratorFunction, ... ) + self:F2( arg ) + + self.DetectionSetZone:ForEachZone( IteratorFunction, arg ) + + return self + end --- Report summary of a detected item using a given numeric index. -- @param #DETECTION_ZONES self @@ -396,7 +412,5 @@ do -- DETECTION_ZONES return IsDetected end - - end \ No newline at end of file From e770af9fc956d6feab6dbd3c17047e61a3ec2e66 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 11 Aug 2019 09:45:28 +0200 Subject: [PATCH 340/485] Optimized the detection intervals + fixed bugs with player detection. --- Moose Development/Moose/Core/Point.lua | 2 +- Moose Development/Moose/Functional/Detection.lua | 8 +++++--- Moose Development/Moose/Wrapper/Positionable.lua | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index e866bf807..a3805d9b6 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1819,7 +1819,7 @@ do -- COORDINATE --- Returns if a Coordinate is in a certain Radius of this Coordinate in 2D plane using the X and Z axis. -- @param #COORDINATE self - -- @param #COORDINATE ToCoordinate The coordinate that will be tested if it is in the radius of this coordinate. + -- @param #COORDINATE Coordinate The coordinate that will be tested if it is in the radius of this coordinate. -- @param #number Radius The radius of the circle on the 2D plane around this coordinate. -- @return #boolean true if in the Radius. function COORDINATE:IsInRadius( Coordinate, Radius ) diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 8c8bdcc0a..ac3f2923e 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -564,10 +564,12 @@ do -- DETECTION_BASE -- Count alive(!) groups only. Solves issue #1173 https://github.com/FlightControl-Master/MOOSE/issues/1173 self.DetectionCount = self:CountAliveRecce() + local DetectionInterval = self.DetectionCount / ( self.RefreshTimeInterval - 1 ) + self:ForEachAliveRecce( function( DetectionGroup ) self:__Detection( DetectDelay, DetectionGroup, DetectionTimeStamp ) -- Process each detection asynchronously. - DetectDelay = DetectDelay + 1 + DetectDelay = DetectDelay + DetectionInterval end ) @@ -1430,14 +1432,14 @@ do -- DETECTION_BASE world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, TargetData ) DetectedItem.PlayersNearBy = nil - local DetectionZone = ZONE_UNIT:New( "DetectionPlayers", DetectedUnit, self.FriendliesRange ) _DATABASE:ForEachPlayer( --- @param Wrapper.Unit#UNIT PlayerUnit function( PlayerUnitName ) local PlayerUnit = UNIT:FindByName( PlayerUnitName ) - if PlayerUnit and PlayerUnit:IsInZone(DetectionZone) then + if PlayerUnit and PlayerUnit:GetCoordinate():IsInRadius( DetectedUnitCoord, self.FriendliesRange ) then + --if PlayerUnit and PlayerUnit:IsInZone(DetectionZone) then local PlayerUnitCategory = PlayerUnit:GetDesc().category diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 7f5bdd17f..db8283a64 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -63,7 +63,7 @@ POSITIONABLE.__.Cargo = {} -- @param #string PositionableName The POSITIONABLE name -- @return #POSITIONABLE self function POSITIONABLE:New( PositionableName ) - local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) + local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) -- #POSITIONABLE self.PositionableName = PositionableName return self From eebc5d90d155083ecd1ae506f17931262d21d1fe Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 11 Aug 2019 10:06:16 +0200 Subject: [PATCH 341/485] - Fixed Messaging of A2G defenses to players within the same coalition. Now the callsign will be used to communicate, not the user id. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 89fbfb31e..498c7a574 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3511,7 +3511,7 @@ do -- AI_A2G_DISPATCHER self:F({"Defender Birth", Defender:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - local DefenderName = Defender:GetName() + local DefenderName = Defender:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) @@ -3525,7 +3525,7 @@ do -- AI_A2G_DISPATCHER self:F({"Defender PatrolRoute", Defender:GetName()}) self:GetParent(self).onafterPatrolRoute( self, Defender, From, Event, To, AttackSetUnit ) - local DefenderName = Defender:GetName() + local DefenderName = Defender:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) if Squadron then @@ -3539,7 +3539,7 @@ do -- AI_A2G_DISPATCHER self:F({"Defender RTB", Defender:GetName()}) self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) - local DefenderName = Defender:GetName() + local DefenderName = Defender:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) @@ -3552,7 +3552,7 @@ do -- AI_A2G_DISPATCHER self:F({"Defender LostControl", Defender:GetName()}) self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - local DefenderName = Defender:GetName() + local DefenderName = Defender:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) @@ -3567,7 +3567,7 @@ do -- AI_A2G_DISPATCHER self:F({"Defender Home", Defender:GetName()}) self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - local DefenderName = Defender:GetName() + local DefenderName = Defender:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) @@ -3619,7 +3619,7 @@ do -- AI_A2G_DISPATCHER self:F({"Defender Birth", Defender:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - local DefenderName = Defender:GetName() + local DefenderName = Defender:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) @@ -3636,7 +3636,7 @@ do -- AI_A2G_DISPATCHER self:F({"Engage Route", Defender:GetName()}) self:GetParent(self).onafterEngageRoute( self, Defender, From, Event, To, AttackSetUnit ) - local DefenderName = Defender:GetName() + local DefenderName = Defender:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) @@ -3652,7 +3652,7 @@ do -- AI_A2G_DISPATCHER self:F({"Engage Route", Defender:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - local DefenderName = Defender:GetName() + local DefenderName = Defender:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) local FirstUnit = AttackSetUnit:GetFirst() @@ -3666,7 +3666,7 @@ do -- AI_A2G_DISPATCHER function Fsm:onafterRTB( Defender, From, Event, To ) self:F({"Defender RTB", Defender:GetName()}) - local DefenderName = Defender:GetName() + local DefenderName = Defender:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " RTB." ) @@ -3681,7 +3681,7 @@ do -- AI_A2G_DISPATCHER self:F({"Defender LostControl", Defender:GetName()}) self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - local DefenderName = Defender:GetName() + local DefenderName = Defender:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) @@ -3697,7 +3697,7 @@ do -- AI_A2G_DISPATCHER self:F({"Defender Home", Defender:GetName()}) self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - local DefenderName = Defender:GetName() + local DefenderName = Defender:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) From 1bfa5b5ee0c62819d5dcc913e1c09434bb52fcfd Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 11 Aug 2019 11:00:33 +0200 Subject: [PATCH 342/485] Fixed problem with detections escalating. Too much events fired. --- Moose Development/Moose/Functional/DetectionZones.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/DetectionZones.lua b/Moose Development/Moose/Functional/DetectionZones.lua index 5d9d69750..a64710a15 100644 --- a/Moose Development/Moose/Functional/DetectionZones.lua +++ b/Moose Development/Moose/Functional/DetectionZones.lua @@ -395,7 +395,7 @@ do -- DETECTION_ZONES self:__DetectedItem( 0.1, DetectedItem ) end end - self:__Detect( self.RefreshTimeInterval ) + self:__Detect( -self.RefreshTimeInterval ) end end From d84ae236eb25197eb47274232ae60b4f0eb1b243 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 11 Aug 2019 12:59:21 +0200 Subject: [PATCH 343/485] Added a task message when it is engaging or not. Important for communication to the player. --- Moose Development/Moose/AI/AI_Escort.lua | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index 5f1dd66f1..c7e9645f7 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -1783,7 +1783,12 @@ function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) end - EscortGroup:MessageTypeToGroup( "Engaging Target!", MESSAGE.Type.Information, self.PlayerGroup ) + local DetectedTargetsReport = REPORT:New( "Engaging target:\n" ) + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, self.PlayerGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportSummary = DetectedItemReportSummary:Text(", ") + DetectedTargetsReport:AddIndent( ReportSummary, "-" ) + + EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text(), MESSAGE.Type.Information, self.PlayerGroup ) end @@ -1803,6 +1808,10 @@ end function AI_ESCORT:_FlightAttackNearestTarget( TargetType ) + self.Detection:Detect() + self:_FlightReportTargetsScheduler() + + local EscortGroup = self.EscortGroupSet:GetFirst() local AttackDetectedItem = nil local DetectedItems = self.Detection:GetDetectedItems() @@ -1825,6 +1834,8 @@ function AI_ESCORT:_FlightAttackNearestTarget( TargetType ) if AttackDetectedItem then self:_FlightAttackTarget( AttackDetectedItem ) + else + EscortGroup:MessageTypeToGroup( "Nothing to attack!", MESSAGE.Type.Information, self.PlayerGroup ) end end From 53b998cfaba2d891d71c9ba9ba33975c111cce18 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 11 Aug 2019 20:25:33 +0200 Subject: [PATCH 344/485] Airboss v1.0.7 - Corrected Foul Deck waveoff. - Added LSOGrade event. --- Moose Development/Moose/Functional/SWAPR.lua | 730 -- Moose Development/Moose/Modules.lua | 1 - Moose Development/Moose/Ops/Airboss.lua | 6289 +++++++++--------- Moose Setup/Moose.files | 1 - 4 files changed, 3158 insertions(+), 3863 deletions(-) delete mode 100644 Moose Development/Moose/Functional/SWAPR.lua diff --git a/Moose Development/Moose/Functional/SWAPR.lua b/Moose Development/Moose/Functional/SWAPR.lua deleted file mode 100644 index e13043acd..000000000 --- a/Moose Development/Moose/Functional/SWAPR.lua +++ /dev/null @@ -1,730 +0,0 @@ ---- **Functional** - (R2.5) - Replace client aircraft by statics until a player enters. --- --- Make the DCS world a bit more lively! --- --- **Main Features:** --- --- * Easy! --- --- ## Known (DCS) Issues --- --- * Does not support clients on ships. --- * Does not support Harriers and helicopters on parking spots below a shelter. --- --- === --- --- ### Author: **Hardcard** (aka Goreuncle on the MOOSE discord) --- ### Contributions: funkyfranky --- --- @module Functional.Swapr --- @image Functional_SWAPR.png - ---- SWAPR class. --- @type SWAPR --- @field #string ClassName Name of the class. --- @field #boolean Debug Debug mode on/off. --- @field #string lid Log debug id text. --- @field Core.Set#SET_CLIENT clientset Set of clients to be replaced. --- @field #table statics Table of static objects. --- @field #table statictemplate Table of static template --- @extends Core.Fsm#FSM - ---- Swap clients and statics --- --- === --- --- ![Banner Image](..\Presentations\SWAPR\SWAPR_Main.png) --- --- # SWAPR Concept --- --- SWAPR will enable you to easily spawn static aircraft on client slots. When a player enters a client slot, the static object is removed and the player aircraft spawned. --- This makes the airbases look a lot more alive. --- --- # Simple Script --- --- The basic script is very simple and consists of only two lines: --- --- local clientset=SET_CLIENT:New():FilterActive(false):FilterOnce() --- swapr=SWAPR:New(clientset) --- --- The first line defines a set of clients (here all) that will be replaced by statics. --- The second lines initiates the SWAPR script. That's all. --- --- **Note** that Harrier and helicopter clients are automatically removed from the client set if they are placed on a sheltered parking spot. Otherwise the statics would be spawned --- on top of the shelter roof. --- --- Similarly, clients on ships are removed as these would be spawned at sea level and not on the ship itself. --- --- All these are *DCS side restriction* when spawning statics. --- --- # 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 @{#SWAPR} class should have the string "SWAPR" 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("SWAPR") --- --- 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 @{#SWAPR.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 #SWAPR -SWAPR = { - ClassName = "SWAPR", - Debug = false, - lid = nil, - clientset = nil, - statics = {}, - statictemplate = {}, -} - ---- Class version. --- @field #string version -SWAPR.version="0.0.2" - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO list -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- DONE: Check for clients on ships ==> get airdrome id from first route point. --- DONE: Check that harrier and helo clients are not spawned in shelters ==> get parking spot type for these units in _Prepare() --- TODO: Check what happens if statics are destroyed. --- TODO: Check what happens if clients eject, crash or are shot down. --- TODO: Check that parking spot is not blocked by other aircraft or statics when spawning a static replacement. --- TODO: Add FSM events, e.g. static spawned, static destroyed etc. --- TODO: Add user functions, e.g. for defining the static FARP offset. --- TODO: Safe/load static templates to/from disk. - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Constructor -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Create a new SWAPR object. --- @param #SWAPR self --- @param Core.Set#SET_CLIENT clientset (Optional) Set of clients to be replaced. Default all. --- @return #SWAPR SWAPR object. -function SWAPR:New(clientset) - - -- Inherit everthing from FSM class. - local self = BASE:Inherit(self, FSM:New()) -- #SWAPR - - -- Carrier type. - self.clientset=clientset or SET_CLIENT:New():FilterActive(false):FilterOnce() - - -- Log ID. - self.lid=string.format("SWAPR | ") - - -- Debug trace. - if false then - self.Debug=true - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - end - - -- Events are handled directly by DCS. - self:T(self.lid.."Events are handled directly by DCS.") - world.addEventHandler(self) - - self:HandleEvent(EVENTS.RemoveUnit) - - -- Prepare stuff by temporarity spawning aircraft to determine the heading. - self:_Prepare() - - ----------------------- - --- FSM Transitions --- - ----------------------- - - -- Start State. - self:SetStartState("Stopped") - - -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("Stopped", "Start", "Running") - self:AddTransition("*", "Status", "*") - self:AddTransition("*", "Stop", "Stopped") - - - --- Triggers the FSM event "Start" that starts the rescue helo. Initializes parameters and starts event handlers. - -- @function [parent=#SWAPR] Start - -- @param #SWAPR self - - --- Triggers the FSM event "Start" that starts the rescue helo after a delay. Initializes parameters and starts event handlers. - -- @function [parent=#SWAPR] __Start - -- @param #SWAPR self - -- @param #number delay Delay in seconds. - - - --- Triggers the FSM event "Status" that updates the helo status. - -- @function [parent=#SWAPR] Status - -- @param #SWAPR self - - --- Triggers the delayed FSM event "Status" that updates the helo status. - -- @function [parent=#SWAPR] __Status - -- @param #SWAPR self - -- @param #number delay Delay in seconds. - - - --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. - -- @function [parent=#SWAPR] Stop - -- @param #SWAPR self - - --- Triggers the FSM event "Stop" that stops the rescue helo after a delay. Event handlers are stopped. - -- @function [parent=#SWAPR] __Stop - -- @param #SWAPR self - -- @param #number delay Delay in seconds. - - return self -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- User functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Event handler -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ---- General event handler. --- @param #SWAPR self --- @param #table Event DCS event table. -function SWAPR:onEvent(Event) - self:F3(Event) - - if Event == nil or Event.initiator == nil then - self:T3("Skipping onEvent. Event or Event.initiator unknown.") - return true - end - - if Unit.getByName(Event.initiator:getName()) == nil then - self:T3("Skipping onEvent. Initiator unit name unknown.") - return true - end - - -- Get unit name and category. - local IniUnitName = Event.initiator:getName() - local IniCategory = Event.initiator:getCategory() - - -- Get client. - local client=self.clientset:FindClient(IniUnitName) - - -- This is not an event involving a client in the defined set. - if not client then - self:T3(self.lid.."Event not associated with client aircraft!") - return - end - - if Event.id==EVENTS.Birth then - - ----------------- - -- BIRTH EVENT -- - ----------------- - - if IniCategory==1 then - - --------------- - -- UNIT BORN -- - --------------- - - local IniDCSGroup = Event.initiator:getGroup() - local IniGroupName = Event.initiator:getGroup():getName() - - -- Debug info. - self:T(self.lid..string.format("Event birth of unit %s of group %s", tostring(IniUnitName), tostring(IniGroupName))) - - -- Get unit. - local unit=UNIT:FindByName(IniUnitName) - - if unit then - - --unit:SmokeGreen() - - -- Group and name. - local group=unit:GetGroup() - local groupname=group:GetName() - - -- Check if this is prepare step to determine the heading. - if string.find(groupname, "_SWAPR") then - --unit:SmokeBlue() - - -- Get info necessary for the static template. - local heading=unit:GetHeading() - local coord=unit:GetCoordinate() - local actype=unit:GetTypeName() - local livery=self:_GetLiveryFromTemplate(IniUnitName) - local airbase=self:_GetAirbaseFromTemplate(IniUnitName) - - -- FARPS suck! - if airbase:GetAirbaseCategory()==Airbase.Category.HELIPAD then - local parkingid=self:_GetParkingFromTemplate(IniUnitName) - self:T2(self.lid..string.format("FARP parking id=%s", tostring(parkingid))) - if parkingid then - local spot=airbase:GetParkingSpotData(parkingid) - coord=spot.Coordinate - coord.z=coord.z+5 - coord.x=coord.x+5 - end - end - - -- Add static template to table. - local statictemplate=self:_AddStaticTemplate(IniUnitName, actype, coord.x, coord.z, heading, unit:GetCountry(), livery) - - -- Destroy unit ==> triggers a remove unit event. - unit:Destroy() - - -- Replace aircraft by static. - --self:_Aircraft2Static(unit) - - else - self:I(self.lid..string.format("Client %s spawned!", IniUnitName)) - - -- Get static that is in place of the spawned client. - local static=self.statics[IniUnitName] --Wrapper.Static#STATIC - - -- Remove static. - if static then - self:I(self.lid..string.format("Destroying static %s!", IniUnitName)) - - -- Looks like the MOOSE Destroy function is not fast enough! - static:destroy() - self.statics[IniUnitName]=nil - else - self:E(self.lid..string.format("WARNING: No static %s to destroy!", IniUnitName)) - end - - end - - end - - elseif IniCategory==3 then - - ----------------- - -- STATIC BORN -- - ----------------- - - self:I(self.lid..string.format("Event birth of static %s", tostring(IniUnitName))) - - -- WORKS! - local static=STATIC:FindByName(IniUnitName, true) - - -- Add spawned static to table. - --self.statics[IniUnitName]=static - self.statics[IniUnitName]=Event.initiator - - end - - elseif Event.id==EVENTS.PlayerLeaveUnit then - - ----------------- - -- PLAYER LEFT -- - ----------------- - - self:I(self.lid..string.format("Event player leave unit %s", IniUnitName)) - - -- Spawn static. Needs to be delayed a tad or DCS crashes to desktop. - local statictemplate=self.statictemplate[IniUnitName] - if statictemplate then - self:ScheduleOnce(0.1, SWAPR._SpawnStaticAircraft, self, statictemplate) - end - end - -end - ---- General event handler. --- @param #SWAPR self --- @param Core.Event#EVENTDATA EventData Event data table. -function SWAPR:OnEventRemoveUnit(EventData) - self:I(EventData) - - if EventData and EventData.IniUnitName then - - -- Debug info. - self:I(self.lid..string.format("Event removed unit %s!", EventData.IniUnitName)) - - -- Spawn static aircraft. - local statictemplate=self.statictemplate[EventData.IniUnitName] - if statictemplate then - self:ScheduleOnce(0.1, SWAPR._SpawnStaticAircraft, self, statictemplate) - end - - end - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Spawn functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Add template to static table. --- @param #SWAPR self --- @param #string name Name of the static. --- @param #string actype Type of the aircraft. --- @param #number x X coordinate of spawn place. --- @param #number y Y coordinate of spawn place. --- @param #number heading Heading of static. --- @param #number country Country ID of static. --- @param #string livery Livery ID of the static. --- @return #table Static template. -function SWAPR:_AddStaticTemplate(name, actype, x, y, heading, country, livery) - - -- Heading is in rad not degrees! - local headingrad=0 - if heading then - headingrad=math.rad(heading) - end - - -- Static template table. - local static={ - livery_id=livery, - heading=headingrad, - type=actype, - name=name, - y=y , - x=x , - CountryID=country, - } - - -- Debug info. - self:T2({statictemplate=static}) - - self.statictemplate[name]=static - - return static -end - ---- General event handler. --- @param #SWAPR self --- @param #table template The static template. -function SWAPR:_SpawnStaticAircraft(template) - self:I({statictemplate=template}) - - if template and not self.statics[template.name] then - - -- Spawn static. - local static=coalition.addStaticObject(template.CountryID, template) - - -- Debug info. - self:T2({spawnedstatic=static}) - self:T(self.lid..string.format("Spawned static %s", template.name)) - - else - self:T3(self.lid.."WARNING: Static template is nil!") - end - -end - ---- Replace a whole aircraft group by statics. --- @param #SWAPR self --- @param Wrapper.Group#GROUP group -function SWAPR:_AircraftGroup2Statics(group) - - -- Get the group template. - local grouptemplate=group:GetTemplate() - - -- Debug info. - self:T3({grouptemplate=grouptemplate}) - - for i,_unit in pairs(group:GetUnits()) do - local unit=_unit --Wrapper.Unit#UNIT - - -- Get unit name. - local unitname=unit:GetName() - - local statictemplate=self.statictemplate[unitname] - local static=self.statics[unitname] - - if statictemplate and not static then - - -- Destroy the unit. - unit:Destroy() - - -- Spawn static aircraft instead. - self:_SpawnStaticAircraft(statictemplate) - end - - end -end - ---- Replace a single aircraft unit by static. --- @param #SWAPR self --- @param Wrapper.Unit#UNIT unit The unit to be replaced. -function SWAPR:_Aircraft2Static(unit) - - if unit and unit:IsAlive() then - - -- Get the group template. - local grouptemplate=unit:GetGroup():GetTemplate() - - -- Debug info. - self:T3({grouptemplate=grouptemplate}) - - -- Get unit name. - local unitname=unit:GetName() - - -- Get the static template. - local statictemplate=self.statictemplate[unitname] - - -- Get the static to check if there already is one. - local static=self.statics[unitname] - - if statictemplate and not static then - - -- Destroy the unit ==> triggers a RemoveUnit event. - unit:Destroy() - - -- Spawn static aircraft instead. - self:_SpawnStaticAircraft(statictemplate) - end - - end -end - - ---- Temporarily spawn uncontrolled aircraft at all client spots to get the correct headings. --- @param #SWAPR self -function SWAPR:_Prepare() - - local remove={} - for _,_client in pairs(self.clientset:GetSet()) do - local client=_client --Wrapper.Client#CLIENT - - -- Unit name - local unitname=client.ClientName - - -- Get airbase if any. - local airbase=self:_GetAirbaseFromTemplate(unitname) - - -- First check that this is not a cliened spawned in air or on a ship. - if airbase==nil then - -- Spawned in air ==> remove! - self:I(self.lid..string.format("Removing client %s because of air start.", unitname)) - table.insert(remove, client) - elseif airbase:GetAirbaseCategory()==Airbase.Category.SHIP then - self:I(self.lid..string.format("Removing client %s because spawned on ship.", unitname)) - table.insert(remove, client) - else - - if true then - - --- - -- Spawn a group to get parameters in particular the heading on the parking spot as this is not correct in the template. - --- - - -- Check that harriers are not spawned in shelters because they would appear on top of them. - -- TODO: Need to do the same of helos? - local _continue=true - if self:_GetTypeFromTemplate(unitname)=="AV8BNA" then - local parkingid=self:_GetParkingFromTemplate(unitname) - if parkingid then - env.info(string.format("Harrier parking spot id %d", parkingid)) - local spot=airbase:GetParkingSpotData(parkingid) - if spot and spot.TerminalType==AIRBASE.TerminalType.Shelter then - _continue=false - table.insert(remove, client) - end - end - end - - if _continue then - - -- Client group name. - local groupname=_DATABASE.Templates.Units[unitname].GroupName - - -- Client group template copy. - local grouptemplate=UTILS.DeepCopy(_DATABASE:GetGroupTemplate(groupname)) - - -- Nillify the group ID. - grouptemplate.groupId=nil - - -- Set skill. - for i=1,#grouptemplate.units do - local unit=grouptemplate.units[i] - unit.skill="Good" - -- Nillify the unit ID. - unit.unitId=nil - end - - -- Uncontrolled - grouptemplate.uncontrolled=true - - -- Add _SWAPR to the group name so that we find it in birth event. - grouptemplate.name=string.format("%s_SWAPR", groupname) - - -- Debug info. - self:I({grouptemplate=grouptemplate}) - - -- Spawn group. - local group=_DATABASE:Spawn(grouptemplate) - - end - - else - - --- - -- Get all info from template. Unfortunately, heading is always 0 in the template, i.e. all statics would face due North! - --- - - local livery=self:_GetLiveryFromTemplate(unitname) - local x,y=self:_GetPositionFromTemplate(unitname) - local actype=self:_GetTypeFromTemplate(unitname) - local heading=self:_GetHeadingFromTemplate(unitname) - - -- TODO: country! - local template=self:_AddStaticTemplate(unitname, actype, x, y, heading, 1, livery) - - self:_SpawnStaticAircraft(template) - - end - - end - - end - - for _,_client in pairs(remove) do - local client=_client --Wrapper.Client#CLIENT - self.clientset:RemoveClientsByName(client.ClientName) - end - - self:I(self.lid..string.format("Number of clients left after prepare = %d", self.clientset:Count())) - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Misc functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Get livery from unit. --- @param #SWAPR self --- @param #string unitname Name of the unit. --- @return #string Livery ID. -function SWAPR:_GetLiveryFromTemplate(unitname) - - local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) - - for _,unit in pairs(grouptemplate.units) do - if unit.name==unitname then - return tostring(unit.livery_id) - end - end - - return nil -end - ---- Get livery from unit. --- @param #SWAPR self --- @param #string unitname Name of the unit. --- @return #number X coordinate. --- @return #number Y coordinate. -function SWAPR:_GetPositionFromTemplate(unitname) - - local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) - - for _,unit in pairs(grouptemplate.units) do - if unit.name==unitname then - return tonumber(unit.x), tonumber(unit.y) - end - end - - return nil, nil -end - - ---- Get livery from unit. --- @param #SWAPR self --- @param #string unitname Name of the unit. --- @return #string Aircraft type. -function SWAPR:_GetTypeFromTemplate(unitname) - - local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) - - for _,unit in pairs(grouptemplate.units) do - if unit.name==unitname then - return tostring(unit.type) - end - end - - return nil -end - ---- Get livery from unit. --- @param #SWAPR self --- @param #string unitname Name of the unit. --- @return #number Heading in degrees. -function SWAPR:_GetHeadingFromTemplate(unitname) - - local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) - - for _,unit in pairs(grouptemplate.units) do - if unit.name==unitname then - return tonumber(unit.heading) - end - end - - return nil -end - ---- Get airbase from template. --- @param #SWAPR self --- @param #string unitname Name of the unit. --- @return Wrapper.Airbase#AIRBASE The airbase object or nil. -function SWAPR:_GetAirbaseFromTemplate(unitname) - - local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) - - -- First waypoint. - local wp=grouptemplate.route.points[1] - - local airbase=nil --Wrapper.Airbase#AIRBASE - local id=-666 - if wp.airdromeId then - id=tonumber(wp.airdromeId) - elseif wp.helipadId then - id=tonumber(wp.helipadId) - end - - -- Find airbase by its id. - airbase=AIRBASE:FindByID(id) - - -- Debug info. - if airbase then - self:T3(self.lid..string.format("Found airbase %s for unit %s, id=%d", airbase:GetName(), unitname, id)) - else - self:T3(self.lid..string.format("Found NO airbase for unit %s, id=%d", unitname,id)) - end - - return airbase -end - ---- Get parking id from template. --- @param #SWAPR self --- @param #string unitname Name of the unit. --- @return #number Parking id or nil. -function SWAPR:_GetParkingFromTemplate(unitname) - - local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) - - for _,unit in pairs(grouptemplate.units) do - if unit.name==unitname then - return tonumber(unit.parking) - end - end - - return nil -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index a082a82a1..a5e12f901 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -62,7 +62,6 @@ __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/Functional/Fox.lua' ) -__Moose.Include( 'Scripts/Moose/Functional/SWAPR.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' ) __Moose.Include( 'Scripts/Moose/Ops/RecoveryTanker.lua' ) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 04e1a741d..181b4ff08 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1,5 +1,5 @@ --- **Ops** - (R2.5) - Manages aircraft CASE X recoveries for carrier operations (X=I, II, III). --- +-- -- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. -- -- **Main Features:** @@ -23,14 +23,14 @@ -- * Persistence of player results (optional). LSO grading data is saved to csv file. -- * Trap sheet (optional). -- * Finite State Machine (FSM) implementation. --- +-- -- **Supported Carriers:** --- +-- -- * [USS John C. Stennis](https://en.wikipedia.org/wiki/USS_John_C._Stennis) (CVN-74) -- * [USS Tarawa](https://en.wikipedia.org/wiki/USS_Tarawa_(LHA-1)) (LHA-1) [**WIP**] --- +-- -- **Supported Aircraft:** --- +-- -- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI) -- * [F-14B Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI) -- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI) @@ -39,34 +39,34 @@ -- * 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) and A-4E community mod as aircraft and the USS John C. Stennis as carrier. --- +-- -- The AV-8B Harrier and the USS Tarawa are WIP. Those two can only be used together, i.e. the Tarawa is the only carrier the harrier is supposed to land on and -- the no other fixed wing aircraft (human or AI controlled) are supposed to land on the Tarawa. Currently only Case I is supported. Case II/III take slightly steps from the CVN carrier. -- However, the two Case II/III pattern are very similar so this is not a big drawback. --- +-- -- Heatblur's mighty F-14B Tomcat has been added (March 13th 2019) as well. -- --- +-- -- ## Discussion --- +-- -- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-airboss channel. -- There you also find an example mission and the necessary voice over sound files. Check out the **pinned messages**. --- +-- -- ## IMPORTANT --- +-- -- Some important restrictions (of DCS) you should be aware of: --- +-- -- * Each player slot (client) should be in a separate group as DCS does only allow for sending messages to groups and not individual units. -- * Players are identified by their player name. Hence, ensure that no two player have the same name, e.g. "New Callsign", as this will lead to unexpected results. -- * The modex (tail number) of an aircraft should **not** be changed dynamically in the mission by a player. Unfortunately, there is no way to get this information via scripting API functions. --- * The A-4E-C mod needs *easy comms* activated to interact with the F10 radio menu. --- +-- * The A-4E-C mod needs *easy comms* activated to interact with the F10 radio menu. +-- -- ## Youtube Videos --- +-- -- ### AIRBOSS 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) -- * [[MOOSE] Airboss - Groove Test: On-the-fly LSO Grading](https://www.youtube.com/watch?v=Xgs1hwDcPyM) @@ -75,26 +75,26 @@ -- * [[MOOSE] Airboss - New LSO/Marshal Voice Overs by Raynor](https://www.youtube.com/watch?v=_Suo68bRu8k) -- * [[MOOSE] Airboss - CASE I, "Until We Go Down" featuring the F-14B by Pikes](https://www.youtube.com/watch?v=ojgHDSw3Doc) -- * [[MOOSE] Airboss - Skipper Menu](https://youtu.be/awnecCxRoNQ) --- +-- -- ### Lex explaining Boat Ops: --- +-- -- * [( DCS HORNET ) Some boat ops basics VID 1](https://www.youtube.com/watch?v=LvGQS-3AzMc) -- * [( DCS HORNET ) Some boat ops basics VID 2](https://www.youtube.com/watch?v=bN44wvtRsw0) --- +-- -- ### Jabbers Case I and III Recovery Tutorials: --- +-- -- * [DCS World - F/A-18 - Case I Carrier Recovery Tutorial](https://www.youtube.com/watch?v=lm-M3VUy-_I) -- * [DCS World - Case I Recovery Tutorial - Followup](https://www.youtube.com/watch?v=cW5R32Q6xC8) -- * [DCS World - CASE III Recovery Tutorial](https://www.youtube.com/watch?v=Lnfug5CVAvo) --- +-- -- ### Wags DCS Hornet Videos: -- -- * [DCS: F/A-18C Hornet - Episode 9: CASE I Carrier Landing](https://www.youtube.com/watch?v=TuigBLhtAH8) -- * [DCS: F/A-18C Hornet – Episode 16: CASE III Introduction](https://www.youtube.com/watch?v=DvlMHnLjbDQ) -- * [DCS: F/A-18C Hornet Case I Carrier Landing Training Lesson Recording](https://www.youtube.com/watch?v=D33uM9q4xgA) --- +-- -- ### AV-8B Harrier at USS Tarawa --- +-- -- * [Harrier Ship Landing Mission with Auto LSO!](https://www.youtube.com/watch?v=lqmVvpunk2c) -- -- === @@ -139,7 +139,7 @@ -- @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 #table players Table of players. +-- @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. -- @field #AIRBOSS.Checkpoint BreakEarly Early break checkpoint. @@ -231,7 +231,7 @@ -- @field #number skipperSpeed Speed in knots for manual recovery start. -- @field #number skipperCase Manual recovery case. -- @field #boolean skipperUturn U-turn on/off via menu. --- @field #number skipperOffset Holding offset angle in degrees for Case II/III manual recoveries. +-- @field #number skipperOffset Holding offset angle in degrees for Case II/III manual recoveries. -- @field #number skipperTime Recovery time in min for manual recovery. -- @extends Core.Fsm#FSM @@ -244,92 +244,92 @@ -- # The AIRBOSS Concept -- -- On a carrier, the AIRBOSS is guy who is really in charge - don't mess with him! --- +-- -- # Recovery Cases --- +-- -- The AIRBOSS class supports all three commonly used recovery cases, i.e. --- --- * **CASE I** during daytime and good weather (ceiling > 3000 ft, visibility > 5 NM), +-- +-- * **CASE I** during daytime and good weather (ceiling > 3000 ft, visibility > 5 NM), -- * **CASE II** during daytime but poor visibility conditions (ceiling > 1000 ft, visibility > 5NM), -- * **CASE III** when below Case II conditions and during nighttime (ceiling < 1000 ft, visibility < 5 NM). --- +-- -- 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 (within reason!) switch recovery cases in the same mission. --- +-- -- ## CASE I --- +-- -- 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 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. --- +-- -- 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. Note that you need to be below 1300 feet to be registered in the initial zone. -- The altitude can be set via the function @{AIRBOSS.SetInitialMaxAlt}(*altitude*) function. -- As described below, the initial zone can be smoked or flared via the AIRBOSS F10 Help radio menu. --- +-- -- ### 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 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 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, flaps & hook down). --- --- 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. --- +-- +-- 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 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 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. --- +-- -- === -- -- # 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. --- +-- -- ![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 multiple carriers, the menu structure will get completely screwed up.) --- +-- -- ## Root Menu --- +-- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuRoot.png) --- +-- -- The general structure --- +-- -- * **F1 Help...** (Help submenu, see below.) -- * **F2 Kneeboard...** (Kneeboard submenu, see below. Carrier information, weather report, player status.) -- * **F3 Request Marshal** @@ -338,260 +338,260 @@ -- * **F6 Spinning** -- * **F7 Emergency Landing** -- * **F8 [Reset My Status]** --- +-- -- ### 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 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. --- +-- -- ### 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 (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. --- +-- 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 --- +-- -- If a recovery tanker 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 itself at its home base. Once the re-fueling is complete, the player has to re-register to the marshal stack. --- +-- -- ### Spinning --- +-- -- If the pattern is full, players can go into the spinning pattern. This step is only allowed, if the player is in the pattern and his next step -- is initial, break entry, early/late break. At this point, the player should climb to 1200 ft a fly on the port side of the boat to go back to the initial again. --- +-- -- If a player is in the spin pattern, flights in the Marshal queue should hold their altitude and are not allowed into the pattern until the spinning aircraft -- proceeds. --- +-- -- Once the player reaches a point 100 meters behind the boat and at least 1 NM port, his step is set to "Initial" and he can resume the normal pattern approach. --- +-- -- If necessary, the player can call "Spinning" again when in the above mentioned steps. -- -- ### Emergency Landing --- +-- -- Request an emergency landing, i.e. bypass all pattern steps and go directly to the final approach. --- +-- -- All section members are supposed to follow. Player (or section lead) is removed from all other queues and automatically added to the landing pattern queue. --- +-- -- If this command is called while the player is currently on the carrier, he will be put in the bolter pattern. So the next expected step after take of -- is the abeam position. This allows for quick landing training exercises without having to go through the whole pattern. --- +-- -- The mission designer can forbid this option my setting @{#AIRBOSS.SetEmergencyLandings}(false) in the script. --- +-- -- ### [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 --- +-- -- ![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. -- 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. --- +-- -- 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. --- * **Naval Aviator**: Less tips are show. Player should be familiar with the procedures and its aircraft parameters. +-- * **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. -- * **Hints On/Off**: Toggle displaying hints. --- +-- -- ### 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 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 glideslope 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}. Note that in the A-4E you will not hear the message unless you are in the pattern. --- +-- -- ### Marshal Radio Check --- +-- -- 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 if no radio relay unit is used. By default subtitles are on. -- Note that subtitles for radio messages which do not have a complete voice over are always displayed. --- +-- -- ### Trapsheet On/Off --- +-- -- Each player can activated or deactivate the recording of his flight data (AoA, glideslope, lineup, etc.) during his landing approaches. --- Note that this feature also has to be enabled by the mission designer. --- +-- Note that this feature also has to be enabled by the mission designer. +-- -- ## 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. -- * **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. --- +-- * **Last Debrief** shows the detailed debriefing of the player's last approach. +-- -- ### 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 frequencies, 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. --- +-- -- For missions using static weather, more information such as cloud base, thickness, precipitation, visibility distance, fog and dust are displayed. -- If your 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 who issues the command becomes the section lead and all other human players -- 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 Charlie 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. --- +-- 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 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 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 with 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 (distances measured from rundown): --- +-- -- * **X** At the Start (0.75 NM = 1390 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 Wires (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 **LO**w: H, LO -- * Too **F**ast or too **SLO**w: F, SLO -- * **Fly through** glideslope **down** or **up**: \\ , / --- +-- -- 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. --- +-- -- 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 -- * 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 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. --- +-- -- 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 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. --- +-- -- * Break Entry -- * Early Break -- * Late Break @@ -599,49 +599,49 @@ -- * 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. --- +-- -- ## 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 happened. -- * 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. -- * 2.0 Points **WOP**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. -- * 2.0 Points **OWO**: "Own Wave-Off**, when pilot flies past the deck without touching it. -- * 1.0 Points **WO**: "Technique Wave-Off": Player got waved off in the final parts of the groove. -- * 1.0 Points **LIG**: "Long In the Groove", when pilot extents the downwind leg too far and screws up the timing for the following aircraft. -- * 0.0 Points **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, +-- +-- 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 LSO radio comms and is supposed to overfly the landing area to enter the Bolter pattern. **This pass is not graded**. --- +-- -- === --- +-- -- # 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}, @@ -650,70 +650,70 @@ -- * 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. --- +-- -- However, good mission planning involves also planning when aircraft are supposed to be launched or recovered. The definition of *case specific* recovery ops within the same mission is described in -- the next section. -- -- ## 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 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 (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. --- +-- -- 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 -- airbossStennis: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 so that to total wind above the deck --- corresponds to this wind speed. For example, if the is blowing with 5 knots, the carrier will go 15 knots so that the total velocity adds up to the specified 20 knots for the pilot. --- +-- corresponds to this wind speed. For example, if the is blowing with 5 knots, the carrier will go 15 knots so that the total velocity adds up to the specified 20 knots for the pilot. +-- -- 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. --- --- Note that the carrier will only head into the wind, if the wind direction is different by more than 5° from the current heading of the carrier (the angled runway, if any, fis taken into account here). --- +-- +-- Note that the carrier will only head into the wind, if the wind direction is different by more than 5° from the current heading of the carrier (the angled runway, if any, fis taken into account here). +-- -- === --- +-- -- # Persistence of Player Results -- -- LSO grades of players can be saved to disk and later reloaded when a new mission is started. @@ -725,30 +725,30 @@ -- do -- sanitizeModule('os') -- --sanitizeModule('io') -- required for saving files --- --sanitizeModule('lfs') -- optional for setting the default path to your "Saved Games\DCS" folder +-- --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 vulnerable 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\\") @@ -763,17 +763,17 @@ -- 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. @@ -813,17 +813,17 @@ -- -- 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. --- +-- -- # Trap Sheet --- +-- -- Important aircraft attitude parameters during the Groove can be saved to file for later analysis. This also requires the **io** and optionally **lfs** modules to be desanitized. --- +-- -- In the script you have to add the @{#AIRBOSS.SetTrapSheet}(*path*) function to activate this feature. --- +-- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetTable.png) --- +-- -- Data the is written to a file in csv format and contains the following information: --- +-- -- * *Time*: time in seconds since start. -- * *Rho*: distance from rundown to player aircraft in NM. -- * *X*: distance parallel to the carrier in meters. @@ -846,205 +846,205 @@ --## Lineup Error -- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetLUE.png) --- +-- -- The graph displays the lineup error (LUE) as a function of the distance to the carrier. --- +-- -- The pilot approaches the carrier from the port side, LUE>0°, at a distance of ~1 NM. -- At the beginning of the groove (X), he significantly overshoots to the starboard side (LUE<5°). -- In the middle (IM), he performs good corrections and smoothly reduces the lineup error. --- Finally, at a distance of ~0.3 NM (IC) he has corrected his lineup with the runway to a reasonable level, |LUE|<0.5°. --- +-- Finally, at a distance of ~0.3 NM (IC) he has corrected his lineup with the runway to a reasonable level, |LUE|<0.5°. +-- -- ## Glideslope Error --- +-- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetGLE.png) --- +-- -- The graph displays the glideslope error (GSE) as a function of the distance to the carrier. --- +-- -- In this case the pilot already enters the groove (X) below the optimal glideslope. He is not able to correct his height in the IM part and -- stays significantly too low. In close, he performs a harsh correction to gain altitude and ends up even slightly too high (GSE>0.5°). -- At his point further corrections are necessary. --- +-- -- ## Angle of Attack --- +-- -- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetAoA.png) --- +-- -- The graph displays the angle of attack (AoA) as a function of the distance to the carrier. --- +-- -- The pilot starts off being on speed after the ball call. Then he get way to fast troughout the most part of the groove. He manages to correct -- this somewhat short before touchdown. -- -- === --- +-- -- # 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 +-- +-- 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/") --- +-- -- ## Carrier Specific Voice Overs --- +-- -- It is possible to use different sound files for different carriers. If you have set up two (or more) AIRBOSS objects at different carriers - say Stennis and Tarawa - each -- carrier would use the files in the specified directory, e.g. --- +-- -- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles Stennis/") -- airbossTarawa:SetSoundfilesFolder("Airboss Soundfiles Tarawa/") --- +-- -- ## Sound Packs --- +-- -- The AIRBOSS currently has two different "sound packs" for both LSO and Marshal radios. These contain voice overs by different actors. -- These can be set by @{#AIRBOSS.SetVoiceOversLSOByRaynor}() and @{#AIRBOSS.SetVoiceOversMarshalByRaynor}(). These are the default settings. -- The other sound files can be set by @{#AIRBOSS.SetVoiceOversLSOByFF}() and @{#AIRBOSS.SetVoiceOversMarshalByFF}(). -- Also combinations can be used, e.g. --- +-- -- airbossStennis:SetVoiceOversLSOByFF() -- airbossStennis:SetVoiceOversMarshalByRaynor() --- +-- -- In this example LSO voice overs by FF and Marshal voice overs by Raynor are used. --- +-- -- **Note** that this only initializes the correct parameters parameters of sound files, i.e. the duration. The correct files have to be in the directory set by the -- @{#AIRBOSS.SetSoundfilesFolder}(*folder*) function. --- +-- -- ## How To Use Your Own Voice Overs --- +-- -- If you have a set of AIRBOSS sound files recorded or got it from elsewhere it is possible to use those instead of the default ones. -- I recommend to use exactly the same file names as the original sound files have. --- +-- -- However, the **timing is critical**! As sometimes sounds are played directly after one another, e.g. by saying the modex but also on other occations, the airboss -- script has a radio queue implemented (actually two - one for the LSO and one for the Marshal/Airboss radio). -- By this it is automatically taken care that played messages are not overlapping and played over each other. The disadvantage is, that the script needs to know -- the exact duration of *each* voice over. For the default sounds this is hard coded in the source code. For your own files, you need to give that bit of information --- to the script via the @{#AIRBOSS.SetVoiceOver}(**radiocall**, **duration**, **subtitle**, **subduration**, **filename**, **suffix**) function. Only the first two +-- to the script via the @{#AIRBOSS.SetVoiceOver}(**radiocall**, **duration**, **subtitle**, **subduration**, **filename**, **suffix**) function. Only the first two -- parameters **radiocall** and **duration** are usually important to adjust here. --- +-- -- For example, if you want to change the LSO "Call the Ball" and "Roger Ball" calls: --- +-- -- airbossStennis:SetVoiceOver(airbossStennis.LSOCall.CALLTHEBALL, 0.6) -- airbossStennis:SetVoiceOver(airbossStennis.LSOCall.ROGERBALL, 0.7) --- +-- -- Again, changing the file name, subtitle, subtitle duration is not required if you name the file exactly like the original one, which is this case would be "LSO-RogerBall.ogg". --- --- --- +-- +-- +-- -- ## The Radio Dilemma --- +-- -- DCS offers two (actually three) ways to send radio messages. Each one has its advantages and disadvantages and it is important to understand the differences. --- +-- -- ### Transmission via Command --- +-- -- *In principle*, the best way to transmit messages is via the [TransmitMessage](https://wiki.hoggitworld.com/view/DCS_command_transmitMessage) command. -- This method has the advantage that subtitles can be used and these subtitles are only displayed to the players who dialed in the same radio frequency as -- used for the transmission. -- However, this method unfortunately only works if the sending unit is an **aircraft**. Therefore, it is not usable by the AIRBOSS per se as the transmission comes from -- a naval unit (i.e. the carrier). --- +-- -- As a workaround, you can put an aircraft, e.g. a Helicopter on the deck of the carrier or another ship of the strike group. The aircraft should be set to -- uncontrolled and maybe even to immortal. With the @{#AIRBOSS.SetRadioUnitName}(*unitname*) function you can use this unit as "radio repeater" for both Marshal and LSO --- radio channels. However, this might lead to interruptions in the transmission if both channels transmit simultaniously. Therefore, it is better to assign a unit for +-- radio channels. However, this might lead to interruptions in the transmission if both channels transmit simultaniously. Therefore, it is better to assign a unit for -- each radio via the @{#AIRBOSS.SetRadioRelayLSO}(unitname) and @{#AIRBOSS.SetRadioRelayMarshal}(unitname) functions. --- +-- -- Of course you can also use any other aircraft in the vicinity of the carrier, e.g. a rescue helo or a recovery tanker. It is just important that this -- unit is and stays close the the boat as the distance from the sender to the receiver is modeled in DCS. So messages from too far away might not reach the players. --- +-- -- **Note** that not all radio messages the airboss sends have voice overs. Therefore, if you use a radio relay unit, users should *not* disable the --- subtitles in the DCS game menu. --- +-- subtitles in the DCS game menu. +-- -- ### Transmission via Trigger --- --- Another way to broadcast messages is via the [radio transmission trigger](https://wiki.hoggitworld.com/view/DCS_func_radioTransmission). This method can be used for all +-- +-- Another way to broadcast messages is via the [radio transmission trigger](https://wiki.hoggitworld.com/view/DCS_func_radioTransmission). This method can be used for all -- units (land, air, naval). However, messages cannot be subtitled. Therefore, subtitles are displayed to the players via normal textout messages. -- The disadvantage is that is is impossible to know which players have the right radio frequencies dialed in. Therefore, subtitles of the Marshal radio calls are displayed to all players -- inside the CCA. Subtitles on the LSO radio frequency are displayed to all players in the pattern. --- +-- -- ### Sound to User --- +-- -- The third way to play sounds to the user via the [outsound trigger](https://wiki.hoggitworld.com/view/DCS_func_outSound). -- These sounds are not coming from a radio station and therefore can be heard by players independent of their actual radio frequency setting. -- The AIRBOSS class uses this method to play sounds to players which are of a more "private" nature - for example when a player has left his assigned altitude -- in the Marshal stack. Often this is the modex of the player in combination with a textout messaged displayed on screen. --- +-- -- If you want to use this method for all radio messages you can enable it via the @{#AIRBOSS.SetUserSoundRadio}() function. This is the analogue of activating easy comms in DCS. --- +-- -- Note that this method is used for all players who are in the A-4E community mod as this mod does not have the ability to use radios due to current DCS restrictions. -- Therefore, A-4E drivers will hear all radio transmissions from the Marshal/Airboss and all LSO messages as soon as their commence the pattern. --- +-- -- === --- +-- -- # 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. --- +-- -- In case only specifc AI groups shall be excluded, it can be done by adding the groups to a set, e.g. --- +-- -- -- AI groups explicitly excluded from handling by the Airboss -- local CarrierExcludeSet=SET_GROUP:New():FilterPrefixes("E-2D Wizard Group"):FilterStart() -- AirbossStennis:SetExcludeAI(CarrierExcludeSet) --- +-- -- Similarly, to the @{#AIRBOSS.SetExcludeAI} function, AI groups can be explicitly *included* via the @{#AIRBOSS.SetSquadronAI} function. If this is used, only the *included* groups are handled -- by the AIRBOSS. --- +-- -- ## Keep the Deck Clean --- +-- -- Once the AI groups have landed on the carrier, they can be despawned automatically after they shut down their engines. This is achieved by the @{#AIRBOSS.SetDespawnOnEngineShutdown}() function. --- +-- -- ## Refueling --- +-- -- AI groups in the marshal pattern can be send to refuel at the recovery tanker or if none is defined to the nearest divert airfield. This can be enabled by the @{#AIRBOSS.SetRefuelAI}(*lowfuelthreshold*). -- The parameter *lowfuelthreshold* is the threshold of fuel in percent. If the fuel drops below this value, the group will go for refueling. If refueling is performed at the recovery tanker, -- the group will return to the marshal stack when done. The aircraft will not return from the divert airfield however. --- +-- -- Note that this feature is not enabled by default as there might be bugs in DCS that prevent a smooth refueling of the AI. Enable at your own risk. --- +-- -- ## Respawning - DCS Landing Bug --- +-- -- AI groups that enter the CCA are usually guided to Marshal stack. However, due to DCS limitations they might not obey the landing task if they have another airfield as departure and/or destination in -- their mission task. Therefore, AI groups can be respawned when detected in the CCA. This should clear all other airfields and allow the aircraft to land on the carrier. -- This is achieved by the @{AIRBOSS.SetRespawnAI}() 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 +-- 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. --- +-- -- === --- +-- -- # Finite State Machine (FSM) --- +-- -- The AIRBOSS class has a Finite State Machine (FSM) implementation for the carrier. This allows mission designers to hook into certain events and helps -- simulate complex behaviour easier. --- +-- -- FSM events are: --- +-- -- * @{#AIRBOSS.Start}: Starts the AIRBOSS FSM. -- * @{#AIRBOSS.Stop}: Stops the AIRBOSS FSM. -- * @{#AIRBOSS.Idle}: Carrier is set to idle and not recovering. @@ -1054,29 +1054,29 @@ -- * @{#AIRBOSS.RecoveryUnpause}: Unpauses the recovery ops. -- * @{#AIRBOSS.RecoveryCase}: Sets/switches the recovery case. -- * @{#AIRBOSS.PassingWaypoint}: Carrier passes a waypoint defined in the mission editor. --- +-- -- These events can be used in the user script. When the event is triggered, it is automatically a function OnAfter*Eventname* called. For example --- +-- -- --- Carrier just passed waypoint *n*. -- function AirbossStennis:OnAfterPassingWaypoint(From, Event, To, n) -- -- Launch green flare. -- self.carrier:FlareGreen() -- end --- +-- -- In this example, we only launch a green flare every time the carrier passes a waypoint defined in the mission editor. But, of course, you can also use it to add new -- recovery windows each time a carrier passes a waypoint. Therefore, you can create an "infinite" number of windows easily. --- +-- -- === --- +-- -- # 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 10 am. Carrier will turn into the wind 5 min before window opens and go at a speed so that wind over the deck is 25 knots. -- local window1=AirbossStennis:AddRecoveryWindow("9:00", "10:00", 1, nil, true, 25) @@ -1084,35 +1084,35 @@ -- local window2=AirbossStennis:AddRecoveryWindow("15:00", "16:00", 2, 15) -- -- Case III with +30 degrees holding offset from 21:00 to 23:30. -- 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 -- 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. -- @@ -1152,10 +1152,10 @@ AIRBOSS = { BreakEntry = {}, BreakEarly = {}, BreakLate = {}, - Abeam = {}, + Abeam = {}, Ninety = {}, Wake = {}, - Final = {}, + Final = {}, Groove = {}, Platform = {}, DirtyUp = {}, @@ -1163,7 +1163,7 @@ AIRBOSS = { defaultcase = nil, case = nil, defaultoffset = nil, - holdingoffset = nil, + holdingoffset = nil, recoverytimes = {}, flights = {}, Qpattern = {}, @@ -1304,7 +1304,7 @@ AIRBOSS.CarrierType={ --- Glideslope error thresholds in degrees. -- @type AIRBOSS.GLE --- @field #number _max Max _OK_ value. Default 0.4 deg. +-- @field #number _max Max _OK_ value. Default 0.4 deg. -- @field #number _min Min _OK_ value. Default -0.3 deg. -- @field #number High (H) threshold. Default 0.8 deg. -- @field #number Low (L) threshold. Default -0.6 deg. @@ -1481,7 +1481,7 @@ AIRBOSS.GroovePos={ -- @field #AIRBOSS.RadioCall SLOW "You're slow" call. -- @field #AIRBOSS.RadioCall STABILIZED "Stabilized" call. -- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call. --- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard" call. +-- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard" call. -- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. -- @field #AIRBOSS.RadioCall NOISE Static noise sound. -- @field #AIRBOSS.RadioCall SPINIT "Spin it" call. @@ -1594,7 +1594,7 @@ AIRBOSS.Difficulty={ -- @field #string theatre DCS map. -- @field #string mitime Mission time in hh:mm:ss+d format -- @field #string midate Mission date in yyyy/mm/dd format. --- @field #string osdate Real live date. Needs **os** to be desanitized. +-- @field #string osdate Real live date. Needs **os** to be desanitized. --- Checkpoint parameters triggering the next step in the pattern. -- @type AIRBOSS.Checkpoint @@ -1689,8 +1689,8 @@ AIRBOSS.version="1.0.7" -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Handle tanker and AWACS. Put them into pattern. --- TODO: Handle cases where AI crashes on carrier deck ==> Clean up deck. +-- TODO: Handle tanker and AWACS. Put them into pattern. +-- TODO: Handle cases where AI crashes on carrier deck ==> Clean up deck. -- TODO: Player eject and crash debrief "gradings". -- TODO: PWO during case 2/3. -- TODO: PWO when player comes too close to other flight. @@ -1715,7 +1715,7 @@ AIRBOSS.version="1.0.7" -- 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: Improve trapped wire calculation. -- 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. @@ -1761,13 +1761,13 @@ function AIRBOSS:New(carriername, alias) -- Inherit everthing from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #AIRBOSS - + -- Debug. self:F2({carriername=carriername, alias=alias}) -- Set carrier unit. self.carrier=UNIT:FindByName(carriername) - + -- Check if carrier unit exists. if self.carrier==nil then -- Error message. @@ -1776,99 +1776,99 @@ function AIRBOSS:New(carriername, alias) self:E(text) return nil end - + -- Set some string id for output to DCS.log file. self.lid=string.format("AIRBOSS %s | ", carriername) -- Current map. - self.theatre=env.mission.theatre + self.theatre=env.mission.theatre self:T2(self.lid..string.format("Theatre = %s.", tostring(self.theatre))) - + -- Get carrier type. self.carriertype=self.carrier:GetTypeName() - + -- Set alias. self.alias=alias or carriername - + -- Set carrier airbase object. self.airbase=AIRBASE:FindByName(carriername) - + -- Create carrier beacon. self.beacon=BEACON:New(self.carrier) - + -- Set Tower Frequency of carrier. self:_GetTowerFrequency() - + -- Init player scores table. self.playerscores={} - + -- Initialize ME waypoints. self:_InitWaypoints() - + -- Current waypoint. self.currentwp=1 - + -- Patrol route. - self:_PatrolRoute() - + self:_PatrolRoute() + ------------- --- Defaults: ------------- - - -- Set up Airboss radio. - self:SetMarshalRadio() - - -- Set up LSO radio. + + -- Set up Airboss radio. + self:SetMarshalRadio() + + -- Set up LSO radio. self:SetLSORadio() - + -- Set LSO call interval. Default 4 sec. self:SetLSOCallInterval() - + -- Radio scheduler. self.radiotimer=SCHEDULER:New() - + -- Set magnetic declination. self:SetMagneticDeclination() - + -- Set ICSL to channel 1. self:SetICLS() - + -- Set TACAN to channel 74X. self:SetTACAN() - + -- Becons are reactivated very 5 min. self:SetBeaconRefresh() -- 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() - + -- Set max flights per stack. Default is 2. self:SetMaxFlightsPerStack() - + -- Set AI handling On. self:SetHandleAION() - + -- Airboss is a nice guy. self:SetAirbossNiceGuy() - + -- Allow emergency landings. self:SetEmergencyLandings() - + -- No despawn after engine shutdown by default. self:SetDespawnOnEngineShutdown(false) - + -- No respawning of AI groups when entering the CCA. self:SetRespawnAI(false) - + -- Mission uses static weather by default. self:SetStaticWeather() - + -- Default recovery case. This sets self.defaultcase and self.case. Default Case I. self:SetRecoveryCase() @@ -1877,44 +1877,44 @@ function AIRBOSS:New(carriername, alias) -- Set Marshal stack radius. Default 2.75 NM, which gives a diameter of 5.5 NM. self:SetMarshalRadius() - + -- Set max alt at initial. Default 1300 ft. self:SetInitialMaxAlt() - + -- Default player skill EASY. self:SetDefaultPlayerSkill(AIRBOSS.Difficulty.EASY) - + -- Default glideslope error thresholds. self:SetGlideslopeErrorThresholds() - + -- Default lineup error thresholds. self:SetLineupErrorThresholds() - + -- CCA 50 NM radius zone around the carrier. self:SetCarrierControlledArea() - + -- CCZ 5 NM radius zone around the carrier. - self:SetCarrierControlledZone() - + self:SetCarrierControlledZone() + -- 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() self:SetDefaultMessageDuration() - + -- Menu options. self:SetMenuMarkZones() self:SetMenuSmokeZones() self:SetMenuSingleCarrier(false) - + -- Welcome players. self:SetWelcomePlayers(true) - + -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then self:_InitStennis() @@ -1931,14 +1931,14 @@ function AIRBOSS:New(carriername, alias) self:E(self.lid..string.format("ERROR: Unknown carrier type %s!", tostring(self.carriertype))) return nil end - + -- Init voice over files. self:_InitVoiceOvers() - + ------------------- -- Debug Section -- ------------------- - + -- Debug trace. if false then self.Debug=true @@ -1947,7 +1947,7 @@ function AIRBOSS:New(carriername, alias) BASE:TraceLevel(1) --self.dTstatus=0.1 end - + -- Smoke zones. if false then local case=2 @@ -1964,55 +1964,55 @@ function AIRBOSS:New(carriername, alias) self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Orange, 45) self:_GetZoneCommence(case):SmokeZone(SMOKECOLOR.Red, 45) end - + -- Carrier parameter debug tests. if false then -- Stern coordinate. local FB=self:GetFinalBearing(false) local hdg=self:GetHeading(false) - + -- Stern pos. local stern=self:_GetSternCoord() - + -- Bow pos. local bow=stern:Translate(self.carrierparam.totlength, hdg) - + -- End of rwy. local rwy=stern:Translate(self.carrierparam.rwylength, FB, true) - + --- Flare points and zones. local function flareme() -- Carrier pos. self:GetCoordinate():FlareYellow() - + -- Stern stern:FlareYellow() -- Bow bow:FlareYellow() - + -- 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(self.carrierparam.totwidthstarboard, hdg+90) cR:FlareYellow() - + -- Left 40 meters from stern. local cL=stern:Translate(self.carrierparam.totwidthport, hdg-90) cL:FlareYellow() - + -- Carrier specific. if self.carrier:GetTypeName()~=AIRBOSS.CarrierType.TARAWA then - + -- Flare wires. local w1=stern:Translate(self.carrierparam.wire1, FB) local w2=stern:Translate(self.carrierparam.wire2, FB) @@ -2022,37 +2022,37 @@ function AIRBOSS:New(carriername, alias) w2:FlareYellow() w3:FlareWhite() w4:FlareYellow() - + else - + -- Abeam landing spot zone. local ALSPT=self:_GetZoneAbeamLandingSpot() ALSPT:FlareZone(FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters(120)) - + -- Primary landing spot zone. local LSPT=self:_GetZoneLandingSpot() LSPT:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) -- Landing spot coordinate. local PLSC=self:_GetLandingSpotCoordinate() - PLSC:FlareWhite() + PLSC:FlareWhite() end - + -- Flare carrier and landing runway. local cbox=self:_GetZoneCarrierBox() - local rbox=self:_GetZoneRunwayBox() + local rbox=self:_GetZoneRunwayBox() cbox:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) rbox:FlareZone(FLARECOLOR.White, 5, nil, self.carrierparam.deckheight) end - + -- Flare points every 3 seconds for 3 minutes. SCHEDULER:New(nil, flareme, {}, 1, 3, nil, 180) end - + ----------------------- --- FSM Transitions --- ----------------------- - + -- Start State. self:SetStartState("Stopped") @@ -2068,7 +2068,8 @@ function AIRBOSS:New(carriername, alias) self:AddTransition("*", "Status", "*") -- Update status of players and queues. self:AddTransition("*", "RecoveryCase", "*") -- Switch to another case recovery. self:AddTransition("*", "PassingWaypoint", "*") -- Carrier is passing a waypoint. - self:AddTransition("*", "Save", "*") -- Save player scores to file. + self:AddTransition("*", "LSOGrade", "*") -- LSO grade. + self:AddTransition("*", "Save", "*") -- Save player scores to file. self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS FMS. @@ -2241,6 +2242,29 @@ function AIRBOSS:New(carriername, alias) -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + --- Triggers the FSM event "LSOGrade". Called when the LSO grades a player + -- @function [parent=#AIRBOSS] LSOGrade + -- @param #AIRBOSS self + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- Triggers the FSM event "LSOGrade". Delayed called when the LSO grades a player. + -- @function [parent=#AIRBOSS] __LSOGrade + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- On after "LSOGrade" user function. Called when the carrier passes a waypoint of its route. + -- @function [parent=#AIRBOSS] OnAfterLSOGrade + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. -- @function [parent=#AIRBOSS] Stop -- @param #AIRBOSS self @@ -2249,7 +2273,7 @@ function AIRBOSS:New(carriername, alias) -- @function [parent=#AIRBOSS] __Stop -- @param #AIRBOSS self -- @param #number delay Delay in seconds. - + return self end @@ -2314,10 +2338,10 @@ function AIRBOSS:SetRecoveryCase(case) -- Set default case or 1. self.defaultcase=case or 1 - + -- Current case init. self.case=self.defaultcase - + return self end @@ -2329,12 +2353,12 @@ end -- @return #AIRBOSS self function AIRBOSS:SetHoldingOffsetAngle(offset) - -- Set default angle or 0. + -- Set default angle or 0. self.defaultoffset=offset or 0 - + -- Current offset init. self.holdingoffset=self.defaultoffset - + return self end @@ -2351,13 +2375,13 @@ function AIRBOSS:SetMenuRecovery(duration, windondeck, uturn, offset) self.skipperTime=duration or 30 self.skipperSpeed=windondeck or 25 self.skipperOffset=offset or 30 - + if uturn then self.skipperUturn=true else self.skipperUturn=false end - + return self end @@ -2375,16 +2399,16 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, tur -- 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) - + -- 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 window rejected.", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop))) @@ -2394,21 +2418,21 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, tur 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 - + -- Case or default value. case=case or self.defaultcase - + -- Holding offset or default value. holdingoffset=holdingoffset or self.defaultoffset - + -- Offset zero for case I. if case==1 then holdingoffset=0 end - + -- Increase counter. self.windowcount=self.windowcount+1 - + -- Recovery window. local recovery={} --#AIRBOSS.Recovery recovery.START=Tstart @@ -2420,22 +2444,22 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, tur recovery.WIND=turnintowind recovery.SPEED=speed or 20 recovery.ID=self.windowcount - + if uturn==nil or uturn==true then recovery.UTURN=true else recovery.UTURN=false end - + -- Add to table table.insert(self.recoverytimes, recovery) - + 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. +-- @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 @@ -2456,11 +2480,11 @@ end -- @param Wrapper.Group#GROUP group The group to be excluded. -- @return #AIRBOSS self function AIRBOSS:AddExcludeAI(group) - + self.excludesetAI=self.excludesetAI or SET_GROUP:New() - + self.excludesetAI:AddGroup(group) - + return self end @@ -2522,10 +2546,10 @@ function AIRBOSS:DeleteRecoveryWindow(window, delay) -- 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. @@ -2533,7 +2557,7 @@ function AIRBOSS:DeleteRecoveryWindow(window, delay) else table.remove(self.recoverytimes, i) end - + end end end @@ -2559,7 +2583,7 @@ 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. +-- @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 @@ -2584,7 +2608,7 @@ function AIRBOSS:SetEmergencyLandings(switch) end ---- Despawn AI groups after they they shut down their engines. +--- Despawn AI groups after they they shut down their engines. -- @param #AIRBOSS self -- @param #boolean switch If true or nil, AI groups are despawned. -- @return #AIRBOSS self @@ -2612,7 +2636,7 @@ end --- Give AI aircraft the refueling task if a recovery tanker is present or send them to the nearest divert airfield. -- @param #AIRBOSS self --- @param #number lowfuelthreshold Low fuel threshold in percent. AI will go refueling if their fuel level drops below this value. Default 10 %. +-- @param #number lowfuelthreshold Low fuel threshold in percent. AI will go refueling if their fuel level drops below this value. Default 10 %. -- @return #AIRBOSS self function AIRBOSS:SetRefuelAI(lowfuelthreshold) self.lowfuelAI=lowfuelthreshold or 10 @@ -2631,7 +2655,7 @@ 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. +-- 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 @@ -2648,9 +2672,9 @@ function AIRBOSS:SetSoundfilesFolder(folderpath) -- Folderpath. self.soundfolder=folderpath - -- Info message. + -- Info message. self:I(self.lid..string.format("Setting sound files folder to: %s", self.soundfolder)) - + return self end @@ -2663,7 +2687,7 @@ function AIRBOSS:SetStatusUpdateTime(interval) return self end ---- Set duration how long messages are displayed to players. +--- Set duration how long messages are displayed to players. -- @param #AIRBOSS self -- @param #number duration Duration in seconds. Default 10 sec. -- @return #AIRBOSS self @@ -2811,7 +2835,7 @@ function AIRBOSS:SetTACAN(channel, mode, morsecode) self.TACANmode=mode or "X" self.TACANmorse=morsecode or "STN" self.TACANon=true - + return self end @@ -2851,24 +2875,24 @@ end --- 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". +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". -- @return #AIRBOSS self function AIRBOSS:SetLSORadio(frequency, modulation) self.LSOFreq=(frequency or 264) modulation=modulation or "AM" - + if modulation=="FM" then self.LSOModu=radio.modulation.FM else self.LSOModu=radio.modulation.AM end - + self.LSORadio={} --#AIRBOSS.Radio self.LSORadio.frequency=self.LSOFreq self.LSORadio.modulation=self.LSOModu self.LSORadio.alias="LSO" - + return self end @@ -2881,18 +2905,18 @@ function AIRBOSS:SetMarshalRadio(frequency, modulation) self.MarshalFreq=frequency or 305 modulation=modulation or "AM" - + if modulation=="FM" then self.MarshalModu=radio.modulation.FM else self.MarshalModu=radio.modulation.AM end - + self.MarshalRadio={} --#AIRBOSS.Radio self.MarshalRadio.frequency=self.MarshalFreq self.MarshalRadio.modulation=self.MarshalModu self.MarshalRadio.alias="MARSHAL" - + return self end @@ -2942,28 +2966,28 @@ function AIRBOSS:SoundCheckLSO(delay) -- Delayed call. SCHEDULER:New(nil, AIRBOSS.SoundCheckLSO, {self}, delay) else - - + + local text="Playing LSO sound files:" - + for _name,_call in pairs(self.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) - + -- Also play the loud version. if call.loud then self:RadioTransmission(self.LSORadio, call, true) end end - + -- Debug message. self:I(self.lid..text) - + end end @@ -2977,28 +3001,28 @@ function AIRBOSS:SoundCheckMarshal(delay) -- Delayed call. SCHEDULER:New(nil, AIRBOSS.SoundCheckMarshal, {self}, delay) else - - + + local text="Playing Marshal sound files:" - + for _name,_call in pairs(self.MarshalCall) 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.MarshalRadio, call, false) - + -- Also play the loud version. if call.loud then self:RadioTransmission(self.MarshalRadio, call, true) end end - + -- Debug message. self:I(self.lid..text) - + end end @@ -3086,7 +3110,7 @@ function AIRBOSS:SetAWACS(awacs) 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} @@ -3097,7 +3121,7 @@ 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 @@ -3105,13 +3129,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 @@ -3141,7 +3165,7 @@ end -- @return #AIRBOSS self function AIRBOSS:SetPatrolAdInfinitum(switch) if switch==false then - self.adinfinitum=false + self.adinfinitum=false else self.adinfinitum=true end @@ -3174,20 +3198,20 @@ end --- Check if carrier is idle, i.e. no operations are carried out. -- @param #AIRBOSS self --- @return #boolean If true, carrier is in idle state. +-- @return #boolean If true, carrier is in idle state. 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 +-- @return #boolean If true, recovery is paused function AIRBOSS:IsPaused() return self:is("Paused") end --- Activate TACAN and ICLS beacons. --- @param #AIRBOSS self +-- @param #AIRBOSS self function AIRBOSS:_ActivateBeacons() self:T(self.lid..string.format("Activating Beacons (TACAN=%s, ICLS=%s)", tostring(self.TACANon), tostring(self.ICLSon))) @@ -3196,14 +3220,14 @@ function AIRBOSS:_ActivateBeacons() self:I(self.lid..string.format("Activating TACAN Channel %d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse)) self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) end - + -- Activate ICLS. if self.ICLSon then self:I(self.lid..string.format("Activating ICLS Channel %d (%s)", self.ICLSchannel, self.ICLSmorse)) self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) end - -- Set time stamp. + -- Set time stamp. self.Tbeacon=timer.getTime() end @@ -3220,37 +3244,37 @@ 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 on map %s", AIRBOSS.version, self.carrier:GetName(), self.carriertype, self.theatre)) - + -- Activate TACAN and ICLS if desired. self:_ActivateBeacons() - + -- 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) - + -- Initial carrier position and orientation. self.Cposition=self:GetCoordinate() self.Corientation=self.carrier:GetOrientationX() self.Corientlast=self.Corientation self.Tpupdate=timer.getTime() - + -- Check if no recovery window is set. DISABLED! if #self.recoverytimes==0 and false then - + -- Open window in 15 minutes for 3 hours. local Topen=timer.getAbsTime()+15*60 local Tclose=Topen+3*60*60 - - -- Add window. + + -- 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. + + -- 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) @@ -3260,7 +3284,7 @@ function AIRBOSS:onafterStart(From, Event, To) self:HandleEvent(EVENTS.Ejection) self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) self:HandleEvent(EVENTS.MissionEnd) - + -- Start status check in 1 second. self:__Status(1) end @@ -3274,35 +3298,35 @@ function AIRBOSS:onafterStatus(From, Event, To) -- Get current time. local time=timer.getTime() - + -- Update marshal and pattern queue every 30 seconds. if time-self.Tqueue>self.dTqueue then - + -- 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() local speed=self.carrier:GetVelocityKNOTS() - + -- Check water is ahead. local collision=self:_CheckCollisionCoord(pos:Translate(self.collisiondist, hdg)) - + local holdtime=0 if self.holdtimestamp then holdtime=timer.getTime()-self.holdtimestamp end - + -- Check if carrier is stationary. local NextWP=self:_GetNextWaypoint() local ExpectedSpeed=UTILS.MpsToKnots(NextWP:GetVelocity()) - if speed<0.5 and ExpectedSpeed>0 and not (self.detour or self.turnintowind) then + if speed<0.5 and ExpectedSpeed>0 and not (self.detour or self.turnintowind) then if not self.holdtimestamp then self:E(self.lid..string.format("Carrier came to an unexpected standstill. Trying to re-route in 3 min. Speed=%.1f knots, expected=%.1f knots", speed, ExpectedSpeed)) self.holdtimestamp=timer.getTime() - else + else if holdtime>3*60 then local coord=self:GetCoordinate():Translate(500, hdg+10) --coord:MarkToAll("Re-route after standstill.") @@ -3316,7 +3340,7 @@ function AIRBOSS:onafterStatus(From, Event, To) local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s - Turning=%s - Collision Warning=%s - Detour=%s - Turn Into Wind=%s - Holdtime=%d sec", clock, self:GetState(), self.case, speed, hdg, self.currentwp, eta, tostring(self.turning), tostring(collision), tostring(self.detour), tostring(self.turnintowind), holdtime) self:T(self.lid..text) - + -- Players online: text="Players:" local i=0 @@ -3329,63 +3353,63 @@ function AIRBOSS:onafterStatus(From, Event, To) text=text.." none" end self:I(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 + + -- 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() - + -- Remove dead/zombie flight groups. Player leaving the server whilst in pattern etc. --self:_RemoveDeadFlightGroups() - + -- Scan carrier zone for new aircraft. self:_ScanCarrierZone() - + -- Check marshal and pattern queues. self:_CheckQueue() - + -- Check if carrier is currently turning. self:_CheckCarrierTurning() - + -- Check if marshal pattern of AI needs an update. self:_CheckPatternUpdate() - + -- Time stamp. self.Tqueue=time end - + -- (Re-)activate TACAN and ICLS channels. if time-self.Tbeacon>self.dTbeacon then self:_ActivateBeacons() - end - + end + -- Check player status. self:_CheckPlayerStatus() - + -- Check AI landing pattern status self:_CheckAIStatus() @@ -3400,73 +3424,73 @@ function AIRBOSS:_CheckAIStatus() -- Loop over all flights in Marshal stack. for _,_flight in pairs(self.Qmarshal) do local flight=_flight --#AIRBOSS.FlightGroup - + -- Only AI! if flight.ai then - + -- Get fuel amount in %. local fuel=flight.group:GetFuelMin()*100 - + -- Debug text. local text=string.format("Group %s fuel=%.1f %%", flight.groupname, fuel) self:T3(self.lid..text) - + -- Check if flight is low on fuel and not yet refueling. if self.lowfuelAI and fuel=recovery.START then -- Start time has passed. - + if time0 then -- Extend recovery time. 5 min per flight. local extmin=5*npattern recovery.STOP=recovery.STOP+extmin*60 - + local text=string.format("We still got flights in the pattern.\nRecovery time prolonged by %d minutes.\nNow get your act together and no more bolters!", extmin) self:MessageToPattern(text, "AIRBOSS", "99", 10, false, nil) - + else - + -- Set carrier to idle. self:RecoveryStop() state="closing now" - + -- Closed. recovery.OPEN=false -- Window just closed. recovery.OVER=true - - end + + end else - + -- Carrier is already idle. state="closed" end - + end - + else -- This recovery is in the future. state="in the future" - + -- This is the next to come as we sorted by start time. if nextwindow==nil then nextwindow=recovery state="next in line" end end - + -- Debug text. 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. self:T(self.lid..text) - + -- Current recovery window. self.recoverywindow=nil - - + + if self:IsIdle() then ----------------------------------------------------------------------------------------------------------------- -- Carrier is idle: We need to make sure that incoming flights get the correct recovery info of the next window. ----------------------------------------------------------------------------------------------------------------- - + -- Check if there is a next windows defined. 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 - + -- Check that wind is blowing from a direction > 5° different from the current heading. local hdg=self:GetHeading() local wind=self:GetHeadingIntoWind() @@ -3723,50 +3747,50 @@ function AIRBOSS:_CheckRecoveryTimes() if vwind<0.1 then uturn=false end - + -- U-turn disabled by user input. if not nextwindow.UTURN then uturn=false end - + --Debug info self:T(self.lid..string.format("Heading=%03d°, Wind=%03d° %.1f kts, Delta=%03d° ==> U-turn=%s", hdg, wind,UTILS.MpsToKnots(vwind), delta, tostring(uturn))) - + -- Time into the wind 1 day or if longer recovery time + the 5 min early. local t=math.max(nextwindow.STOP-nextwindow.START+300, 60*60*24) - - -- Recovery wind on deck in knots. + + -- Recovery wind on deck in knots. local v=UTILS.KnotsToMps(nextwindow.SPEED) - + -- Check that we do not go above max possible speed. local vmax=self.carrier:GetSpeedMax()/3.6 -- convert to m/s v=math.min(v,vmax) - + -- Route carrier into the wind. Sets self.turnintowind=true self:CarrierTurnIntoWind(t, v, uturn) - + end - + -- Set current recovery window. self.recoverywindow=nextwindow - + else -- No next window. Set default values. self:RecoveryCase() end - + else ------------------------------------------------------------------------------------- - -- Carrier is recovering: We set the recovery window to the current one or next one. + -- Carrier is recovering: We set the recovery window to the current one or next one. ------------------------------------------------------------------------------------- - + if currwindow then self.recoverywindow=currwindow else self.recoverywindow=nextwindow end end - + self:T2({"FF", recoverywindow=self.recoverywindow}) end @@ -3788,7 +3812,7 @@ function AIRBOSS:_GetFlightLead(flight) end ---- On before "RecoveryCase" event. Check if case or holding offset did change. If not transition is denied. +--- On before "RecoveryCase" event. Check if case or holding offset did change. If not transition is denied. -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. @@ -3799,10 +3823,10 @@ function AIRBOSS:onbeforeRecoveryCase(From, Event, To, Case, Offset) -- Input or default value. Case=Case or self.defaultcase - + -- Input or default value Offset=Offset or self.defaultoffset - + if Case==self.case and Offset==self.holdingoffset then return false end @@ -3810,7 +3834,7 @@ function AIRBOSS:onbeforeRecoveryCase(From, Event, To, Case, Offset) return true end ---- On after "RecoveryCase" event. Sets new aircraft recovery case. Updates +--- On after "RecoveryCase" event. Sets new aircraft recovery case. Updates -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. @@ -3821,10 +3845,10 @@ function AIRBOSS:onafterRecoveryCase(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. local text=string.format("Switching recovery case %d ==> %d", self.case, Case) if Case>1 then @@ -3832,14 +3856,14 @@ function AIRBOSS:onafterRecoveryCase(From, Event, To, Case, Offset) end MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) self:T(self.lid..text) - + -- Set new recovery case. self.case=Case - + -- Set holding offset. self.holdingoffset=Offset - - -- Update case of all flights not in Marshal or Pattern queue. + + -- 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 @@ -3847,19 +3871,19 @@ function AIRBOSS:onafterRecoveryCase(From, Event, To, Case, Offset) -- 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 @@ -3875,13 +3899,13 @@ 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 - + -- Radio message: "99, starting aircraft recovery case X ops. (Marshal radial XYZ degrees)" self:_MarshalCallRecoveryStart(Case) - + -- Switch to case. self:RecoveryCase(Case, Offset) end @@ -3894,32 +3918,32 @@ end function AIRBOSS:onafterRecoveryStop(From, Event, To) -- Debug output. self:T(self.lid..string.format("Stopping aircraft recovery.")) - - -- Recovery ops stopped message. + + -- Recovery ops stopped message. self:_MarshalCallRecoveryStopped(self.case) - + -- If carrier is currently heading into the wind, we resume the original route. if self.turnintowind then - + -- Coordinate to return to. local coord=self.Creturnto - + -- No U-turn. if self.recoverywindow and self.recoverywindow.UTURN==false then coord=nil end - + -- Carrier resumes route. self:CarrierResumeRoute(coord) end - + -- 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 - + end + -- Check recovery windows. This sets self.recoverywindow to the next window. self:_CheckRecoveryTimes() end @@ -3934,28 +3958,28 @@ end function AIRBOSS:onafterRecoveryPause(From, Event, To, duration) -- Debug output. self:T(self.lid..string.format("Pausing aircraft recovery.")) - + -- Message text - + if duration then - + -- Auto resume. self:__RecoveryUnpause(duration) - + -- Time to resume. local clock=UTILS.SecondsToClock(timer.getAbsTime()+duration) - + -- Marshal call: "99, aircraft recovery paused and will be resume at XX:YY." self:_MarshalCallRecoveryPausedResumedAt(clock) else local text=string.format("aircraft recovery is paused until further notice.") - + -- Marshal call: "99, aircraft recovery paused until further notice." self:_MarshalCallRecoveryPausedNotice() - + end - + end --- On after "RecoveryUnpause" event. Recovery of aircraft is resumed. @@ -3966,7 +3990,7 @@ end function AIRBOSS:onafterRecoveryUnpause(From, Event, To) -- Debug output. self:T(self.lid..string.format("Unpausing aircraft recovery.")) - + -- Resume recovery. self:_MarshalCallRecoveryResume() @@ -3993,7 +4017,7 @@ function AIRBOSS:onafterIdle(From, Event, To) self:T(self.lid..string.format("Carrier goes to idle.")) end ---- On after Stop event. Unhandle events. +--- On after Stop event. Unhandle events. -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. @@ -4021,24 +4045,24 @@ function AIRBOSS:_InitStennis() -- Carrier Parameters. self.carrierparam.sterndist =-153 self.carrierparam.deckheight = 19 - + -- Total size of the carrier (approx as rectangle). 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 - + -- Landing runway. self.carrierparam.rwyangle = -9 self.carrierparam.rwylength = 225 self.carrierparam.rwywidth = 20 - + -- 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. - + -- 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. @@ -4048,8 +4072,8 @@ function AIRBOSS:_InitStennis() self.Platform.LimitXmin=nil -- Limits via zone self.Platform.LimitXmax=nil self.Platform.LimitZmin=nil - self.Platform.LimitZmax=nil - + self.Platform.LimitZmax=nil + -- Level out at 1200 ft and dirty up. self.DirtyUp.name="Dirty Up" self.DirtyUp.Xmin=-UTILS.NMToMeters(21) -- Not more than 21 NM behind the boat. @@ -4059,8 +4083,8 @@ function AIRBOSS:_InitStennis() self.DirtyUp.LimitXmin=nil -- Limits via zone self.DirtyUp.LimitXmax=nil self.DirtyUp.LimitZmin=nil - self.DirtyUp.LimitZmax=nil - + self.DirtyUp.LimitZmax=nil + -- Intercept glide slope and follow bullseye. self.Bullseye.name="Bullseye" self.Bullseye.Xmin=-UTILS.NMToMeters(11) -- Not more than 11 NM behind the boat. Last check was at 10 NM. @@ -4071,7 +4095,7 @@ function AIRBOSS:_InitStennis() self.Bullseye.LimitXmax=nil self.Bullseye.LimitZmin=nil self.Bullseye.LimitZmax=nil - + -- Break entry. 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. @@ -4093,7 +4117,7 @@ function AIRBOSS:_InitStennis() 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=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. @@ -4104,7 +4128,7 @@ function AIRBOSS:_InitStennis() 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=-UTILS.NMToMeters(5) -- Not more then 5 NM astern of boat. Should be LIG call anyway. @@ -4125,7 +4149,7 @@ function AIRBOSS:_InitStennis() self.Ninety.LimitXmin=nil self.Ninety.LimitXmax=nil self.Ninety.LimitZmin=nil - self.Ninety.LimitZmax=-UTILS.NMToMeters(0.6) -- Check and next step when 0.6 NM port. + self.Ninety.LimitZmax=-UTILS.NMToMeters(0.6) -- Check and next step when 0.6 NM port. -- At the Wake. self.Wake.name="Wake" @@ -4148,7 +4172,7 @@ function AIRBOSS:_InitStennis() 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. @@ -4168,27 +4192,27 @@ function AIRBOSS:_InitTarawa() -- Init Stennis as default. self:_InitStennis() - + -- Carrier Parameters. self.carrierparam.sterndist =-125 self.carrierparam.deckheight = 21 --69 ft - + -- Total size of the carrier (approx as rectangle). self.carrierparam.totlength=245 self.carrierparam.totwidthport=10 self.carrierparam.totwidthstarboard=25 - + -- Landing runway. self.carrierparam.rwyangle = 0 self.carrierparam.rwylength = 225 self.carrierparam.rwywidth = 15 - + -- Wires. self.carrierparam.wire1=nil self.carrierparam.wire2=nil self.carrierparam.wire3=nil self.carrierparam.wire4=nil - + -- 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. @@ -4199,14 +4223,14 @@ function AIRBOSS:_InitTarawa() self.BreakLate.LimitXmax= nil self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.5) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 self.BreakLate.LimitZmax= nil - + end --- Init parameters for Marshal Voice overs *Gabriella* by HighwaymanEd. -- @param #AIRBOSS self -- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. function AIRBOSS:SetVoiceOversMarshalByGabriella(mizfolder) - + -- Set sound files folder. if mizfolder then local lastchar=string.sub(mizfolder, -1) @@ -4218,7 +4242,7 @@ function AIRBOSS:SetVoiceOversMarshalByGabriella(mizfolder) -- Default is the general folder. self.soundfolderMSH=self.soundfolder end - + -- Report for duty. self:I(self.lid..string.format("Marshal Gabriella reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) @@ -4270,7 +4294,7 @@ end -- @param #AIRBOSS self -- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. function AIRBOSS:SetVoiceOversMarshalByRaynor(mizfolder) - + -- Set sound files folder. if mizfolder then local lastchar=string.sub(mizfolder, -1) @@ -4282,7 +4306,7 @@ function AIRBOSS:SetVoiceOversMarshalByRaynor(mizfolder) -- Default is the general folder. self.soundfolderMSH=self.soundfolder end - + -- Report for duty. self:I(self.lid..string.format("Marshal Raynor reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) @@ -4311,7 +4335,7 @@ function AIRBOSS:SetVoiceOversMarshalByRaynor(mizfolder) self.MarshalCall.N8.duration=0.38 self.MarshalCall.N9.duration=0.34 self.MarshalCall.NEGATIVE.duration=0.60 - self.MarshalCall.NEWFB.duration=1.10 + self.MarshalCall.NEWFB.duration=1.10 self.MarshalCall.OPS.duration=0.46 self.MarshalCall.POINT.duration=0.21 self.MarshalCall.RADIOCHECK.duration=0.95 @@ -4402,9 +4426,9 @@ function AIRBOSS:SetVoiceOversLSOByFF(mizfolder) -- Default is the general folder. self.soundfolderLSO=self.soundfolder end - + -- Report for duty. - self:I(self.lid..string.format("LSO FF reporting for duty! Soundfolder=%s", tostring(self.soundfolderLSO))) + self:I(self.lid..string.format("LSO FF reporting for duty! Soundfolder=%s", tostring(self.soundfolderLSO))) self.LSOCall.BOLTER.duration=0.75 self.LSOCall.CALLTHEBALL.duration=0.60 @@ -4458,7 +4482,7 @@ function AIRBOSS:SetVoiceOversMarshalByFF(mizfolder) -- Default is the general folder. self.soundfolderMSH=self.soundfolder end - + -- Report for duty. self:I(self.lid..string.format("Marshal FF reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) @@ -4524,7 +4548,7 @@ function AIRBOSS:_InitVoiceOvers() }, CALLTHEBALL={ file="LSO-CallTheBall", - suffix="ogg", + suffix="ogg", loud=false, subtitle="Call the ball", duration=0.6, @@ -4588,7 +4612,7 @@ function AIRBOSS:_InitVoiceOvers() }, POWER={ file="LSO-Power", - suffix="ogg", + suffix="ogg", loud=true, subtitle="Power", duration=0.50, --0.45 was too short @@ -4612,12 +4636,12 @@ function AIRBOSS:_InitVoiceOvers() }, ROGERBALL={ file="LSO-RogerBall", - suffix="ogg", - loud=false, + suffix="ogg", + loud=false, subtitle="Roger ball", duration=1.00, subduration=2, - }, + }, WAVEOFF={ file="LSO-WaveOff", suffix="ogg", @@ -4789,9 +4813,9 @@ function AIRBOSS:_InitVoiceOvers() subtitle="", duration=0.73, subduration=5, - }, + }, } - + ----------------- -- Pilot Calls -- ----------------- @@ -4874,7 +4898,7 @@ function AIRBOSS:_InitVoiceOvers() loud=false, subtitle="", duration=0.33, - }, + }, SKYHAWK={ file="PILOT-Skyhawk", suffix="ogg", @@ -4953,7 +4977,7 @@ function AIRBOSS:_InitVoiceOvers() duration=1.95, }, } - + ------------------- -- MARSHAL Radio -- ------------------- @@ -5160,7 +5184,7 @@ function AIRBOSS:_InitVoiceOvers() loud=false, subtitle="", duration=0.33, - }, + }, RADIOCHECK={ file="MARSHAL-RadioCheck", suffix="ogg", @@ -5267,7 +5291,7 @@ function AIRBOSS:_InitVoiceOvers() -- Default timings by Raynor self:SetVoiceOversLSOByRaynor() self:SetVoiceOversMarshalByRaynor() - + end --- Init voice over radio transmission call. @@ -5296,10 +5320,10 @@ function AIRBOSS:_GetAircraftAoA(playerData) 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. aoa.SLOW = 9.8 @@ -5356,34 +5380,34 @@ function AIRBOSS:_AoAUnit2Deg(playerData, 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. -- However: AoA=15 Units ==> 15 degrees, which is too much. degrees=-10+50/30*aoaunits - + -- HB Facebook page https://www.facebook.com/heatblur/photos/a.683612385159716/754368278084126 -- AoA=15 Units <==> AoA=10.359 degrees. degrees=0.918*aoaunits-3.411 - + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then - + ---------- -- A-4E -- ---------- -- A-4E-C source code suggests a simple factor of 1/2 for conversion. degrees=0.5*aoaunits - + end - + return degrees end @@ -5403,29 +5427,29 @@ function AIRBOSS:_AoADeg2Units(playerData, degrees) ------------- -- 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 -- HB Facebook page https://www.facebook.com/heatblur/photos/a.683612385159716/754368278084126 - -- AoA=15 Units <==> AoA=10.359 degrees. + -- AoA=15 Units <==> AoA=10.359 degrees. aoaunits=1.089*degrees+3.715 elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then - + ---------- -- A-4E -- ---------- - + -- A-4E source code suggests a simple factor of two as conversion. aoaunits=2*degrees - + end - + return aoaunits end @@ -5441,128 +5465,128 @@ 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 local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B - + -- Return values. local alt local aoa - local dist + local dist local speed - -- Aircraft specific AoA. + -- Aircraft specific AoA. local aoaac=self:_GetAircraftAoA(playerData) - + if step==AIRBOSS.PatternStep.PLATFORM then - + alt=UTILS.FeetToMeters(5000) - + --dist=UTILS.NMToMeters(20) - + speed=UTILS.KnotsToMps(250) - + elseif step==AIRBOSS.PatternStep.ARCIN then - + if tomcat then speed=UTILS.KnotsToMps(150) else speed=UTILS.KnotsToMps(250) - end - + end + elseif step==AIRBOSS.PatternStep.ARCOUT then - + if tomcat then speed=UTILS.KnotsToMps(150) else speed=UTILS.KnotsToMps(250) - end - + end + elseif step==AIRBOSS.PatternStep.DIRTYUP then - - alt=UTILS.FeetToMeters(1200) - + + alt=UTILS.FeetToMeters(1200) + --speed=UTILS.KnotsToMps(250) - + elseif step==AIRBOSS.PatternStep.BULLSEYE then alt=UTILS.FeetToMeters(1200) - + dist=-UTILS.NMToMeters(3) - + aoa=aoaac.OnSpeed - + elseif step==AIRBOSS.PatternStep.INITIAL then - if hornet or tomcat or harrier then + if hornet or tomcat or harrier 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.BREAKENTRY then - if hornet or tomcat or harrier then + if hornet or tomcat or harrier 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.EARLYBREAK then - if hornet or tomcat or harrier then + if hornet or tomcat or harrier then alt=UTILS.FeetToMeters(800) elseif skyhawk then - alt=UTILS.FeetToMeters(600) - end - + alt=UTILS.FeetToMeters(600) + end + elseif step==AIRBOSS.PatternStep.LATEBREAK then - if hornet or tomcat or harrier then + if hornet or tomcat or harrier then alt=UTILS.FeetToMeters(800) elseif skyhawk then - alt=UTILS.FeetToMeters(600) - end - + alt=UTILS.FeetToMeters(600) + end + elseif step==AIRBOSS.PatternStep.ABEAM then - - if hornet or tomcat or harrier then + + if hornet or tomcat or harrier then alt=UTILS.FeetToMeters(600) elseif skyhawk then - alt=UTILS.FeetToMeters(500) + alt=UTILS.FeetToMeters(500) end - + aoa=aoaac.OnSpeed - + if harrier then -- 0.8 to 1.0 NM dist=UTILS.NMToMeters(0.9) else dist=UTILS.NMToMeters(1.2) end - + elseif step==AIRBOSS.PatternStep.NINETY then - if hornet or tomcat then + if hornet or tomcat then alt=UTILS.FeetToMeters(500) elseif skyhawk then alt=UTILS.FeetToMeters(500) elseif harrier then - alt=UTILS.FeetToMeters(425) + alt=UTILS.FeetToMeters(425) end - + aoa=aoaac.OnSpeed - + elseif step==AIRBOSS.PatternStep.WAKE then - - if hornet then + + if hornet then alt=UTILS.FeetToMeters(370) elseif tomcat then alt=UTILS.FeetToMeters(430) -- Tomcat should be a bit higher as it intercepts the GS a bit higher. @@ -5570,12 +5594,12 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) alt=UTILS.FeetToMeters(370) --? end -- Harrier wont get into wake pos. Runway is not angled and it stays port. - + aoa=aoaac.OnSpeed - + elseif step==AIRBOSS.PatternStep.FINAL then - if hornet then + if hornet then alt=UTILS.FeetToMeters(300) elseif tomcat then alt=UTILS.FeetToMeters(360) @@ -5585,9 +5609,9 @@ function AIRBOSS:_GetAircraftParameters(playerData, step) -- 300-325 ft alt=UTILS.FeetToMeters(300) end - + aoa=aoaac.OnSpeed - + end return alt, aoa, dist, speed @@ -5605,19 +5629,19 @@ function AIRBOSS:_GetNextMarshalFight() -- Loop over all marshal flights. for _,_flight in pairs(self.Qmarshal) do local flight=_flight --#AIRBOSS.FlightGroup - + -- Current stack. local stack=flight.flag - + -- Total marshal time in seconds. local Tmarshal=timer.getAbsTime()-flight.time - + -- Min time in marshal stack. local TmarshalMin=2*60 --Two minutes for human players. - if flight.ai then + if flight.ai then TmarshalMin=3*60 -- Three minutes for AI. end - + -- Check if conditions are right. if stack==1 and flight.holding~=nil and Tmarshal>=TmarshalMin then if flight.ai then @@ -5638,7 +5662,7 @@ end --- Check marshal and pattern queues. -- @param #AIRBOSS self function AIRBOSS:_CheckQueue() - + -- Print queues. if self.Debug then self:_PrintQueue(self.flights, "All Flights") @@ -5647,40 +5671,40 @@ function AIRBOSS:_CheckQueue() self:_PrintQueue(self.Qpattern, "Pattern") self:_PrintQueue(self.Qwaiting, "Waiting") self:_PrintQueue(self.Qspinning, "Spinning") - + -- 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. + + -- 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 end -- Check if carrier is currently in recovery mode. if not self:IsRecovering() then - + ----------------------------- -- Switching Recovery Case -- ----------------------------- @@ -5688,84 +5712,84 @@ function AIRBOSS:_CheckQueue() -- Loop over all flights currently in the marshal queue. for _,_flight in pairs(self.Qmarshal) do local flight=_flight --#AIRBOSS.FlightGroup - + -- TODO: In principle this should be done/necessary only if case 1-->2/3 or 2/3-->1, right? -- When recovery switches from 2->3 or 3-->2 nothing changes in the marshal stack. - + -- Check if a change of stack is necessary. if (flight.case==1 and self.case>1) or (flight.case>1 and self.case==1) 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 + break elseif flight.case~=self.case then - + -- This should handle 2-->3 or 3-->2 flight.case=self.case - + end - - end + + end end -- Not recovering ==> skip the rest! return - end - + end + -- Get number of airborne aircraft units(!) currently in pattern. local _,npattern=self:_GetQueueInfo(self.Qpattern) - + -- Get number of aircraft units spinning. local _,nspinning=self:_GetQueueInfo(self.Qspinning) - + -- Get next marshal flight. local marshalflight=self:_GetNextMarshalFight() - + -- Check if there are flights waiting in the Marshal stack and if the pattern is free. No one should be spinning. if marshalflight and npattern0 then - + -- Last flight group send to pattern. local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.FlightGroup - + -- Recovery case of pattern flight. pcase=patternflight.case - + -- Number of airborne aircraft in this group. Count includes section members. local npunits=self:_GetFlightUnits(patternflight, false) - + -- Get time in pattern. Tpattern=timer.getAbsTime()-patternflight.time 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. local TpatternMin if pcase==1 then @@ -5773,13 +5797,13 @@ function AIRBOSS:_CheckQueue() else TpatternMin=2*60*npunits --120*npunits -- 120 seconds interval per plane! end - + -- 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:_ClearForLanding(marshalflight) end - + end end @@ -5792,27 +5816,27 @@ 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) - + -- Cleared for Case X recovery. self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) - + else - + -- Cleared for Case X recovery. if flight.step~=AIRBOSS.PatternStep.COMMENCING then self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) flight.time=timer.getAbsTime() - end + end -- 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 - + end --- Set player step. Any warning is erased and next step hint shown. @@ -5826,20 +5850,20 @@ function AIRBOSS:_SetPlayerStep(playerData, step, delay) -- 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 @@ -5847,67 +5871,67 @@ end --- Scan carrier zone for (new) units. -- @param #AIRBOSS self function AIRBOSS:_ScanCarrierZone() - + -- Carrier position. local coord=self:GetCoordinate() - + -- Scan radius = radius of the CCA. 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(RCCZ, true, false, false) - + -- Make a table with all groups currently in the CCA zone. - local insideCCA={} + local insideCCA={} for _,_unit in pairs(unitscan) do local unit=_unit --Wrapper.Unit#UNIT - + -- Necessary conditions to be met: 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 airborne and closing in. if airborne and inzone and friendly and carrierac 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 - + -- 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 knownflight then - + -- Debug output. self:T2(self.lid..string.format("Known flight group %s of type %s in CCA.", groupname, actype)) - + -- Check if flight is AI and if we want to handle it at all. if knownflight.ai and self.handleai then - + -- Defines if AI group should be handled by the airboss. local iscarriersquad=true - + -- Check if AI group is part of the group set if a set was defined. if self.squadsetAI then local group=self.squadsetAI:FindGroup(groupname) @@ -5926,72 +5950,72 @@ function AIRBOSS:_ScanCarrierZone() end end - + -- Get distance to carrier. local dist=knownflight.group:GetCoordinate():Get2DDistance(self:GetCoordinate()) - + -- 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 AI flight group %s closed in by %.1f NM", knownflight.groupname, UTILS.MetersToNM(closein))) - + -- Is this group the tanker? local istanker=self.tanker and self.tanker.tanker:GetName()==groupname - + -- Is this group the AWACS? local isawacs=self.awacs and self.awacs.tanker:GetName()==groupname - + -- Send tanker to marshal stack? local tanker2marshal = istanker and self.tanker:IsReturning() and self.tanker.airbase:GetName()==self.airbase:GetName() and knownflight.flag==-100 and self.tanker.recovery==true - + -- Send AWACS to marhsal stack? local awacs2marshal = isawacs and self.awacs:IsReturning() and self.awacs.airbase:GetName()==self.airbase:GetName() and knownflight.flag==-100 and self.awacs.recovery==true - + -- Put flight into Marshal. local putintomarshal=closein>UTILS.NMToMeters(5) and knownflight.flag==-100 and iscarriersquad and istanker==false and isawacs==false - + -- Send AI flight to marshal stack if group closes in more than 5 and has initial flag value. if putintomarshal or tanker2marshal or awacs2marshal then - + -- Get the next free stack for current recovery case. local stack=self:_GetFreeStack(knownflight.ai) - -- Repawn. + -- Repawn. local respawn=self.respawnAI --or tanker2marshal - + if stack then - + -- Send AI to marshal stack. We respawn the group to clean possible departure and destination airbases. self:_MarshalAI(knownflight, stack, respawn) - + 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, respawn) -- Group is respawned to clear any attached airfields. end - + end - + -- Break the loop to not have all flights at once! Spams the message screen. break - + end -- Closed in or tanker/AWACS end -- AI - + else - + -- Unknown new AI flight. Create a new flight group. if not self:_IsHuman(group) then self:_CreateFlightGroup(group) end - + end - + end - + -- Find flights that are not in CCA. local remove={} for _,_flight in pairs(self.flights) do @@ -6003,38 +6027,38 @@ function AIRBOSS:_ScanCarrierZone() end end end - + -- Remove flight groups outside CCA. for _,flight in pairs(remove) do self:_RemoveFlightFromQueue(self.flights, flight) end - + end --- 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:_WaitPlayer(playerData) - + -- Check if flight is known to the airboss already. if playerData then -- Number of waiting flights local nwaiting=#self.Qwaiting - + -- Radio message: Stack is full. self:_MarshalCallStackFull(playerData.onboard, nwaiting) -- 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 @@ -6042,9 +6066,9 @@ function AIRBOSS:_WaitPlayer(playerData) flight.time=timer.getAbsTime() flight.warning=nil end - + end - + end @@ -6053,42 +6077,42 @@ end -- @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, stack) - + -- Set step to holding. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.HOLDING) - + -- 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 - + -- XXX: Inform player? Should be done by lead via radio? - + -- Set step. self:_SetPlayerStep(flight, AIRBOSS.PatternStep.HOLDING) - + -- Holding to nil, until arrived. flight.holding=nil - + -- Set case to that of lead. flight.case=playerData.case - - -- Set stack flag. + + -- Set stack flag. flight.flag=stack end - + else - self:E(self.lid.."ERROR: Could not add player to Marshal stack! playerData=nil") - end - + self:E(self.lid.."ERROR: Could not add player to Marshal stack! playerData=nil") + end + end --- Command AI flight to orbit outside the 10 NM zone and wait for a free Marshal stack. @@ -6110,29 +6134,29 @@ function AIRBOSS:_WaitAI(flight, respawn) -- 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) @@ -6141,40 +6165,40 @@ function AIRBOSS:_WaitAI(flight, respawn) -- 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 - + if respawn then - - -- This should clear the landing waypoints. + + -- This should clear the landing waypoints. -- Note: This resets the weapons and the fuel state. But not the units fortunately. -- Get group template. local Template=group:GetTemplate() - + -- Set route points. Template.route.points=wp - + -- Respawn the group. - group=group:Respawn(Template, true) - + group=group:Respawn(Template, true) + end - + -- Reinit waypoints. group:WayPointInitialize(wp) - + -- Route group. group:Route(wp, 1) - + 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. @@ -6197,13 +6221,13 @@ function AIRBOSS:_MarshalAI(flight, nstack, respawn) self:E(self.lid.."ERROR: cannot get coordinate of flight group.") return end - + -- 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 - + -- Explode unit for testing. Worked! --local u1=flight.group:GetUnit(1) --Wrapper.Unit#UNIT --u1:Explode(500, 10) @@ -6217,129 +6241,129 @@ function AIRBOSS:_MarshalAI(flight, nstack, respawn) -- Flight group name. local group=flight.group local groupname=flight.groupname - + -- Set new stack. flight.flag=nstack - + -- Current carrier position. local Carrier=self:GetCoordinate() - + -- Carrier heading. local hdg=self:GetHeading() - + -- 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) - + local altitude local p0 --Core.Point#COORDINATE local p1 --Core.Point#COORDINATE local p2 --Core.Point#COORDINATE - - -- Get altitude and positions. + + -- Get altitude and positions. altitude, p1, p2=self:_GetMarshalAltitude(nstack, case) - + -- Waypoints array to be filled depending on case etc. local wp={} - + -- If flight has not arrived in the holding zone, we guide it there. if not flight.holding then - + ---------------------- -- Route to Holding -- ---------------------- - + -- Debug info. 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") - + -- Task function when arriving at the holding zone. This will set flight.holding=true. local TaskArrivedHolding=flight.group:TaskFunction("AIRBOSS._ReachedHoldingZone", self, flight) - + -- Select case. if case==1 then - + -- Initial point 7 NM and a bit port of carrier. 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), hdg-135):SetAltitude(altitude) - + -- 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=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, true) - + -- 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 -- + -- In Marshal Pattern -- ------------------------ -- 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. 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(), true) - + 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 - + if respawn then - - -- This should clear the landing waypoints. + + -- This should clear the landing waypoints. -- Note: This resets the weapons and the fuel state. But not the units fortunately. -- Get group template. local Template=group:GetTemplate() - + -- Set route points. Template.route.points=wp - + -- Respawn the group. flight.group=group:Respawn(Template, true) - + end - + -- Reinit waypoints. flight.group:WayPointInitialize(wp) - + -- Route group. flight.group:Route(wp, 1) - + end --- Tell AI to refuel. Either at the recovery tanker or at the nearest divert airfield. @@ -6355,11 +6379,11 @@ function AIRBOSS:_RefuelAI(flight) -- Current positon. wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") - + -- Check if aircraft can be refueled. -- TODO: This should also depend on the tanker type AC. local refuelac=false - local actype=flight.group:GetTypeName() + local actype=flight.group:GetTypeName() if actype==AIRBOSS.AircraftCarrier.AV8B or actype==AIRBOSS.AircraftCarrier.F14A or actype==AIRBOSS.AircraftCarrier.F14B or @@ -6370,79 +6394,79 @@ function AIRBOSS:_RefuelAI(flight) actype==AIRBOSS.AircraftCarrier.S3BTANKER then refuelac=true end - + -- Message. - local text="" - + local text="" + -- Refuel or divert? if self.tanker and refuelac then - + -- Current Tanker position. local tankerpos=self.tanker.tanker:GetCoordinate() - + -- Task refueling. local TaskRefuel=flight.group:TaskRefueling() -- Task to go back to Marshal. local TaskMarshal=flight.group:TaskFunction("AIRBOSS._TaskFunctionMarshalAI", self, flight) - + -- Waypoint with tasks. wp[#wp+1]=tankerpos:WaypointAirTurningPoint(nil, CurrentSpeed, {TaskRefuel, TaskMarshal}, "Refueling") - + -- Marshal Message. self:_MarshalCallGasAtTanker(flight.onboard) - + else - + ------------------------------ -- Guide AI to divert field -- ------------------------------ - + -- Closest Airfield of the coaliton. local divertfield=self:GetCoordinate():GetClosestAirbase(Airbase.Category.AIRDROME, self:GetCoalition()) - + -- Handle case where there is no divert field of the own coalition and try neutral instead. if divertfield==nil then divertfield=self:GetCoordinate():GetClosestAirbase(Airbase.Category.AIRDROME, 0) end - + if divertfield then - + -- Coordinate. local divertcoord=divertfield:GetCoordinate() - + -- Landing waypoint. wp[#wp+1]=divertcoord:WaypointAirLanding(UTILS.KnotsToKmph(300), divertfield, {}, "Divert Field") - + -- Marshal Message. self:_MarshalCallGasAtDivert(flight.onboard, divertfield:GetName()) - + -- Respawn! - + -- Get group template. local Template=flight.group:GetTemplate() - + -- Set route points. Template.route.points=wp - + -- Respawn the group. - flight.group=flight.group:Respawn(Template, true) - + flight.group=flight.group:Respawn(Template, true) + else -- Set flight to refueling so this is not called again. self:E(self.lid..string.format("WARNING: No recovery tanker or divert field available for group %s.", flight.groupname)) flight.refueling=true return end - + end - + -- Reinit waypoints. flight.group:WayPointInitialize(wp) - + -- Route group. flight.group:Route(wp, 1) - + -- Set refueling switch. flight.refueling=true @@ -6455,13 +6479,13 @@ function AIRBOSS:_LandAI(flight) -- Debug info. 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! + -- Unfortunately, the correct speed depends on the aircraft type! -- Aircraft speed when flying the pattern. 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 @@ -6471,34 +6495,34 @@ function AIRBOSS:_LandAI(flight) elseif flight.actype==AIRBOSS.AircraftCarrier.S3B or flight.actype==AIRBOSS.AircraftCarrier.S3BTANKER then Speed=UTILS.KnotsToKmph(140) end - + -- Carrier position. local Carrier=self:GetCoordinate() - + -- Carrier heading. local hdg=self:GetHeading() -- Waypoints array. local wp={} - + local CurrentSpeed=flight.group:GetVelocityKMH() -- Current positon. wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") - + -- 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]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLanding(Speed, self.airbase, nil, "Landing") --wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLandingReFu(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) - + -- Route group. flight.group:Route(wp, 0) end @@ -6516,13 +6540,13 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) if stack<=0 then return 0,nil,nil end - + -- Recovery case. case=case or self.case -- Carrier position. local Carrier=self:GetCoordinate() - + -- Altitude of first stack. Depends on recovery case. local angels0 local Dist @@ -6531,63 +6555,63 @@ function AIRBOSS:_GetMarshalAltitude(stack, case) -- Stack number. local nstack=stack-1 - + 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 - + -- Get true heading of carrier. local hdg=self.carrier:GetHeading() - + -- For CCW pattern: First point astern, second ahead of the carrier. - + -- First point over carrier. p1=Carrier - + -- Second point 1.5 NM ahead. p2=Carrier:Translate(UTILS.NMToMeters(1.5), hdg) - + -- Tarawa Delta pattern. if self.carriertype==AIRBOSS.CarrierType.TARAWA then - + -- Pattern is directly overhead the carrier. p1=Carrier:Translate(UTILS.NMToMeters(1.0), hdg+90) p2=p1:Translate(2.5, hdg) - + end - + else - + -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. angels0=6 - + -- 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. local radial=self:GetRadial(case, false, true) - + -- For CCW pattern: p1 further astern than p2. - + -- Length of the race track pattern. local l=UTILS.NMToMeters(10) - + -- First point of race track pattern. p1=Carrier:Translate(Dist+l, radial) - + -- Second point. p2=Carrier:Translate(Dist, radial) - + end -- Pattern altitude. local altitude=UTILS.FeetToMeters((nstack+angels0)*1000) - + -- Set altitude of coordinate. p1:SetAltitude(altitude, true) p2:SetAltitude(altitude, true) - + return altitude, p1, p2 end @@ -6599,18 +6623,18 @@ function AIRBOSS:_GetCharlieTime(flightgroup) -- Get current stack of player. local stack=flightgroup.flag - + -- 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. @@ -6618,72 +6642,72 @@ function AIRBOSS:_GetCharlieTime(flightgroup) 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 - + -- 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. - + -- 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() - + -- 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 @@ -6695,37 +6719,37 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) -- Set flag value. This corresponds to the stack number which starts at 1. flight.flag=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()) - -- Stack altitude. + -- Stack altitude. --local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) local alt=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) - + -- Convert to clock string. local Ccharlie=UTILS.SecondsToClock(flight.Tcharlie) - + -- Combined marshal call. self:_MarshalCallArrived(flight.onboard, flight.case, brc, alt, Ccharlie, 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. @@ -6737,7 +6761,7 @@ function AIRBOSS:_AddMarshalGroup(flight, stack) local text=string.format("Select TACAN %03d°, channel %d%s (%s)", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) self:MessageToPlayer(flight, text, nil, "") end - + end --- Collapse marshal stack. @@ -6749,108 +6773,108 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- Recovery case of flight. local case=flight.case - + -- Stack of flight. local stack=flight.flag - + -- 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() -- Decrease flag values of all flight groups in marshal stack. for _,_flight in pairs(self.Qmarshal) do local mflight=_flight --#AIRBOSS.PlayerData - + -- 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. local mstack=mflight.flag - + -- Only collapse stacks above the new pattern flight. if mstack>stack then - + -- TODO: Is this now right as we allow more flights per stack? -- 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) - + -- Free stack has to be below. if newstack and newstack %d.", mflight.groupname, mflight.case, mstack, newstack)) - + if mflight.ai 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=newstack - + -- Angels of new stack. local angels=self:_GetAngels(self:_GetMarshalAltitude(newstack, case)) - + -- Inform players. if mflight.difficulty~=AIRBOSS.Difficulty.HARD then - - -- Send message to all non-pros that they can descent. + + -- 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=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 - end + end end - - + + if nopattern then - + -- Debug message. self:T(self.lid..string.format("Flight %s is leaving stack but not going to pattern.", flight.groupname)) - + 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)) - + -- Add flight to pattern queue. self:_AddFlightToPatternQueue(flight) - + end -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42). @@ -6858,7 +6882,7 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) -- New time stamp for time in pattern. flight.time=timer.getAbsTime() - + end --- Get next free Marshal stack. Depending on AI/human and recovery case. @@ -6868,7 +6892,7 @@ end -- @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 @@ -6877,23 +6901,23 @@ function AIRBOSS:_GetFreeStack(ai, case, empty) 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 - + if n>0 then if flight.ai or flight.case>1 then stack[n]=0 -- AI get one stack on their own. Also CASE II/III get one stack each. @@ -6903,10 +6927,10 @@ function AIRBOSS:_GetFreeStack(ai, case, empty) 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 @@ -6925,8 +6949,8 @@ function AIRBOSS:_GetFreeStack(ai, case, empty) end end end - - return nfree + + return nfree end --- Get number of (airborne) units in a flight. @@ -6937,7 +6961,7 @@ end -- @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 @@ -6969,14 +6993,14 @@ function AIRBOSS:_GetFlightUnits(flight, onground) 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 + local secflight=sec --#AIRBOSS.PlayerData -- Count alive units in air. nsection=nsection+countunits(secflight.group, inair) end @@ -6994,52 +7018,52 @@ 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.FlightGroup - + -- 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 - + -- 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 - + else - + --------------------------------------------------------------------------- -- No specific case requested. Count all groups & units in selected queue. --------------------------------------------------------------------------- - + -- 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 @@ -7060,10 +7084,10 @@ function AIRBOSS:_PrintQueue(queue, name) else for i,_flight in pairs(queue) do local flight=_flight --#AIRBOSS.FlightGroup - + local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) local case=flight.case - local stack=flight.flag + local stack=flight.flag local fuel=flight.group:GetFuelMin()*100 local ai=tostring(flight.ai) local lead=flight.seclead @@ -7071,10 +7095,10 @@ function AIRBOSS:_PrintQueue(queue, name) local actype=self:_GetACNickname(flight.actype) local onboard=flight.onboard local holding=tostring(flight.holding) - + -- Airborne units. local _, nunits, nsec=self:_GetFlightUnits(flight, false) - + -- Text. 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) @@ -7084,7 +7108,7 @@ function AIRBOSS:_PrintQueue(queue, name) end for j,_element in pairs(flight.elements) do local element=_element --#AIRBOSS.FlightElement - text=text..string.format("\n (%d) %s (%s): ai=%s, ballcall=%s, recovered=%s", + text=text..string.format("\n (%d) %s (%s): ai=%s, ballcall=%s, recovered=%s", j, element.onboard, element.unitname, tostring(element.ai), tostring(element.ballcall), tostring(element.recovered)) end end @@ -7104,18 +7128,18 @@ function AIRBOSS:_CreateFlightGroup(group) -- Debug info. 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 - + -- Check if not already in flights if not self:_InQueue(self.flights, group) then -- Flight group name local groupname=group:GetName() local human, playername=self:_IsHuman(group) - - -- Queue table item. + + -- Queue table item. flight.group=group flight.groupname=group:GetName() flight.nunits=#group:GetUnits() @@ -7131,10 +7155,10 @@ function AIRBOSS:_CreateFlightGroup(group) flight.refueling=false flight.holding=nil flight.name=flight.group:GetUnit(1):GetName() --Will be overwritten in _Newplayer with player name if human player in the group. - + -- Note, this should be re-set elsewhere! flight.case=self.case - + -- Flight elements. local text=string.format("Flight elements of group %s:", flight.groupname) flight.elements={} @@ -7151,8 +7175,8 @@ function AIRBOSS:_CreateFlightGroup(group) text=text..string.format("\n[%d] %s onboard #%s, AI=%s", i, element.unitname, tostring(element.onboard), tostring(element.ai)) table.insert(flight.elements, element) end - self:T(self.lid..text) - + self:T(self.lid..text) + -- Onboard if flight.ai then local onboard=flight.onboardnumbers[flight.seclead] @@ -7160,10 +7184,10 @@ function AIRBOSS:_CreateFlightGroup(group) else flight.onboard=self:_GetOnboardNumberPlayer(group) end - + -- Add to known flights. table.insert(self.flights, flight) - + else self:E(self.lid..string.format("ERROR: Flight group %s already exists in self.flights!", group:GetName())) return nil @@ -7181,21 +7205,21 @@ function AIRBOSS:_NewPlayer(unitname) -- Get player unit and name. local playerunit, playername=self:_GetPlayerUnitAndName(unitname) - + if playerunit and playername then - + -- Get group. local group=playerunit:GetGroup() -- Player data. local playerData --#AIRBOSS.PlayerData - + -- Create a flight group for the player. playerData=self:_CreateFlightGroup(group) - + -- Nil check. if playerData then - + -- Player unit, client and callsign. playerData.unit = playerunit playerData.unitname = unitname @@ -7203,32 +7227,32 @@ function AIRBOSS:_NewPlayer(unitname) playerData.callsign = playerData.unit:GetCallsign() playerData.client = CLIENT:FindByName(unitname, nil, true) playerData.seclead = playername - + -- Number of passes done by player in this slot. playerData.passes=0 --playerData.passes or 0 - + -- Messages for player. playerData.messages={} - + -- Debriefing tables. playerData.lastdebrief=playerData.lastdebrief or {} - + -- Attitude monitor. playerData.attitudemonitor=false - + -- Trap sheet save. if playerData.trapon==nil then playerData.trapon=self.trapsheet end - + -- Set difficulty level. playerData.difficulty=playerData.difficulty or self.defaultskill - + -- Subtitles of player. if playerData.subtitles==nil then playerData.subtitles=true end - + -- Show step hints. if playerData.showhints==nil then if playerData.difficulty==AIRBOSS.Difficulty.HARD then @@ -7237,30 +7261,30 @@ function AIRBOSS:_NewPlayer(unitname) playerData.showhints=true end end - + -- Points rewarded. playerData.points={} - + -- Init stuff for this round. playerData=self:_InitPlayer(playerData) - + -- Init player data. self.players[playername]=playerData - + -- Init player grades table if necessary. self.playerscores[playername]=self.playerscores[playername] or {} - + -- Welcome player message. if self.welcome then self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), string.format("AIRBOSS %s", self.alias), "", 5) end - + end - + -- Return player data table. - return playerData + return playerData end - + return nil end @@ -7271,7 +7295,7 @@ end -- @return #AIRBOSS.PlayerData Initialized player data. 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.groove={} playerData.debrief={} @@ -7292,7 +7316,7 @@ function AIRBOSS:_InitPlayer(playerData, step) playerData.TIG0=nil playerData.wire=nil playerData.flag=-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 self:MessageToPlayer(playerData, "Group name contains \"Groove\". Happy groove testing.") @@ -7301,12 +7325,12 @@ function AIRBOSS:_InitPlayer(playerData, step) self:_AddFlightToPatternQueue(playerData) self.dTstatus=0.1 end - + return playerData end ---- Get flight from group in a queue. +--- Get flight from group in a queue. -- @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. @@ -7318,24 +7342,24 @@ 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.FlightGroup - + 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)) + + self:T2(self.lid..string.format("WARNING: Flight group %s could not be found in queue.", name)) end - + self:T2(self.lid..string.format("WARNING: Flight group could not be found in queue. Group is nil!")) return nil, nil end ---- Get element in flight. +--- Get element in flight. -- @param #AIRBOSS self -- @param #string unitname Name of the unit. -- @return #AIRBOSS.FlightElement Element of the flight or nil. @@ -7345,33 +7369,33 @@ function AIRBOSS:_GetFlightElement(unitname) -- Get the unit. local unit=UNIT:FindByName(unitname) - + -- Check if unit exists. if unit then - + -- Get flight element from all flights. local flight=self:_GetFlightFromGroupInQueue(unit:GetGroup(), self.flights) - + -- Check if fight exists. if flight then -- Loop over all elements in flight group. for i,_element in pairs(flight.elements) do local element=_element --#AIRBOSS.FlightElement - + if element.unit:GetName()==unitname then return element, i, flight end end - + self:T2(self.lid..string.format("WARNING: Flight element %s could not be found in flight group.", unitname, flight.groupname)) end end - + return nil, nil, nil end ---- Get element in flight. +--- Get element in flight. -- @param #AIRBOSS self -- @param #string unitname Name of the unit. -- @return #boolean If true, element could be removed or nil otherwise. @@ -7411,13 +7435,13 @@ end -- @return #AIRBOSS.FlightGroup Flight group. function AIRBOSS:_RemoveDeadFlightGroups() - -- Remove dead flights from all flights table. + -- 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) - end + end end -- Remove dead flights from Marhal queue table. @@ -7427,7 +7451,7 @@ function AIRBOSS:_RemoveDeadFlightGroups() self:T(string.format("Removing dead flight group %s from Marshal Queue table.", flight.groupname)) table.remove(self.Qmarshal, i) end - end + end -- Remove dead flights from Pattern queue table. for i=#self.Qpattern,1,-1 do @@ -7436,8 +7460,8 @@ function AIRBOSS:_RemoveDeadFlightGroups() self:T(string.format("Removing dead flight group %s from Pattern Queue table.", flight.groupname)) table.remove(self.Qpattern, i) end - end - + end + end --- Get the lead flight group of a flight group. @@ -7448,12 +7472,12 @@ function AIRBOSS:_GetLeadFlight(flight) -- Init. local lead=flight - + -- Only human players can be section leads of other players. if flight.name~=flight.seclead then lead=self.players[flight.seclead] end - + return lead end @@ -7479,11 +7503,11 @@ function AIRBOSS:_CheckSectionRecovered(flight) return false end end - + -- Now check all section members, if any. for _,_section in pairs(lead.section) do local sectionmember=_section --#AIRBOSS.FlightGroup - + -- Check all elements of the secmember flight group. for _,_element in pairs(sectionmember.elements) do local element=_element --#AIRBOSS.FlightElement @@ -7492,10 +7516,10 @@ function AIRBOSS:_CheckSectionRecovered(flight) end end end - + -- Remove lead flight from pattern queue. It is this flight who is added to the queue. self:_RemoveFlightFromQueue(self.Qpattern, lead) - + -- Just for now, check if it is in other queues as well. if self:_InQueue(self.Qmarshal, lead.group) then self:E(self.lid..string.format("ERROR: lead flight group %s should not be in marshal queue", lead.groupname)) @@ -7506,7 +7530,7 @@ function AIRBOSS:_CheckSectionRecovered(flight) self:E(self.lid..string.format("ERROR: lead flight group %s should not be in pattern queue", lead.groupname)) self:_RemoveFlightFromQueue(self.Qwaiting, lead) end - + return true end @@ -7517,18 +7541,18 @@ function AIRBOSS:_AddFlightToPatternQueue(flight) -- Add flight to table. table.insert(self.Qpattern, flight) - + -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42). flight.flag=-1 -- New time stamp for time in pattern. flight.time=timer.getAbsTime() - + -- Init recovered switch. flight.recovered=false for _,elem in pairs(flight.elements) do elem.recoverd=false end - + -- Set recovered for all section members. for _,sec in pairs(flight.section) do -- Set flag and timestamp for section members @@ -7548,12 +7572,12 @@ function AIRBOSS:_RecoveredElement(unit) -- Get element of flight. local element, idx, flight=self:_GetFlightElement(unit:GetName()) --#AIRBOSS.FlightElement - + -- Nil check. Could be if a helo landed or something else we dont know! if element then element.recovered=true end - + return flight end @@ -7567,44 +7591,44 @@ function AIRBOSS:_RemoveFlightFromMarshalQueue(flight, nopattern) -- Remove flight from marshal queue if it is in. local removed, idx=self:_RemoveFlightFromQueue(self.Qmarshal, flight) - + -- 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. + + -- 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 end - + return removed, idx end @@ -7619,7 +7643,7 @@ function AIRBOSS:_RemoveFlightFromQueue(queue, flight) -- Loop over all flights in group. for i,_flight in pairs(queue) do local qflight=_flight --#AIRBOSS.FlightGroup - + -- Check for name. if qflight.groupname==flight.groupname then self:T(self.lid..string.format("Removing flight group %s from queue.", flight.groupname)) @@ -7627,7 +7651,7 @@ function AIRBOSS:_RemoveFlightFromQueue(queue, flight) return true, i end end - + return false, nil end @@ -7641,41 +7665,41 @@ function AIRBOSS:_RemoveUnitFromFlight(unit) -- Get group. local group=unit:GetGroup() - + -- Check if group exists. if group then - + -- Get flight. local flight=self:_GetFlightFromGroupInQueue(group, self.flights) - + -- Check if flight exists. if flight then -- Remove element from flight group. local removed=self:_RemoveFlightElement(unit:GetName()) - + if removed then - + -- Get number of units (excluding section members). For AI only those that are still in air as we assume once they landed, they are out of the game. local _,nunits=self:_GetFlightUnits(flight, not flight.ai) - + -- Number of flight elements still left. local nelements=#flight.elements - + -- Debug info. self:T(self.lid..string.format("Removed unit %s: nunits=%d, nelements=%d", unit:GetName(), nunits, nelements)) - + -- Check if no units are left. if nunits==0 or nelements==0 then -- Remove flight from all queues. self:_RemoveFlight(flight) end - - end - end + + end + end end end - + end --- Remove a flight, which is a member of a section, from this section. @@ -7698,7 +7722,7 @@ function AIRBOSS:_RemoveFlightFromSection(flight) end end end - + end --- Update section if a flight is removed. @@ -7714,71 +7738,71 @@ function AIRBOSS:_UpdateFlightSection(flight) -------------------- -- Section Leader -- -------------------- - + -- This player is the leader ==> We need a new one. if #flight.section>=1 then - + -- New leader. local newlead=flight.section[1] --#AIRBOSS.FlightGroup newlead.seclead=newlead.name - + -- Adjust new section members. for i=2,#flight.section do local member=flight.section[i] --#AIRBOSS.FlightGroup - - -- Add remaining members new leaders table. + + -- Add remaining members new leaders table. table.insert(newlead.section, member) - + -- Set new section lead of member. member.seclead=newlead.name end - + end - + -- Flight section empty flight.section={} - + else - + -------------------- -- Section Member -- -------------------- - + -- Remove flight from its leaders section. self:_RemoveFlightFromSection(flight) - + end end ---- 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. +--- 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. -- @param #boolean completely If true, also remove human flight from all flights table. function AIRBOSS:_RemoveFlight(flight, completely) self:F(self.lid.. string.format("Removing flight %s, ai=%s completely=%s.", tostring(flight.groupname), tostring(flight.ai), tostring(completely))) - + -- Remove flight from all queues. self:_RemoveFlightFromMarshalQueue(flight, true) self:_RemoveFlightFromQueue(self.Qpattern, flight) self:_RemoveFlightFromQueue(self.Qwaiting, flight) self:_RemoveFlightFromQueue(self.Qspinning, flight) - + -- Check if player or AI - if flight.ai then - + if flight.ai then + -- Remove AI flight completely. Pure AI flights have no sections and cannot be members. self:_RemoveFlightFromQueue(self.flights, flight) - + else - + -- Check if flight should be completely removed, e.g. after the player died or simply left the slot. if completely then -- Update flight section. Remove flight from section or find new section leader if flight was the lead. self:_UpdateFlightSection(flight) - + -- Remove completely. self:_RemoveFlightFromQueue(self.flights, flight) @@ -7789,37 +7813,37 @@ function AIRBOSS:_RemoveFlight(flight, completely) table.remove(grades, #grades) end end - - + + -- Remove player from players table. local playerdata=self.players[flight.name] if playerdata then self:I(self.lid..string.format("Removing player %s completely.", flight.name)) self.players[flight.name]=nil end - + -- Remove flight. flight=nil - + else - + -- Set player step to undefined. self:_SetPlayerStep(flight, AIRBOSS.PatternStep.UNDEFINED) - + -- Also set this for the section members as they are in the same boat. for _,sectionmember in pairs(flight.section) do self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.UNDEFINED) -- Also remove section member in case they are in the spinning queue. self:_RemoveFlightFromQueue(self.Qspinning, sectionmember) end - + -- What if flight is member of a section. His status is now undefined. Should he be removed from the section? -- I think yes, if he pulls the trigger. self:_RemoveFlightFromSection(flight) - + end end - + end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -7831,142 +7855,142 @@ end function AIRBOSS:_CheckPlayerStatus() -- Loop over all players. - for _playerName,_playerData in pairs(self.players) do + for _playerName,_playerData in pairs(self.players) do local playerData=_playerData --#AIRBOSS.PlayerData - + if playerData then - + -- Player unit. local unit=playerData.unit - + -- Check if unit is alive. if unit and unit:IsAlive() then -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). -- TODO: This might cause problems if the CCA is set to be very small! if unit:IsInZone(self.zoneCCA) then - + -- Display aircraft attitude and other parameters as message text. if playerData.attitudemonitor then self:_AttitudeMonitor(playerData) - end - + end + -- Check distance to other flights. self:_CheckPlayerPatternDistance(playerData) - + -- Foul deck check. self:_CheckFoulDeck(playerData) - -- Check current step. + -- Check current step. 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)) elseif playerData.step==AIRBOSS.PatternStep.REFUELING then - + -- Nothing to do here at the moment. - + elseif playerData.step==AIRBOSS.PatternStep.SPINNING then - + -- Player is spinning. self:_Spinning(playerData) elseif playerData.step==AIRBOSS.PatternStep.HOLDING then - + -- 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) - + self:_Waiting(playerData) + elseif playerData.step==AIRBOSS.PatternStep.COMMENCING then - + -- CASE I/II/III: New approach. self:_Commencing(playerData, true) - + elseif playerData.step==AIRBOSS.PatternStep.BOLTER then - + -- CASE I/II/III: Bolter pattern. self:_BolterPattern(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) - + 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 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 - + -- CASE I/II: Player is at the initial position entering the landing pattern. self:_Initial(playerData) - + elseif playerData.step==AIRBOSS.PatternStep.BREAKENTRY then - + -- CASE I/II: Break entry. self:_BreakEntry(playerData) - + elseif playerData.step==AIRBOSS.PatternStep.EARLYBREAK then - + -- CASE I/II: Early break. self:_Break(playerData, AIRBOSS.PatternStep.EARLYBREAK) - + elseif playerData.step==AIRBOSS.PatternStep.LATEBREAK then - + -- CASE I/II: Late break. self:_Break(playerData, AIRBOSS.PatternStep.LATEBREAK) - + elseif playerData.step==AIRBOSS.PatternStep.ABEAM then - + -- CASE I/II: Abeam position. self:_Abeam(playerData) - + elseif playerData.step==AIRBOSS.PatternStep.NINETY then - + -- CASE:I/II: Check long down wind leg. self:_CheckForLongDownwind(playerData) - + -- At the ninety. self:_Ninety(playerData) - + elseif playerData.step==AIRBOSS.PatternStep.WAKE then - + -- CASE I/II: In the wake. self:_Wake(playerData) - + elseif playerData.step==AIRBOSS.PatternStep.EMERGENCY then - + -- Emergency landing. Player pos is not checked. self:_Final(playerData, true) - + elseif playerData.step==AIRBOSS.PatternStep.FINAL then - + -- CASE I/II: Turn to final and enter the groove. self:_Final(playerData) - + elseif playerData.step==AIRBOSS.PatternStep.GROOVE_XX or playerData.step==AIRBOSS.PatternStep.GROOVE_IM or playerData.step==AIRBOSS.PatternStep.GROOVE_IC or @@ -7974,39 +7998,39 @@ function AIRBOSS:_CheckPlayerStatus() playerData.step==AIRBOSS.PatternStep.GROOVE_AL or playerData.step==AIRBOSS.PatternStep.GROOVE_LC or playerData.step==AIRBOSS.PatternStep.GROOVE_IW then - + -- CASE I/II: In the groove. self:_Groove(playerData) - + elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then - + -- 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:T2(self.lid.."WARNING: Player unit not inside the CCA!") end - + else -- Unit not alive. self:T(self.lid.."WARNING: Player unit is not alive!") end end end - + end @@ -8019,42 +8043,42 @@ function AIRBOSS:_CheckMissedStepOnEntry(playerData) local rightcase=playerData.case>1 local rightqueue=self:_InQueue(self.Qpattern, playerData.group) local rightflag=playerData.flag~=-42 - + -- 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 - + -- 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 @@ -8062,11 +8086,11 @@ function AIRBOSS:_CheckMissedStepOnEntry(playerData) -- 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=-42 - end - end + end + end end end @@ -8074,7 +8098,7 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data. function AIRBOSS:_SetTimeInGroove(playerData) - + -- Set time in the groove if playerData.TIG0 then playerData.Tgroove=timer.getTime()-playerData.TIG0 @@ -8092,7 +8116,7 @@ function AIRBOSS:_GetTimeInGroove(playerData) local Tgroove=999 - -- Get time in the groove. + -- Get time in the groove. if playerData.TIG0 then Tgroove=timer.getTime()-playerData.TIG0 end @@ -8110,7 +8134,7 @@ end -- @param Core.Event#EVENTDATA EventData function AIRBOSS:OnEventBirth(EventData) self:F3({eventbirth = EventData}) - + -- Nil checks. if EventData==nil then self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") @@ -8121,26 +8145,26 @@ function AIRBOSS:OnEventBirth(EventData) self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") self:E(EventData) return - end - + end + local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - + self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) self:T(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("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) - + -- Check if aircraft type the player occupies is carrier capable. local rightaircraft=self:_IsCarrierAircraft(_unit) if rightaircraft==false then @@ -8149,7 +8173,7 @@ 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.") @@ -8157,14 +8181,14 @@ function AIRBOSS:OnEventBirth(EventData) self:T(self.lid..text) return end - + -- Add Menu commands. self:_AddF10Commands(_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) - - end + + end end --- Airboss event handler for event land. @@ -8172,7 +8196,7 @@ end -- @param Core.Event#EVENTDATA EventData function AIRBOSS:OnEventLand(EventData) self:F3({eventland = EventData}) - + -- Nil checks. if EventData==nil then self:E(self.lid.."ERROR: EventData=nil in event LAND!") @@ -8184,143 +8208,143 @@ function AIRBOSS:OnEventLand(EventData) self:E(EventData) return end - + -- Get unit name that landed. local _unitName=EventData.IniUnitName - + -- Check if this was a player. local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - + -- Debug output. self:T(self.lid.."LAND: unit = "..tostring(EventData.IniUnitName)) self:T(self.lid.."LAND: group = "..tostring(EventData.IniGroupName)) self:T(self.lid.."LAND: player = "..tostring(_playername)) - + -- 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. if airbasename==self.airbase:GetName() then - + -- 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. if _unit and _playername then - + ------------------------- -- Human Player landed -- ------------------------- - + -- Get info. 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 at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) self:T(self.lid..text) MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.Debug) - + -- Player data. local playerData=self.players[_playername] --#AIRBOSS.PlayerData - + -- Check if playerData is okay. if playerData==nil then self:E(self.lid..string.format("ERROR: playerData nil in landing event. unit=%s player=%s", tostring(_unitName), tostring(_playername))) 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) - + -- Clear queues just in case. self:_RemoveFlightFromMarshalQueue(playerData, true) self:_RemoveFlightFromQueue(self.Qpattern, playerData) self:_RemoveFlightFromQueue(self.Qwaiting, playerData) self:_RemoveFlightFromQueue(self.Qspinning, playerData) - + -- Reinitialize player data. - self:_InitPlayer(playerData) - + self:_InitPlayer(playerData) + return end - + -- 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)) - + else - + -- We did land. playerData.landed=true - + -- Switch attitude monitor off if on. playerData.attitudemonitor=false - + -- 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. if self.Debug and false then -- Debug mark of player landing coord. local lp=coord:MarkToAll("Landing coord.") - coord:SmokeGreen() + coord:SmokeGreen() end - + -- Set time in the groove of player. self:_SetTimeInGroove(playerData) - + -- Debug text. local text=string.format("Player %s AC type %s landed at dist=%.1f m. Tgroove=%.1f sec.", playerData.name, playerData.actype, dist, self:_GetTimeInGroove(playerData)) text=text..string.format(" X=%.1f m, Z=%.1f m, rho=%.1f m.", X, Z, rho) - self:T(self.lid..text) - + self:T(self.lid..text) + -- Check carrier type. if self.carriertype==AIRBOSS.CarrierType.TARAWA then - + -- Power "Idle". self:RadioTransmission(self.LSORadio, self.LSOCall.IDLE, false, 1, nil, true) - + -- Next step debrief. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) - + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) + else - + -- Next step undefined until we know more. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.UNDEFINED) - + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.UNDEFINED) + -- Call trapped function in 1 second to make sure we did not bolter. SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1) - + end - + end - + else -- Handle case where player did not land on the carrier. -- Well, I guess, he leaves the slot or ejects. Both should be handled. @@ -8328,41 +8352,41 @@ function AIRBOSS:OnEventLand(EventData) 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 - + -------------------- -- AI unit landed -- -------------------- - + if self.carriertype~=AIRBOSS.CarrierType.TARAWA then - + -- 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(coord, 0) - + -- Aircraft type. local _type=EventData.IniUnit:GetTypeName() - + -- Debug 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) end - + -- AI always lands ==> remove unit from flight group and queues. local flight=self:_RecoveredElement(EventData.IniUnit) - + -- Check if all were recovered. If so update pattern queue. self:_CheckSectionRecovered(flight) - + end - end + end end --- Airboss event handler for event that a unit shuts down its engines. @@ -8370,7 +8394,7 @@ end -- @param Core.Event#EVENTDATA EventData function AIRBOSS:OnEventEngineShutdown(EventData) self:F3({eventengineshutdown=EventData}) - + -- Nil checks. if EventData==nil then self:E(self.lid.."ERROR: EventData=nil in event ENGINESHUTDOWN!") @@ -8386,48 +8410,48 @@ function AIRBOSS:OnEventEngineShutdown(EventData) local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - + self:T3(self.lid.."ENGINESHUTDOWN: unit = "..tostring(EventData.IniUnitName)) self:T3(self.lid.."ENGINESHUTDOWN: group = "..tostring(EventData.IniGroupName)) self:T3(self.lid.."ENGINESHUTDOWN: player = "..tostring(_playername)) - + if _unit and _playername then - + -- Debug message. self:T(self.lid..string.format("Player %s shut down its engines!",_playername)) - + else - + -- Debug message. self:T(self.lid..string.format("AI unit %s shut down its engines!", _unitName)) - + -- Get flight. local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) - + -- Only AI flights. if flight and flight.ai then - + -- Check if all elements were recovered. local recovered=self:_CheckSectionRecovered(flight) - + -- Despawn group and completely remove flight. if recovered then self:T(self.lid..string.format("AI group %s completely recovered. Despawning group after engine shutdown event as requested in 5 seconds.", tostring(EventData.IniGroupName))) - + -- Remove flight. self:_RemoveFlight(flight) - + -- Check if this is a tanker or AWACS associated with the carrier. local istanker=self.tanker and self.tanker.tanker:GetName()==EventData.IniGroupName local isawacs=self.awacs and self.awacs.tanker:GetName()==EventData.IniGroupName - + -- Destroy group if desired. Recovery tankers have their own logic for despawning. if self.despawnshutdown and not (istanker or isawacs) then EventData.IniGroup:Destroy(nil, 5) end - + end - + end end end @@ -8453,47 +8477,47 @@ function AIRBOSS:OnEventTakeoff(EventData) local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - + self:T3(self.lid.."TAKEOFF: unit = "..tostring(EventData.IniUnitName)) self:T3(self.lid.."TAKEOFF: group = "..tostring(EventData.IniGroupName)) self:T3(self.lid.."TAKEOFF: player = "..tostring(_playername)) - + -- Airbase. local airbase=EventData.Place - + -- Airbase name. local airbasename="unknown" if airbase then airbasename=airbase:GetName() end - + -- Check right airbase. if airbasename==self.airbase:GetName() then - + if _unit and _playername then - + -- Debug message. self:T(self.lid..string.format("Player %s took off at %s!",_playername, airbasename)) - + else - + -- Debug message. self:T2(self.lid..string.format("AI unit %s took off at %s!", _unitName, airbasename)) - + -- Get flight. local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) - + if flight then - + -- Set ballcall and recoverd status. for _,elem in pairs(flight.elements) do local element=elem --#AIRBOSS.FlightElement element.ballcall=false element.recovered=nil end - end + end end - + end end @@ -8502,7 +8526,7 @@ end -- @param Core.Event#EVENTDATA EventData function AIRBOSS:OnEventCrash(EventData) self:F3({eventcrash = EventData}) - + -- Nil checks. if EventData==nil then self:E(self.lid.."ERROR: EventData=nil in event CRASH!") @@ -8518,32 +8542,32 @@ function AIRBOSS:OnEventCrash(EventData) local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - + 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 -- 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. -- This also updates the section, if any and removes any unfinished gradings of the player. if flight then self:_RemoveFlight(flight, true) end - + else -- 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 - + end --- Airboss event handler for event Ejection. @@ -8551,7 +8575,7 @@ end -- @param Core.Event#EVENTDATA EventData function AIRBOSS:OnEventEjection(EventData) self:F3({eventland = EventData}) - + -- Nil checks. if EventData==nil then self:E(self.lid.."ERROR: EventData=nil in event EJECTION!") @@ -8567,17 +8591,17 @@ function AIRBOSS:OnEventEjection(EventData) local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - + 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: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) @@ -8586,16 +8610,16 @@ function AIRBOSS:OnEventEjection(EventData) else -- Debug message. self:T(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) - + -- Remove element/unit from flight group and from all queues if no elements alive. self:_RemoveUnitFromFlight(EventData.IniUnit) - + -- What could happen is, that another element has landed (recovered) already and this one crashes. - -- This would mean that the flight would not be deleted from the queue ==> Check if section recovered. + -- This would mean that the flight would not be deleted from the queue ==> Check if section recovered. local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) self:_CheckSectionRecovered(flight) end - + end --- Airboss event handler for event player leave unit. @@ -8604,7 +8628,7 @@ end --function AIRBOSS:OnEventPlayerLeaveUnit(EventData) function AIRBOSS:_PlayerLeft(EventData) self:F3({eventleave=EventData}) - + -- Nil checks. if EventData==nil then self:E(self.lid.."ERROR: EventData=nil in event PLAYERLEFTUNIT!") @@ -8620,26 +8644,26 @@ function AIRBOSS:_PlayerLeft(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 --- Airboss event function handling the mission end event. @@ -8670,19 +8694,19 @@ function AIRBOSS:_Spinning(playerData) SpinIt.LimitXmax=nil SpinIt.LimitZmin=-UTILS.NMToMeters(1) -- 1 NM port SpinIt.LimitZmax=nil - + -- 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 we are in front of the boat (diffX > 0). if self:_CheckLimits(X, Z, SpinIt) then - + -- Player is "de-spinned". Should go to initial again. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.INITIAL) - + -- Remove player from spinning queue. self:_RemoveFlightFromQueue(self.Qspinning, playerData) - + end end @@ -8698,7 +8722,7 @@ function AIRBOSS:_Waiting(playerData) -- 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 @@ -8706,9 +8730,9 @@ function AIRBOSS:_Waiting(playerData) 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") - playerData.warning=true + playerData.warning=true end - + -- Reset warning. if inzone==false and playerData.warning==true then playerData.warning=nil @@ -8723,42 +8747,42 @@ function AIRBOSS:_Holding(playerData) -- Player unit and flight. local unit=playerData.unit - + -- Current stack. local stack=playerData.flag - + -- Check for reported error. if stack<=0 then local text=string.format("ERROR: player %s in step %s is holding but has stack=%s (<=0)", playerData.name, playerData.step, tostring(stack)) self:E(self.lid..text) end - + --------------------------- -- Holding Pattern Check -- --------------------------- - + -- Pattern altitude. local patternalt=self:_GetMarshalAltitude(stack, playerData.case) - + -- Player altitude. local playeralt=unit:GetAltitude() - + -- Get holding zone of player. local zoneHolding=self:_GetZoneHolding(playerData.case, stack) - + -- Nil check. if zoneHolding==nil then self:E(self.lid.."ERROR: zoneHolding is nil!") self:E({playerData=playerData}) return end - + -- Check if player is in holding zone. local inholdingzone=unit:IsInZone(zoneHolding) - + -- Altitude difference between player and assigned stack. local altdiff=playeralt-patternalt - + -- Acceptable altitude depending on player skill. local altgood=UTILS.FeetToMeters(500) if playerData.difficulty==AIRBOSS.Difficulty.HARD then @@ -8771,40 +8795,40 @@ function AIRBOSS:_Holding(playerData) -- Students should be within +-500 ft. altgood=UTILS.FeetToMeters(500) end - + -- 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. local dT=timer.getTime()-self.Tcollapse - + -- TODO: check if this works. --local dT=timer.getAbsTime()-playerData.time - + -- Check if less then 90 seconds. if dT<=90 then justcollapsed=true end end - + -- Check if altitude is acceptable. local goodalt=math.abs(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 - + end - + -- Back to assigned altitude. if playerData.warning and math.abs(altdiff)<=altback then text=text..string.format("Altitude is looking good again.") playerData.warning=nil end - + elseif playerData.holding==false then - + -- Player left holding zone if inholdingzone then -- Player is back in the holding zone. @@ -8854,18 +8878,18 @@ function AIRBOSS:_Holding(playerData) -- Player is still outside the holding zone. self:T3("Player still outside the holding zone. What are you doing man?!") end - + elseif playerData.holding==nil then -- Player did not entered the holding zone yet. - + if inholdingzone then - + -- Player arrived in holding zone. playerData.holding=true - + -- Inform player. text=text..string.format("You arrived at the holding zone.") - + -- Feedback on altitude. if goodalt then text=text..string.format(" Altitude is good.") @@ -8878,68 +8902,68 @@ function AIRBOSS:_Holding(playerData) text=text..string.format("\nCurrently assigned altitude is %d ft.", UTILS.MetersToFeet(patternalt)) playerData.warning=true end - + else -- Player did not yet arrive in holding zone. self:T3("Waiting for player to arrive in the holding zone.") end - + end - + -- Send message. if playerData.showhints then self:MessageToPlayer(playerData, text, "MARSHAL") end - + end ---- Commence approach. This step initializes the player data. Section members are also set to commence. 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. -- @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 -- Friendly reminder. if timer.getAbsTime()-playerData.time>180 then self:_MarshalCallClearedForRecovery(playerData.onboard, playerData.case) - playerData.time=timer.getAbsTime() - end - + playerData.time=timer.getAbsTime() + end + -- Skip the rest. 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.HARD then - -- Text + -- Text local text="" - + -- Positive response. if playerData.case==1 then text=text.."Proceed to initial." @@ -8949,11 +8973,11 @@ function AIRBOSS:_Commencing(playerData, zonecheck) text=text.." VSI 4000 ft/min until you reach 5000 ft." end end - + -- Message to player. self:MessageToPlayer(playerData, text, "MARSHAL") end - + -- Next step: depends on case recovery. local nextstep if playerData.case==1 then @@ -8963,16 +8987,16 @@ function AIRBOSS:_Commencing(playerData, zonecheck) -- CASE II/III: Player has to start the descent at 4000 ft/min to the platform at 5k ft. 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 - + end --- Start pattern when player enters the initial zone in case I/II recoveries. @@ -8986,19 +9010,19 @@ function AIRBOSS:_Initial(playerData) -- Relative heading to carrier direction. local relheading=self:_GetRelativeHeading(playerData.unit, false) - + -- Alitude of player in feet. local altitude=playerData.unit:GetAltitude() - + -- Check if player is in zone and flying roughly in the right direction. if inzone and math.abs(relheading)<60 and altitude<=self.initialmaxalt then - + -- Send message for normal and easy difficulty. if playerData.showhints then - + -- Inform player. local hint=string.format("Initial") - + -- Hook down for students. if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then @@ -9006,17 +9030,17 @@ function AIRBOSS:_Initial(playerData) else hint=hint.." - Hook down!" end - end - + end + self:MessageToPlayer(playerData, hint, "MARSHAL") end - + -- Next step: Break entry. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.BREAKENTRY) - + return true end - + return false end @@ -9027,21 +9051,21 @@ function AIRBOSS:_CheckCorridor(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 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.", "AIRBOSS") playerData.warning=false - end + end end @@ -9049,19 +9073,19 @@ end -- @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)) - + -- Check if we are in zone. if inzone then - + -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) - + -- Next step: depends. local nextstep if math.abs(self.holdingoffset)>0 and playerData.case>1 then @@ -9076,10 +9100,10 @@ function AIRBOSS:_Platform(playerData) nextstep=AIRBOSS.PatternStep.DIRTYUP end end - + -- Next step hint. self:_SetPlayerStep(playerData, nextstep) - + end end @@ -9091,18 +9115,18 @@ function AIRBOSS:_ArcInTurn(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:_GetZoneArcIn(playerData.case)) - + if inzone then - + -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) - + -- Next step: Arc Out Turn. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.ARCOUT) - + end end @@ -9113,15 +9137,15 @@ function AIRBOSS:_ArcOutTurn(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:_GetZoneArcOut(playerData.case)) - + if inzone then -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) - + -- Next step: local nextstep if playerData.case==3 then @@ -9131,7 +9155,7 @@ function AIRBOSS:_ArcOutTurn(playerData) -- Case II: Initial. nextstep=AIRBOSS.PatternStep.INITIAL end - + -- Next step hint. self:_SetPlayerStep(playerData, nextstep) end @@ -9146,13 +9170,13 @@ function AIRBOSS:_DirtyUp(playerData) self:_CheckCorridor(playerData) -- Check if we are inside the moving zone. - local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) - + local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) + if inzone then -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) - + -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. if playerData.actype==AIRBOSS.AircraftCarrier.HORNET or playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then local callsay=self:_NewRadioCall(self.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard) @@ -9160,12 +9184,12 @@ function AIRBOSS:_DirtyUp(playerData) self:RadioTransmission(self.MarshalRadio, callsay, false, 55, nil, true) self:RadioTransmission(self.MarshalRadio, callfly, false, 60, nil, true) end - + -- TODO: Make Fly Bullseye call if no automatic ICLS is active. - + -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.BULLSEYE) - + end end @@ -9180,24 +9204,24 @@ function AIRBOSS:_Bullseye(playerData) -- Check if we are inside the moving zone. local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) - + -- 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 - + if inzone and math.abs(relheading)<60 then + -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) - + -- LSO expect spot 7.5 call - if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, nil, nil, nil, true) end - + -- Next step: Groove Call the ball. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) - + end end @@ -9208,7 +9232,7 @@ 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" @@ -9219,8 +9243,8 @@ function AIRBOSS:_BolterPattern(playerData) Bolter.LimitXmin= 100 -- Check that 100 meter ahead and port Bolter.LimitXmax= nil Bolter.LimitZmin= nil - Bolter.LimitZmax= nil - + Bolter.LimitZmax= nil + -- Check if we are in front of the boat (diffX > 0). if self:_CheckLimits(X, Z, Bolter) then local nextstep @@ -9229,7 +9253,7 @@ function AIRBOSS:_BolterPattern(playerData) else nextstep=AIRBOSS.PatternStep.BULLSEYE end - self:_SetPlayerStep(playerData, nextstep) + self:_SetPlayerStep(playerData, nextstep) end end @@ -9242,22 +9266,22 @@ function AIRBOSS:_BreakEntry(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) local X, Z=self:_GetDistances(playerData.unit) - + -- Abort condition check. 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.BreakEntry) then - + -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) - + -- Next step: Early Break. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.EARLYBREAK) - + end end @@ -9270,19 +9294,19 @@ 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) - - -- Early or late break. + + -- Early or late break. local breakpoint = self.BreakEarly if part==AIRBOSS.PatternStep.LATEBREAK then breakpoint = self.BreakLate end - + -- Check abort conditions. if self:_CheckAbort(X, Z, breakpoint) then self:_AbortPattern(playerData, X, Z, breakpoint, true) return end - + -- Player made a very tight turn and did not trigger the latebreak threshold at 0.8 NM. local tooclose=false if part==AIRBOSS.PatternStep.LATEBREAK then @@ -9295,12 +9319,12 @@ function AIRBOSS:_Break(playerData, part) self:MessageToPlayer(playerData, "your turn was too tight! Allow for more distance to the boat next time.", "LSO") end tooclose=true - end + end end -- Check limits. if self:_CheckLimits(X, Z, breakpoint) or tooclose then - + -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) @@ -9311,7 +9335,7 @@ function AIRBOSS:_Break(playerData, part) else nextstep=AIRBOSS.PatternStep.ABEAM end - + self:_SetPlayerStep(playerData, nextstep) end end @@ -9320,37 +9344,37 @@ end -- @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) - -- 1.6 NM from carrier is too far. + -- 1.6 NM from carrier is too far. local limit=UTILS.NMToMeters(-1.6) - + -- For the tarawa we give a bit more space. if self.carriertype==AIRBOSS.CarrierType.TARAWA then limit=UTILS.NMToMeters(-2.0) end - + -- 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") @@ -9432,26 +9456,26 @@ end --- At the Wake. -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. -function AIRBOSS:_Wake(playerData) +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 - + -- Hint for player about altitude, AoA etc. self:_PlayerHint(playerData) - + -- Next step: Final. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.FINAL) - + end end @@ -9463,26 +9487,26 @@ function AIRBOSS:_GetGrooveData(playerData) -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier). local X, Z=self:_GetDistances(playerData.unit) - + -- Stern position at the rundown. local stern=self:_GetSternCoord() - + -- Distance from rundown to player aircraft. local rho=stern:Get2DDistance(playerData.unit:GetCoordinate()) - + -- Aircraft is behind the carrier. local astern=X5. This would mean the player has not turned in correctly! - + -- Groove data. playerData.groove.X0=UTILS.DeepCopy(groovedata) - + -- Set time stamp. Next call in 4 seconds. playerData.Tlso=timer.getTime() - + -- Next step: X start. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) end - + -- Groovedata step. - groovedata.Step=playerData.step + groovedata.Step=playerData.step end @@ -9569,7 +9593,7 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. 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 @@ -9577,197 +9601,197 @@ function AIRBOSS:_Groove(playerData) 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. + -- Groove data. local groovedata=self:_GetGrooveData(playerData) - + -- Add data to trapsheet. table.insert(playerData.trapsheet, groovedata) - + -- Coords. local X=groovedata.X local Z=groovedata.Z - + -- Check abort conditions. if self:_CheckAbort(groovedata.X, groovedata.Z, self.Groove) then self:_AbortPattern(playerData, groovedata.X, groovedata.Z, self.Groove, true) return - end - + end + -- Shortcuts. local rho=groovedata.Rho local lineupError=groovedata.LUE local glideslopeError=groovedata.GSE local AoA=groovedata.AoA - - + + if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX and (math.abs(groovedata.Roll)<=4.0 or playerData.unit:IsInZone(self:_GetZoneLineup())) then - + -- Start time in groove playerData.TIG0=timer.getTime() - + -- LSO "Call the ball" call. self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true) playerData.Tlso=timer.getTime() - + -- Pilot "405, Hornet Ball, 3.2". - + -- LSO "Roger ball" call in three seconds. self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, false, nil, 2, true) - + -- Store data. playerData.groove.XX=UTILS.DeepCopy(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. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IM) - + elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then - + -- Store data. playerData.groove.IM=UTILS.DeepCopy(groovedata) - + -- Next step: in close. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IC) - + elseif rho<=RIC and playerData.step==AIRBOSS.PatternStep.GROOVE_IC then - - -- Store data. + + -- Store data. playerData.groove.IC=UTILS.DeepCopy(groovedata) - + -- Next step: AR at the ramp. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_AR) - + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_AR) + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then - + -- Store data. playerData.groove.AR=UTILS.DeepCopy(groovedata) - + -- Next step: in the wires. if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_AL) - else + else self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IW) end - + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AL then - + -- Store data. playerData.groove.AL=UTILS.DeepCopy(groovedata) - + -- Get zone abeam LDG spot. local ZoneALS=self:_GetZoneAbeamLandingSpot() - + -- Get player velocity in km/h. local vplayer=playerData.unit:GetVelocityKMH() - + -- Get carrier velocity in km/h. local vcarrier=self.carrier:GetVelocityKMH() - + -- Speed difference. local dv=math.abs(vplayer-vcarrier) - + -- Stable when speed difference < 10 km/h. local stable=dv<10 - + -- Check if player is inside the zone. if playerData.unit:IsInZone(ZoneALS) and stable then - - -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. + + -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. self:RadioTransmission(self.LSORadio, self.LSOCall.CLEAREDTOLAND, nil, nil, nil, true) - + -- Next step: Level cross. - self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_LC) + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_LC) end elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_LC then - + -- Store data. - playerData.groove.LC=UTILS.DeepCopy(groovedata) - + playerData.groove.LC=UTILS.DeepCopy(groovedata) + -- Get zone primary LDG spot. local ZoneLS=self:_GetZoneLandingSpot() - + -- Get player velocity in km/h. local vplayer=playerData.unit:GetVelocityKMH() - + -- Get carrier velocity in km/h. local vcarrier=self.carrier:GetVelocityKMH() - + -- Speed difference. local dv=math.abs(vplayer-vcarrier) - + -- Stable when v<7.5 km/h. local stable=dv<7.5 - + -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. if playerData.unit:IsInZone(ZoneLS) and stable and playerData.warning==false then self:RadioTransmission(self.LSORadio, self.LSOCall.STABILIZED, nil, nil, nil, true) playerData.warning=true end - + -- We keep it in this step until landed. - + end - + -------------- -- Wave Off -- -------------- - + -- 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, self.LSOCall.WAVEOFF, nil, nil, nil, true) playerData.Tlso=timer.getTime() - + -- Player was waved off! playerData.waveoff=true - + -- Nothing else necessary. return end - + end - + -- Groovedata step. groovedata.Step=playerData.step - + ----------------- -- Groove Data -- ----------------- - + -- 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 LUE at step %s, d=%.3f: LUE %.3f>%.3f", gs, d, lineupError, gd.LUE)) gd.LUE=lineupError end - + -- Fly through good window of glideslope. if gd.GSE>0.4 and glideslopeError<-0.3 then -- Fly through down ==> "\" @@ -9778,40 +9802,40 @@ function AIRBOSS:_Groove(playerData) gd.FlyThrough="/" self:E(self.lid..string.format("Got Fly through UP at step %s, d=%.3f: Min GSE=%.3f, lower GSE=%.3f", gs, d, gd.GSE, glideslopeError)) end - + -- Update max deviation of glideslope error. if math.abs(glideslopeError)>math.abs(gd.GSE) then self:T(self.lid..string.format("Got bigger GSE at step %s, d=%.3f: GSE |%.3f|>|%.3f|", gs, d, glideslopeError, gd.GSE)) gd.GSE=glideslopeError end - + -- 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 step %s, d=%.3f: AoA %.3f>%.3f.", gs, d, AoA, gd.AoA)) gd.AoA=AoA end - - --local gs2=self:_GS(groovedata.Step, -1) + + --local gs2=self:_GS(groovedata.Step, -1) --env.info(string.format("groovestep %s %s d=%.3f NM: GSE=%.3f %.3f, LUE=%.3f %.3f, AoA=%.3f %.3f", gs, gs2, d, groovedata.GSE, gd.GSE, groovedata.LUE, gd.LUE, groovedata.AoA, gd.AoA)) - + end - + --------------- -- LSO Calls -- --------------- - + -- Time since last LSO call. local deltaT=timer.getTime()-playerData.Tlso - + -- Wait until player passed the 0.75 NM distance. local _advice=true - if playerData.TIG0==nil and playerData.difficulty~=AIRBOSS.Difficulty.EASY then --rho>RXX + if playerData.TIG0==nil and playerData.difficulty~=AIRBOSS.Difficulty.EASY then --rho>RXX _advice=false end @@ -9819,53 +9843,53 @@ function AIRBOSS:_Groove(playerData) if deltaT>=self.LSOdT and _advice 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 - + 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 were waved off.") end - + elseif playerData.boltered then - + -- This should not happen because landing event was triggered. self:_AddToDebrief(playerData, "You boltered.") - + else - + -- This should not happen. self:T("Player was not waved off but flew past the carrier without landing ==> Own wave off!") - - -- We count this as OWO. + + -- We count this as OWO. self:_AddToDebrief(playerData, "Own waveoff.") - + -- Set Owo playerData.owo=true - + end - + -- Next step: debrief. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) - + end - + end --- LSO check if player needs to wave off. -- Wave off conditions are: --- +-- -- * Glideslope error <1.2 or >1.8 degrees. -- * |Line up error| > 3 degrees. -- * AoA check but only for TOPGUN graduates. @@ -9879,12 +9903,12 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- Assume we're all good. local waveoff=false - + -- Parameters local glMax= 1.8 local glMin=-1.2 local luAbs= 3.0 - + -- For the harrier, we allow a bit more room. if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then glMax= 4.0 @@ -9893,7 +9917,7 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) -- No waveoff for harrier pilots at the moment. return false end - + -- Too high or too low? if glideslopeError>glMax then local text=string.format("\n- Waveoff due to glideslope error %.2f > %.1f degrees!", glideslopeError, glMax) @@ -9914,12 +9938,12 @@ function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) self:_AddToDebrief(playerData, text) waveoff=true end - + -- 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. + local aoaac=self:_GetAircraftAoA(playerData) + -- Check too slow or too fast. if AoA Foul deck!", unit:GetName()) self:T(self.lid..text) @@ -10006,36 +10030,36 @@ function AIRBOSS:_CheckFoulDeck(playerData) foulunit=unit end end - - -- Add to debrief and + + -- 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, self.LSOCall.FOULDECK, false, 1) self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, false, 1.2, nil, true) - + -- Player hint for flight students. if playerData.showhints then local text=string.format("overfly landing area and enter bolter pattern.") self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) - end - + end + -- Set player parameters for foul deck. playerData.wofd=true - + -- Debrief. playerData.step=AIRBOSS.PatternStep.DEBRIEF playerData.warning=nil - + -- Pass would be invalid if the player lands. playerData.valid=false - + -- Send a message to the player that blocks the runway. if foulunit then local foulflight=self:_GetFlightFromGroupInQueue(foulunit:GetGroup(), self.flights) @@ -10055,14 +10079,14 @@ 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() - - -- Stern coordinate (sterndist<0). + + -- Stern coordinate (sterndist<0). if self.carriertype==AIRBOSS.CarrierType.TARAWA then -- Tarawa: Translate 8 meters port. stern=stern:Translate(self.carrierparam.sterndist, hdg):Translate(8, FB-90) @@ -10070,7 +10094,7 @@ function AIRBOSS:_GetSternCoord() -- Stennis: translate 7 meters starboard wrt Final bearing. stern=stern:Translate(self.carrierparam.sterndist, hdg):Translate(7, FB+90) end - + -- Set altitude. stern:SetAltitude(self.carrierparam.deckheight) @@ -10086,19 +10110,19 @@ function AIRBOSS:_GetWire(Lcoord, dc) -- Final bearing (true). local FB=self:GetFinalBearing() - + -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. local Scoord=self:_GetSternCoord() - + -- Distance to landing coord. local Ldist=Lcoord:Get2DDistance(Scoord) -- For human (not AI) the lading event is delayed unfortunately. Therefore, we need another correction factor. dc= dc or 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 - + -- Shift wires from stern to their correct position. local w1=self.carrierparam.wire1 local w2=self.carrierparam.wire2 @@ -10118,38 +10142,38 @@ function AIRBOSS:_GetWire(Lcoord, dc) else wire=99 end - + if self.Debug and false 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() - + end - + -- Debug output. self:T(string.format("GetWire: L=%.1f, L-dc=%.1f ==> wire=%d (dc=%.1f)", Ldist, Ldist-dc, wire, dc)) @@ -10160,25 +10184,25 @@ end -- @param #AIRBOSS self -- @param #AIRBOSS.PlayerData playerData Player data table. 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 - + -- 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() - + -- 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. local dcorr=100 if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then @@ -10190,33 +10214,33 @@ function AIRBOSS:_Trapped(playerData) -- A-4E gets slowed down much faster the the F/A-18C! dcorr=56 end - + -- Get wire. local wire=self:_GetWire(coord, dcorr) -- Debug. 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! if v>5 then - - -- Check if we passed all wires. + + -- Check if we passed all wires. if wire>4 and v>10 and not playerData.warning then -- Looks like we missed the wires ==> Bolter! self:RadioTransmission(self.LSORadio, self.LSOCall.BOLTER, nil, nil, nil, true) playerData.warning=true end - + -- Call function again and check if converged or back in air. SCHEDULER:New(nil, self._Trapped, {self, playerData}, 0.1) return end - + ---------------------------------------- --- Form this point on we have converged ---------------------------------------- - + -- Put some smoke and a mark. if self.Debug then coord:SmokeBlue() @@ -10226,8 +10250,8 @@ function AIRBOSS:_Trapped(playerData) -- Set player wire. playerData.wire=wire - - -- Message to player. + + -- Message to player. local text=string.format("Trapped %d-wire.", wire) if wire==3 then text=text.." Well done!" @@ -10238,26 +10262,26 @@ function AIRBOSS:_Trapped(playerData) elseif wire==1 then text=text.." Try harder next time!" end - + -- Message to player. self:MessageToPlayer(playerData, text, "LSO", "") - + -- Debrief. local hint = string.format("Trapped %d-wire.", wire) self:_AddToDebrief(playerData, hint, "Groove: IW") - + else - + --Again in air ==> Boltered! local text=string.format("Player %s boltered in trapped function.", playerData.name) self:T(self.lid..text) MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.debug) - + -- Bolter switch on. playerData.boltered=true - + end - + -- Next step: debriefing. playerData.step=AIRBOSS.PatternStep.DEBRIEF playerData.warning=nil @@ -10275,25 +10299,25 @@ function AIRBOSS:_GetZoneInitial(case) -- Get radial, i.e. inverse of BRC. local radial=self:GetRadial(2, false, false) - + -- Carrier coordinate. local cv=self:GetCoordinate() - + -- Vec2 array. 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=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. vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()} - + else -- Case II @@ -10302,14 +10326,14 @@ function AIRBOSS:_GetZoneInitial(case) 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 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("Zone CASE I/II Initial", vec2) @@ -10323,20 +10347,20 @@ function AIRBOSS:_GetZoneLineup() -- Get radial, i.e. inverse of BRC. local fbi=self:GetRadial(1, false, false) - + -- Stern coordinate. local st=self:_GetOptLandingCoordinate() - + -- Zone points. local c1=st local c2=st:Translate(UTILS.NMToMeters(0.50), fbi+15) local c3=st:Translate(UTILS.NMToMeters(0.50), fbi+self.lue._max-0.05) local c4=st:Translate(UTILS.NMToMeters(0.77), fbi+self.lue._max-0.05) local c5=c4:Translate(UTILS.NMToMeters(0.25), fbi-90) - + -- Vec2 array. local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()} - + -- Polygon zone. local zone=ZONE_POLYGON_BASE:New("Zone Lineup", vec2) @@ -10358,10 +10382,10 @@ function AIRBOSS:_GetZoneGroove(l, w, b) -- Get radial, i.e. inverse of BRC. local fbi=self:GetRadial(1, false, false) - + -- Stern coordinate. local st=self:_GetSternCoord() - + -- Zone points. local c1=st:Translate(self.carrierparam.totwidthstarboard, fbi-90) local c2=st:Translate(UTILS.NMToMeters(0.10), fbi-90):Translate(UTILS.NMToMeters(0.3), fbi) @@ -10369,10 +10393,10 @@ function AIRBOSS:_GetZoneGroove(l, w, b) local c4=st:Translate(UTILS.NMToMeters(w/2), fbi+90):Translate(UTILS.NMToMeters(l), fbi) local c5=st:Translate(UTILS.NMToMeters(b), fbi+90):Translate(UTILS.NMToMeters(0.3), fbi) local c6=st:Translate(self.carrierparam.totwidthport, fbi+90) - + -- Vec2 array. local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2()} - + -- Polygon zone. local zone=ZONE_POLYGON_BASE:New("Zone Groove", vec2) @@ -10387,20 +10411,20 @@ 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=self:GetRadial(case, false, false) - + -- 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 @@ -10412,20 +10436,20 @@ function AIRBOSS:_GetZoneDirtyUp(case) -- Radius = 1 NM. local radius=UTILS.NMToMeters(1) - + -- Distance = 9 NM local distance=UTILS.NMToMeters(9) - + -- Zone depends on Case recovery. local radial=self:GetRadial(case, false, false) - + -- 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 @@ -10437,19 +10461,19 @@ function AIRBOSS:_GetZoneArcOut(case) -- Radius = 1.25 NM. local radius=UTILS.NMToMeters(1.25) - + -- Distance = 12 NM local distance=UTILS.NMToMeters(11.75) - + -- Zone depends on Case recovery. local radial=self:GetRadial(case, false, false) - + -- Get coordinate of carrier and translate. local coord=self:GetCoordinate():Translate(distance, radial) - + -- Create zone. local zone=ZONE_RADIUS:New("Zone Arc Out", coord:GetVec2(), radius) - + return zone end @@ -10461,25 +10485,25 @@ function AIRBOSS:_GetZoneArcIn(case) -- Radius = 1.25 NM. local radius=UTILS.NMToMeters(1.25) - + -- Zone depends on Case recovery. local radial=self:GetRadial(case, false, true) - + -- Angle between FB/BRC and holding zone. local alpha=math.rad(self.holdingoffset) - + -- 14+x NM from carrier local x=14 --/math.cos(alpha) - + -- Distance = 14 NM local distance=UTILS.NMToMeters(x) - + -- Get coordinate. local coord=self:GetCoordinate():Translate(distance, radial) -- Create zone. local zone=ZONE_RADIUS:New("Zone Arc In", coord:GetVec2(), radius) - + return zone end @@ -10491,7 +10515,7 @@ function AIRBOSS:_GetZonePlatform(case) -- Radius = 1 NM. local radius=UTILS.NMToMeters(1) - + -- Zone depends on Case recovery. local radial=self:GetRadial(case, false, true) @@ -10500,13 +10524,13 @@ function AIRBOSS:_GetZonePlatform(case) -- Distance = 19 NM local distance=UTILS.NMToMeters(19) --/math.cos(alpha) - + -- Get coordinate. local coord=self:GetCoordinate():Translate(distance, radial) -- Create zone. local zone=ZONE_RADIUS:New("Zone Platform", coord:GetVec2(), radius) - + return zone end @@ -10524,57 +10548,57 @@ function AIRBOSS:_GetZoneCorridor(case, l) -- Radial and offset. local radial=self:GetRadial(case, false, false) local offset=self:GetRadial(case, false, true) - + -- Distance shift ahead of carrier to allow for some space to bolter. local dx=5 - + -- Width of the box in NM. local w=2 local w2=w/2 - + -- Distance from carrier to arc out zone. local d=12 - + -- Carrier position. local cv=self:GetCoordinate() - + -- Polygon points. local c={} - + -- First point. Carrier coordinate translated 5 NM in direction of travel to allow for bolter space. - c[1]=cv:Translate(-UTILS.NMToMeters(dx), radial) - + c[1]=cv:Translate(-UTILS.NMToMeters(dx), radial) + if math.abs(self.holdingoffset)>=5 then - + ----------------- -- Angled Case -- ----------------- - + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier, dx ahead. c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right - + c[4]=cv:Translate(UTILS.NMToMeters(15), offset):Translate(UTILS.NMToMeters(1), offset-90) c[5]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset-90) c[6]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset+90) c[7]=cv:Translate(UTILS.NMToMeters(13), offset):Translate(UTILS.NMToMeters(1), offset+90) c[8]=cv:Translate(UTILS.NMToMeters(11), radial):Translate(UTILS.NMToMeters(1), radial+90) - + c[9]=c[1]:Translate(UTILS.NMToMeters(w2), 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(dx+l), radial) -- 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 - + -- Create an array of a square! local p={} for _i,_c in ipairs(c) do @@ -10589,24 +10613,24 @@ function AIRBOSS:_GetZoneCorridor(case, l) local zone=ZONE_POLYGON_BASE:New("CASE II/III Approach Corridor", p) return zone - + --[[ -- OLD -- Angle between radial and offset in rad. - local alpha=math.rad(self.holdingoffset) + local alpha=math.rad(self.holdingoffset) -- Some math... local y1=d-w2 local x1=y1*math.tan(alpha) local y2=d+w2 - local x2=y2*math.tan(alpha) + local x2=y2*math.tan(alpha) local b=w2*(1/math.cos(alpha)-1) - + -- This is what we need. local P=x1+b local Q=x2-b - + -- Debug output. self:T3(string.format("FF case %d radial = %d", case, radial)) self:T3(string.format("FF case %d offset = %d", case, offset)) @@ -10624,18 +10648,18 @@ function AIRBOSS:_GetZoneCorridor(case, l) -- Complicated case with an angle. 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[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[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) - + -- 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) + --c[7]=c[7]:Translate(UTILS.NMToMeters(2), offset) ]] - + end @@ -10646,31 +10670,31 @@ 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.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.totlength, 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) @@ -10684,10 +10708,10 @@ function AIRBOSS:_GetZoneRunwayBox() -- Stern coordinate. local S=self:_GetSternCoord() - + -- Current carrier heading. local FB=self:GetFinalBearing(false) - + -- Coordinate array. local p={} @@ -10702,11 +10726,11 @@ function AIRBOSS:_GetZoneRunwayBox() 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 + return zone end @@ -10715,19 +10739,19 @@ end -- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. function AIRBOSS:_GetZoneAbeamLandingSpot() - -- Primary landing Spot coordinate. + -- Primary landing Spot coordinate. local S=self:_GetOptLandingCoordinate() - + -- Current carrier heading. local FB=self:GetFinalBearing(false) - + -- Coordinate array. local p={} -- Points. p[1]=S:Translate( 15, FB):Translate(15, FB+90) -- Top-Right p[2]=S:Translate(-15, FB):Translate(15, FB+90) -- Bottom-Right - p[3]=S:Translate(-15, FB):Translate(15, FB-90) -- Bottom-Left + p[3]=S:Translate(-15, FB):Translate(15, FB-90) -- Bottom-Left p[4]=S:Translate( 15, FB):Translate(15, FB-90) -- Top-Left -- Convert to vec2. @@ -10735,11 +10759,11 @@ function AIRBOSS:_GetZoneAbeamLandingSpot() for _,coord in ipairs(p) do table.insert(vec2, coord:GetVec2()) end - + -- Create polygon zone. local zone=ZONE_POLYGON_BASE:New("Abeam Landing Spot Zone", vec2) - return zone + return zone end @@ -10748,12 +10772,12 @@ end -- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. function AIRBOSS:_GetZoneLandingSpot() - -- Primary landing Spot coordinate. + -- Primary landing Spot coordinate. local S=self:_GetLandingSpotCoordinate() - + -- Current carrier heading. local FB=self:GetFinalBearing(false) - + -- Coordinate array. local p={} @@ -10768,11 +10792,11 @@ function AIRBOSS:_GetZoneLandingSpot() for _,coord in ipairs(p) do table.insert(vec2, coord:GetVec2()) end - + -- Create polygon zone. local zone=ZONE_POLYGON_BASE:New("Landing Spot Zone", vec2) - return zone + return zone end @@ -10791,51 +10815,51 @@ function AIRBOSS:_GetZoneHolding(case, stack) self:E(self.lid.."ERROR: Stack <= 0 in _GetZoneHolding!") self:E({case=case, stack=stack}) return nil - end - + end + -- Pattern altitude. local patternalt, c1, c2=self:_GetMarshalAltitude(stack, case) - + -- Select case. if case==1 then -- CASE I - + -- Get current carrier heading. local hdg=self:GetHeading() - + -- Distance to the post. local D=UTILS.NMToMeters(2.5) - + -- 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(), self.marshalradius) - + -- Delta pattern. if self.carriertype==AIRBOSS.CarrierType.TARAWA then zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", self.carrier:GetVec2(), UTILS.NMToMeters(5)) end - - - else + + + else -- CASE II/II - + -- Get radial. local radial=self:GetRadial(case, false, true) - + -- 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 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. zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) end - + return zoneHolding end @@ -10847,55 +10871,55 @@ 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) - + if self.carriertype==AIRBOSS.CarrierType.TARAWA then - + local Dx=UTILS.NMToMeters(2.25) - + local Dz=UTILS.NMToMeters(2.25) - + R=UTILS.NMToMeters(1) - + Three=self:GetCoordinate():Translate(Dz, hdg-90):Translate(Dx, hdg-180) - + end - + -- Create holding zone. zone=ZONE_RADIUS:New("CASE I Commence Zone", Three:GetVec2(), R) - + else -- Case II/III - + -- We simply take the corridor for now. But a bit shorter. Holding starts at 21 and add 2 NM box as commence. --zone=self:_GetZoneCorridor(case, 23) - + -- Total length. local l=21 - + -- Offset angle local offset=self:GetRadial(case, false, true) - + -- Carrier position. local cv=self:GetCoordinate() - + -- Polygon points. local c={} - + c[1]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset-90) c[2]=cv:Translate(UTILS.NMToMeters(l+2.5), offset):Translate(UTILS.NMToMeters(1), offset-90) c[3]=cv:Translate(UTILS.NMToMeters(l+2.5), offset):Translate(UTILS.NMToMeters(1), offset+90) @@ -10906,10 +10930,10 @@ function AIRBOSS:_GetZoneCommence(case) for _i,_c in ipairs(c) do p[_i]=_c:GetVec2() end - + -- Zone polygon. zone=ZONE_POLYGON_BASE:New("CASE II/III Commence Zone", p) - + end return zone @@ -10926,23 +10950,23 @@ function AIRBOSS:_AttitudeMonitor(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:GetCoordinate()) local dx,dz,rho,phi=self:_GetDistances(unit) -- Wind vector. local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() - + -- Aircraft veloecity vector. local velo=unit:GetVelocityVec3() - local vabs=UTILS.VecNorm(velo) + local vabs=UTILS.VecNorm(velo) local rwy=false local step=playerData.step @@ -10957,15 +10981,15 @@ function AIRBOSS:_AttitudeMonitor(playerData) step=self:_GS(step,-1) rwy=true end - + -- Relative heading Aircraft to Carrier. local relhead=self:_GetRelativeHeading(playerData.unit, rwy) - + --local lc=self:_GetOptLandingCoordinate() --lc:FlareRed() - + -- Output - local text=string.format("Pattern step: %s", step) + local text=string.format("Pattern step: %s", step) text=text..string.format("\nAoA=%.1f° = %.1f Units | |V|=%.1f knots", aoa, self:_AoADeg2Units(playerData, aoa), UTILS.MpsToKnots(vabs)) if self.Debug then -- Velocity vector. @@ -10977,9 +11001,9 @@ function AIRBOSS:_AttitudeMonitor(playerData) text=text..string.format("\nClimb Angle=%.1f° | Rate=%d ft/min", unit:GetClimbAngle(), velo.y*196.85) local dist=self:_GetOptLandingCoordinate():Get3DDistance(playerData.unit) -- Get player velocity in km/h. - local vplayer=playerData.unit:GetVelocityKMH() + local vplayer=playerData.unit:GetVelocityKMH() -- Get carrier velocity in km/h. - local vcarrier=self.carrier:GetVelocityKMH() + local vcarrier=self.carrier:GetVelocityKMH() -- Speed difference. local dv=math.abs(vplayer-vcarrier) local alt=self:_GetAltCarrier(playerData.unit) @@ -11004,7 +11028,7 @@ function AIRBOSS:_AttitudeMonitor(playerData) text=text..string.format("\nR=%.2f NM | X=%d Z=%d m", UTILS.MetersToNM(rho), dx, dz) text=text..string.format("\nGamma=%.1f° | Rho=%.1f°", relhead, phi) end - + MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) end @@ -11021,27 +11045,27 @@ function AIRBOSS:_Glideslope(unit, optangle) else optangle=3.5 end - end + end -- Landing coordinate local landingcoord=self:_GetOptLandingCoordinate() -- Distance from stern to aircraft. local x=unit:GetCoordinate():Get2DDistance(landingcoord) - + -- Altitude of unit corrected by the deck height of the carrier. local h=self:_GetAltCarrier(unit) - + -- Harrier should be 40-50 ft above the deck. if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then h=unit:GetAltitude()-(UTILS.FeetToMeters(50)+self.carrierparam.deckheight+2) end - + -- Glide slope. local glideslope=math.atan(h/x) - + -- Glide slope (error) in degrees. local gs=math.deg(glideslope)-optangle - + return gs end @@ -11058,34 +11082,34 @@ function AIRBOSS:_Glideslope2(unit, optangle) else optangle=3.5 end - end + end -- Landing coordinate local landingcoord=self:_GetOptLandingCoordinate() -- Distance from stern to aircraft. local x=unit:GetCoordinate():Get3DDistance(landingcoord) - + -- Altitude of unit corrected by the deck height of the carrier. local h=self:_GetAltCarrier(unit) - + -- Harrier should be 40-50 ft above the deck. if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then h=unit:GetAltitude()-(UTILS.FeetToMeters(50)+self.carrierparam.deckheight+2) end - + -- Glide slope. local glideslope=math.asin(h/x) - + -- Glide slope (error) in degrees. local gs=math.deg(glideslope)-optangle - + -- Debug. self:T3(self.lid..string.format("Glide slope error = %.1f, x=%.1f h=%.1f", gs, x, h)) - + return gs end ---- Get line up of player wrt to carrier. +--- Get line up of player wrt to carrier. -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Aircraft unit. -- @param #boolean runway If true, include angled runway. @@ -11094,43 +11118,43 @@ function AIRBOSS:_Lineup(unit, runway) -- Landing coordinate local landingcoord=self:_GetOptLandingCoordinate() - + -- Vector to landing coord. local A=landingcoord:GetVec3() - + -- Vector to player. local B=unit:GetVec3() - + -- Vector from player to carrier. local C=UTILS.VecSubstract(A, B) - + -- Only in 2D plane. C.y=0.0 - + -- Orientation of carrier. - local X=self.carrier:GetOrientationX() + local X=self.carrier:GetOrientationX() X.y=0.0 - + -- Rotate orientation to angled runway. - if runway then + if runway then X=UTILS.Rotate2D(X, -self.carrierparam.rwyangle) end - + -- Projection of player pos on x component. local x=UTILS.VecDot(X, C) - + -- Orientation of carrier. local Z=self.carrier:GetOrientationZ() Z.y=0.0 - + -- Rotate orientation to angled runway. - if runway then + if runway then Z=UTILS.Rotate2D(Z, -self.carrierparam.rwyangle) - end - + end + -- Projection of player pos on z component. local z=UTILS.VecDot(Z, C) - + --- local lineup=math.deg(math.atan2(z, x)) @@ -11140,14 +11164,14 @@ function AIRBOSS:_Lineup(unit, runway) -- 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)) ]] - + return lineup end @@ -11157,11 +11181,11 @@ end -- @return #number Altitude in meters wrt carrier height. function AIRBOSS:_GetAltCarrier(unit) - -- TODO: Value 4 meters is for the Hornet. Adjust for Harrier, A4E and + -- TODO: Value 4 meters is for the Hornet. Adjust for Harrier, A4E and - -- Altitude of unit corrected by the deck height of the carrier. + -- Altitude of unit corrected by the deck height of the carrier. local h=unit:GetAltitude()-self.carrierparam.deckheight-2 - + return h end @@ -11172,30 +11196,30 @@ function AIRBOSS:_GetOptLandingCoordinate() -- Stern coordinate. local stern=self:_GetSternCoord() - + -- Final bearing. local FB=self:GetFinalBearing(false) - + if self.carriertype==AIRBOSS.CarrierType.TARAWA then - - -- Landing 100 ft abeam, 120 ft alt. + + -- Landing 100 ft abeam, 120 ft alt. stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) - + -- Alitude 120 ft. stern:SetAltitude(UTILS.FeetToMeters(120)) - + else - + -- 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 w3=self.carrierparam.wire3 stern=stern:Translate(w3, FB, true) end - + -- Add 2 meters to account for aircraft height. stern.y=stern.y+2 - + end return stern @@ -11208,17 +11232,17 @@ function AIRBOSS:_GetLandingSpotCoordinate() -- Stern coordinate. local stern=self:_GetSternCoord() - + if self.carriertype==AIRBOSS.CarrierType.TARAWA then - - -- Landing 100 ft abeam, 120 alt. + + -- Landing 100 ft abeam, 120 alt. local hdg=self:GetHeading() - + -- Primary landing spot 7.5 stern=stern:Translate(57, hdg):SetAltitude(self.carrierparam.deckheight) - + end - + return stern end @@ -11226,22 +11250,22 @@ end -- @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:GetHeading(magnetic) +function AIRBOSS:GetHeading(magnetic) self:F3({magnetic=magnetic}) - + -- Carrier heading local hdg=self.carrier:GetHeading() - + -- Include magnetic declination. if magnetic then hdg=hdg-self.magvar end - + -- Adjust negative values. if hdg<0 then hdg=hdg+360 - end - + end + return hdg end @@ -11264,19 +11288,19 @@ function AIRBOSS:GetWind(alt, magnetic, coord) -- Current position of the carrier or input. local cv=coord or self:GetCoordinate() - + -- Wind direction and speed. By default at 50 meters ASL. local Wdir, Wspeed=cv:GetWind(alt or 50) - + -- Include magnetic declination. if magnetic then Wdir=Wdir-self.magvar -- Adjust negative values. if Wdir<0 then Wdir=Wdir+360 - end + end end - + return Wdir, Wspeed end @@ -11287,39 +11311,39 @@ end -- @return #number Wind component perpendicular to runway in m/s. -- @return #number Total wind strength in m/s. function AIRBOSS:GetWindOnDeck(alt) - + -- Position of carrier. local cv=self:GetCoordinate() - + -- Velocity vector of carrier. local vc=self.carrier:GetVelocityVec3() - + -- Carrier orientation X. local xc=self.carrier:GetOrientationX() - + -- Carrier orientation Z. local zc=self.carrier:GetOrientationZ() - + -- Rotate back so that angled deck points to wind. xc=UTILS.Rotate2D(xc, -self.carrierparam.rwyangle) zc=UTILS.Rotate2D(zc, -self.carrierparam.rwyangle) - + -- Wind (from) vector local vw=cv:GetWindWithTurbulenceVec3(alt or 50) - + -- Total wind velocity vector. -- Carrier velocity has to be negative. If carrier drives in the direction the wind is blowing from, we have less wind in total. local vT=UTILS.VecSubstract(vw, vc) -- || Parallel component. local vpa=UTILS.VecDot(vT,xc) - + -- == Perpendicular component. - local vpp=UTILS.VecDot(vT,zc) - - -- Strength. + local vpp=UTILS.VecDot(vT,zc) + + -- Strength. local vabs=UTILS.VecNorm(vT) - + -- We return positive values as head wind and negative values as tail wind. --TODO: Check minus sign. return -vpa, vpp, vabs @@ -11335,27 +11359,27 @@ function AIRBOSS:GetHeadingIntoWind(magnetic, coord) -- Get direction the wind is blowing from. This is where we want to go. local windfrom, vwind=self:GetWind(nil, nil, coord) - + -- Actually, we want the runway in the wind. local intowind=windfrom-self.carrierparam.rwyangle - + -- If no wind, take current heading. if vwind<0.1 then intowind=self:GetHeading() end - + -- Magnetic heading. if magnetic then intowind=intowind-self.magvar end - + -- Adjust negative values. if intowind<0 then intowind=intowind+360 - end + end return intowind -end +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. @@ -11369,32 +11393,32 @@ 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 *TrueNorth* parameter to true. +-- The true bearing can be obtained by setting the *TrueNorth* parameter to true. -- @param #AIRBOSS self -- @param #boolean magnetic If true, magnetic FB is returned. -- @return #number FB in degrees. function AIRBOSS:GetFinalBearing(magnetic) - -- First get the heading. - local fb=self:GetHeading(magnetic) - + -- First get the heading. + local fb=self:GetHeading(magnetic) + -- Final baring = BRC including angled deck. fb=fb+self.carrierparam.rwyangle - + -- Adjust negative values. if fb<0 then fb=fb+360 end - + return fb end --- 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. @@ -11405,38 +11429,38 @@ function AIRBOSS:GetRadial(case, magnetic, offset, inverse) -- 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 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. radial=self:GetFinalBearing(magnetic)-180 - + -- Holding offset angle (+-15 or 30 degrees usually) if offset then radial=radial+self.holdingoffset end - + end - + -- Adjust for negative values. if radial<0 then radial=radial+360 @@ -11444,39 +11468,39 @@ function AIRBOSS:GetRadial(case, magnetic, offset, inverse) -- Inverse? if inverse then - + -- Inverse radial radial=radial-180 -- Adjust for negative values. if radial<0 then radial=radial+360 - end - + end + end return radial end ---- Get difference between to headings in degrees taking into accound the [0,360) periodocity. +--- Get difference between to headings in degrees taking into accound the [0,360) periodocity. -- @param #AIRBOSS self -- @param #number hdg1 Heading one. -- @param #number hdg2 Heading two. -- @return #number Difference between the two headings in degrees. function AIRBOSS:_GetDeltaHeading(hdg1, hdg2) - + local V={} --DCS#Vec3 V.x=math.cos(math.rad(hdg1)) V.y=0 V.z=math.sin(math.rad(hdg1)) - + local W={} --DCS#Vec3 W.x=math.cos(math.rad(hdg2)) W.y=0 W.z=math.sin(math.rad(hdg2)) - + local alpha=UTILS.VecAngle(V,W) - + return alpha end @@ -11486,26 +11510,26 @@ end -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Player 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. +-- @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() - + -- Include runway angle. if runway then vC=UTILS.Rotate2D(vC, -self.carrierparam.rwyangle) end - + -- 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. + + -- 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 degrees. - local rhdg=UTILS.VecAngle(vC,vP) - + local rhdg=UTILS.VecAngle(vC,vP) + -- Return heading in degrees. return rhdg end @@ -11513,15 +11537,15 @@ end --- Get relative velocity of player unit wrt to carrier -- @param #AIRBOSS self -- @param Wrapper.Unit#UNIT unit Player unit. --- @return #number Relative velocity in m/s. +-- @return #number Relative velocity in m/s. function AIRBOSS:_GetRelativeVelocity(unit) local vC=self.carrier:GetVelocityVec3() local vP=unit:GetVelocityVec3() - + -- Only X-Z plane is necessary here. vC.y=0 ; vP.y=0 - + local v=UTILS.VecSubstract(vP, vC) return UTILS.VecNorm(v),v @@ -11529,7 +11553,7 @@ end --- Calculate distances between carrier and aircraft unit. --- @param #AIRBOSS self +-- @param #AIRBOSS self -- @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. @@ -11539,37 +11563,37 @@ function AIRBOSS:_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) - - + + -- Not exactly sure any more what I wanted to calculate here. local phi=math.deg(math.atan2(dz,dx)) - + -- Correct for negative values. if phi<0 then phi=phi+360 end - + return dx,dz,rho,phi end @@ -11586,12 +11610,12 @@ function AIRBOSS:_CheckLimits(X, Z, check) 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", + 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:T3(self.lid..text) @@ -11612,7 +11636,7 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- Advice time. local advice=0 - + -- Glideslope high/low calls. if glideslopeError>self.gle.HIGH then --1.5 then -- "You're high!" @@ -11634,7 +11658,7 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) -- "Good altitude." end - + -- Lineup left/right calls. if lineupErrorself.lue.RIGHT then --3 then -- "Right for lineup!" self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, true, nil, nil, true) - advice=advice+self.LSOCall.RIGHTFORLINEUP.duration + advice=advice+self.LSOCall.RIGHTFORLINEUP.duration elseif lineupError>self.lue.Right then -- 1 then -- "Right for lineup." self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, false, nil, nil, true) @@ -11655,10 +11679,10 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) else -- "Good lineup." end - + -- Get current AoA. local AOA=playerData.unit:GetAoA() - + -- Get aircraft AoA parameters. local acaoa=self:_GetAircraftAoA(playerData) @@ -11672,7 +11696,7 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) elseif AOA>acaoa.Slow then -- "Your're slow." self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, false, nil, nil, true) - advice=advice+self.LSOCall.SLOW.duration + advice=advice+self.LSOCall.SLOW.duration --S="SLO" elseif AOA>acaoa.OnSpeedMax then -- No call. @@ -11692,22 +11716,22 @@ function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) --S=little("F") end end - + -- Set last time. playerData.Tlso=timer.getTime() end --- Grade player time in the groove - from turning to final until touchdown. --- +-- -- If time --- --- * < 9 seconds: No Grade "--" +-- +-- * < 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\_". +-- 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. @@ -11716,7 +11740,7 @@ function AIRBOSS:_EvalGrooveTime(playerData) -- Time in groove. local t=playerData.Tgroove - + local grade="" if t<9 then grade="--" @@ -11729,12 +11753,12 @@ function AIRBOSS:_EvalGrooveTime(playerData) else grade="--" end - + -- The unicorn! if t>=16.4 and t<=16.6 then grade="_OK_" end - + return grade end @@ -11745,7 +11769,7 @@ end -- @return #number Points. -- @return #string LSO analysis of flight path. function AIRBOSS:_LSOgrade(playerData) - + --- Count deviations. local function count(base, pattern) return select(2, string.gsub(base, pattern, "")) @@ -11756,16 +11780,16 @@ function AIRBOSS:_LSOgrade(playerData) 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 - + -- Count 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 grade local points if N==0 then @@ -11776,7 +11800,7 @@ function AIRBOSS:_LSOgrade(playerData) else if nL>0 then -- Larger deviations ==> "No grade" 2.0 points. - grade="--" + grade="--" points=2.0 elseif nN>0 then -- No larger but average deviations ==> "Fair Pass" Pass with average deviations and corrections. @@ -11788,11 +11812,11 @@ function AIRBOSS:_LSOgrade(playerData) points=4.0 end end - - -- Replace" )"( and "__" + + -- Replace" )"( and "__" G=G:gsub("%)%(", "") - G=G:gsub("__","") - + G=G:gsub("__","") + -- Debug info local text="LSO grade:\n" text=text..G.."\n" @@ -11802,7 +11826,7 @@ function AIRBOSS:_LSOgrade(playerData) text=text.."# of normal deviations = "..nN.."\n" text=text.."# of small deviations ( = "..nS.."\n" self:T2(self.lid..text) - + -- Special cases. if playerData.wop then --------------------- @@ -11819,7 +11843,7 @@ function AIRBOSS:_LSOgrade(playerData) grade="WOP" points=2.0 G="n/a" - end + end elseif playerData.wofd then ----------------------- -- Foul Deck Waveoff -- @@ -11878,7 +11902,7 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) local function underline(text) return string.format("_%s_", text) end - + -- Groove Data. local fdata=playerData.groove[groovestep] --#AIRBOSS.GrooveData @@ -11894,7 +11918,7 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) local GSE=fdata.GSE local LUE=fdata.LUE local ROL=fdata.Roll - + -- Aircraft specific AoA values. local acaoa=self:_GetAircraftAoA(playerData) @@ -11929,7 +11953,7 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) elseif GSEself.lue.RIGHT then @@ -11945,7 +11969,7 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep) elseif LUE %d=Zmax", Z, pos.Zmax)) abort=true end - + return abort end @@ -12096,7 +12120,7 @@ function AIRBOSS:_TooFarOutText(X, Z, posData) -- Intro. local text="you are too " - + -- X text. local xtext=nil if posData.Xmin and X FB for Case II or BRC for Case III. + -- Get inverse magnetic radial without offset ==> FB for Case II or BRC for Case III. local radial=self:GetRadial(playerData.case, true, false, true) local turn="right" if self.holdingoffset<0 then @@ -12283,9 +12307,9 @@ function AIRBOSS:_PlayerHint(playerData, delay, soundoff) end hint=hint..string.format("\nTurn %s and select TACAN %03d°.", turn, radial) end - + end - + -- DIRTUP additonal info. if playerData.step==AIRBOSS.PatternStep.DIRTYUP then if playerData.difficulty==AIRBOSS.Difficulty.EASY then @@ -12297,9 +12321,9 @@ function AIRBOSS:_PlayerHint(playerData, delay, soundoff) end end end - + -- BULLSEYE additonal info. - if playerData.step==AIRBOSS.PatternStep.BULLSEYE then + if playerData.step==AIRBOSS.PatternStep.BULLSEYE then -- Hint follow the needles. if playerData.difficulty==AIRBOSS.Difficulty.EASY then if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then @@ -12309,7 +12333,7 @@ function AIRBOSS:_PlayerHint(playerData, delay, soundoff) end end end - + -- Message to player. if hint~="" then local text=string.format("%s%s", playerData.step, hint) @@ -12323,34 +12347,34 @@ end -- @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 + + -- Set step. + step=step or playerData.step -- Message is only for "Flight Students". if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.showhints 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 %d ft", UTILS.MetersToFeet(alt)) end - + -- AoA. if aoa then hint=hint..string.format("\nAoA %.1f", self:_AoADeg2Units(playerData, aoa)) end - + -- Speed. if speed then hint=hint..string.format("\nSpeed %d knots", UTILS.MpsToKnots(speed)) end - + -- Distance to the boat. if dist then hint=hint..string.format("\nDistance to the boat %.1f NM", UTILS.MetersToNM(dist)) @@ -12362,7 +12386,7 @@ function AIRBOSS:_StepHint(playerData, step) hint=hint.."\nWing Sweep 20°, Gear DOWN < 280 KIAS." end end - + -- Abeam. if step==AIRBOSS.PatternStep.ABEAM then if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then @@ -12373,18 +12397,18 @@ function AIRBOSS:_StepHint(playerData, step) hint=hint.."\nDirty up! Gear DOWN, flaps DOWN. Check hook down." end 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:%s", step, hint) - + -- Send hint to player. self:MessageToPlayer(playerData, text, "AIRBOSS", "", nil, false, 1) - + end - + end end @@ -12404,16 +12428,16 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) -- Player altitude. local altitude=playerData.unit:GetAltitude() - + -- Get relative score. local lowscore, badscore=self:_GetGoodBadScore(playerData) - + -- Altitude error +-X% local _error=(altitude-altopt)/altopt*100 - + -- Radio call for flight students. local radiocall=nil --#AIRBOSS.RadioCall - + local hint="" if _error>badscore then --hint=string.format("You're high.") @@ -12430,7 +12454,7 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) else hint=string.format("Good altitude. ") end - + -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then -- Also inform students about the optimal altitude. @@ -12442,10 +12466,10 @@ function AIRBOSS:_AltitudeCheck(playerData, altopt) -- No hint at all for the pros. hint="" end - + -- Debrief text. local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(altopt)) - + return hint, debrief,radiocall end @@ -12461,19 +12485,19 @@ function AIRBOSS:_AoACheck(playerData, optaoa) 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-optaoa)/optaoa*100 - + local _error=(aoa-optaoa)/optaoa*100 + -- Get aircraft AoA parameters. local aircraftaoa=self:_GetAircraftAoA(playerData) - + -- Radio call for flight students. local radiocall=nil --#AIRBOSS.RadioCall @@ -12510,10 +12534,10 @@ function AIRBOSS:_AoACheck(playerData, optaoa) -- No hint at all for the pros. hint="" end - + -- Debriefing text. local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.", self:_AoADeg2Units(playerData, aoa), _error, self:_AoADeg2Units(playerData, optaoa)) - + return hint, debrief,radiocall end @@ -12532,16 +12556,16 @@ function AIRBOSS:_SpeedCheck(playerData, speedopt) -- 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 - + -- Radio call for flight students. - local radiocall=nil --#AIRBOSS.RadioCall - + local radiocall=nil --#AIRBOSS.RadioCall + local hint="" if _error>badscore then --hint=string.format("You're fast.") @@ -12558,7 +12582,7 @@ function AIRBOSS:_SpeedCheck(playerData, speedopt) 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 speed is %d knots.", UTILS.MpsToKnots(speedopt)) @@ -12569,10 +12593,10 @@ function AIRBOSS:_SpeedCheck(playerData, speedopt) -- No hint at all for pros. hint="" end - + -- Debrief text. local debrief=string.format("Speed %d knots = %d%% deviation from %d knots.", UTILS.MpsToKnots(speed), _error, UTILS.MpsToKnots(speedopt)) - + return hint, debrief, radiocall end @@ -12588,20 +12612,20 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) if optdist==nil then return nil, nil end - + -- Distance to carrier. local distance=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) -- Get relative score. local lowscore, badscore = self:_GetGoodBadScore(playerData) - + -- Altitude error +-X% local _error=(distance-optdist)/optdist*100 - + local hint if _error>badscore then hint=string.format("You're too far from the boat!") - elseif _error>lowscore then + elseif _error>lowscore then 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!") @@ -12610,7 +12634,7 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) else hint=string.format("Good distance to the boat.") end - + -- Extend or decrease depending on skill. if playerData.difficulty==AIRBOSS.Difficulty.EASY then -- Also inform students about optimal value. @@ -12625,7 +12649,7 @@ function AIRBOSS:_DistanceCheck(playerData, optdist) -- Debriefing text. local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(optdist)) - + return hint, debrief, nil end @@ -12648,39 +12672,39 @@ 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) - + -- Insert points to table of all points until player landed. if points and points>=0 then table.insert(playerData.points, points) end - + -- 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 - + end + -- 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 + + -- My LSO grade. + local mygrade={} --#AIRBOSS.LSOgrade mygrade.grade=grade mygrade.points=points mygrade.details=analysis @@ -12703,109 +12727,112 @@ function AIRBOSS:_Debrief(playerData) if os then mygrade.osdate=os.date() --os.date("%d.%m.%Y") end - + -- Save trap sheet. if playerData.trapon and self.trapsheet then self:_SaveTrapSheet(playerData, mygrade) end - + -- Add LSO grade to player grades table. table.insert(self.playerscores[playerData.name], mygrade) + -- Trigger grading event. + self:LSOGrade(playerdata, mygrade) + -- 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.wop or playerData.wofd) then - + -- Wire trapped. Not if pattern WI. if playerData.wire and playerData.wire<=4 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.Tgroove<=360 and playerData.case<3 then text=text..string.format("\nTime in the groove %.1f 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.") end - + -- Message. self:MessageToPlayer(playerData, text, "LSO", "", 30, true) - - + + -- Set step to undefined and check if other cases apply. playerData.step=AIRBOSS.PatternStep.UNDEFINED -- Check what happened? if playerData.wop 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? -- 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... - - -- Can become nil when I crashed and changed to observer. Which events are captured? Nil check for unit? + -- 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 - + -- Heading and distance tip. local heading, distance - + if playerData.case==1 or playerData.case==2 then - - -- Next step: Initial again. + + -- Next step: Initial again. playerData.step=AIRBOSS.PatternStep.INITIAL - + -- Create a point 3.0 NM astern for re-entry. 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(initial) distance=playerData.unit:GetCoordinate():Get2DDistance(initial) - + 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 - + -- 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 %03d° for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 5) else - + -- Unit does not seem to be alive! -- TODO: What now? self:E(self.lid..string.format("ERROR: Player unit not alive!")) - + end - + elseif playerData.wofd then --------------- @@ -12818,37 +12845,37 @@ function AIRBOSS:_Debrief(playerData) playerData.step=AIRBOSS.PatternStep.BOLTER else - + -- Welcome aboard! self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) - + -- Airboss talkto! local text=string.format("deck was fouled but you landed anyway. Airboss wants to talk to you!") self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) - + end - + elseif playerData.owo then ------------------ -- Own Wave Off -- ------------------ - + if playerData.unit:InAir() then - + -- Bolter pattern. Then Abeam or bullseye. playerData.step=AIRBOSS.PatternStep.BOLTER - + else - + -- Welcome aboard! -- NOTE: This should not happen as owo is only triggered if player flew past the carrier. self:E(self.lid.."ERROR: player landed when OWO was issues. This should not happen. Please report!") self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) - - end - + end + + elseif playerData.waveoff then -------------- @@ -12856,47 +12883,47 @@ function AIRBOSS:_Debrief(playerData) -------------- if playerData.unit:InAir() then - + -- Bolter pattern. Then Abeam or bullseye. playerData.step=AIRBOSS.PatternStep.BOLTER - + else - + -- Welcome aboard! self:Sound2Player(playerData, self.LSORadio, self.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, nil, false, 3) - + end - + elseif playerData.boltered then - + -------------- -- Boltered -- - -------------- + -------------- if playerData.unit:InAir() then - + -- Bolter pattern. Then Abeam or bullseye. playerData.step=AIRBOSS.PatternStep.BOLTER - + end - + elseif playerData.landed then - + ------------ -- Landed -- - ------------ - + ------------ + if not playerData.unit:InAir() then - + -- Welcome aboard! self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) - + end - + else -- Message to player. @@ -12904,30 +12931,30 @@ function AIRBOSS:_Debrief(playerData) -- Next step. playerData.step=AIRBOSS.PatternStep.UNDEFINED - + end - + -- Player landed and is not in air anymore. if playerData.landed and not playerData.unit:InAir() then -- Set recovered flag. self:_RecoveredElement(playerData.unit) - - -- Check if all elements + + -- Check if all elements self:_CheckSectionRecovered(playerData) 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) - + -- Auto save player results. if self.autosave and mygrade.finalscore then self:Save(self.autosavepath, self.autosavefile) @@ -12944,51 +12971,51 @@ end -- @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) - +function AIRBOSS:_CheckCollisionCoord(coordto, coordfrom) + -- Increment in meters. local dx=100 - + -- From coordinate. Default 500 in front of the carrier. - local d=0 + local d=0 if coordfrom then d=0 else d=250 coordfrom=self:GetCoordinate():Translate(d, self:GetHeading()) - end - + 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! + 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)) @@ -12996,7 +13023,7 @@ function AIRBOSS:_CheckCollisionCoord(coordto, coordfrom) 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 @@ -13009,16 +13036,16 @@ 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 @@ -13035,34 +13062,34 @@ function AIRBOSS:_Pathfinder() -- 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 path is clear, we start a little detour. if not collision then self:CarrierDetour(fromcoord) return end - + distance=distance+500 - end + end end end @@ -13092,50 +13119,50 @@ function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed, tcoord) -- Current coordinate of the carrier. local pos0=self:GetCoordinate() - + -- Current speed in knots. local vel0=self.carrier:GetVelocityKNOTS() - + -- Default. If speed is not given we take the current speed but at least 5 knots. speed=speed or math.max(vel0, 5) - + -- Speed in km/h. At least 2 knots. local speedkmh=math.max(UTILS.KnotsToKmph(speed), UTILS.KnotsToKmph(2)) - + -- Turn speed in km/h. At least 10 knots. local cspeedkmh=math.max(self.carrier:GetVelocityKMH(), UTILS.KnotsToKmph(10)) - + -- U-turn speed in km/h. local uspeedkmh=UTILS.KnotsToKmph(uspeed or speed) - + -- Waypoint table. local wp={} - + -- Waypoint at current position. table.insert(wp, pos0:WaypointGround(cspeedkmh)) - + -- Waypooint to help the turn. if tcoord then table.insert(wp, tcoord:WaypointGround(cspeedkmh)) end - + -- Detour waypoint. table.insert(wp, coord:WaypointGround(speedkmh)) - + -- U-turn waypoint. If enabled, go back to where you came from. if uturn then table.insert(wp, pos0:WaypointGround(uspeedkmh)) 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 if tcoord then @@ -13146,10 +13173,10 @@ function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed, tcoord) pos0:MarkToAll(string.format("Detour U-turn WP. Speed %.1f knots", UTILS.KmphToKnots(uspeedkmh))) end end - + -- Detour switch true. self.detour=true - + -- Route carrier into the wind. self.carrier:Route(wp) end @@ -13161,68 +13188,68 @@ end -- @param #boolean uturn Make U-turn and go back to initial after downwind leg. -- @return #AIRBOSS self function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) - + -- Wind speed. local _,vwind=self:GetWind() - + -- Speed of carrier in m/s but at least 2 knots. local vtot=math.max(vdeck-vwind, UTILS.KnotsToMps(2)) - + -- Distance to travel local dist=vtot*time - + -- Speed in knots local speedknots=UTILS.MpsToKnots(vtot) local distNM=UTILS.MetersToNM(dist) - + -- Debug output self:I(self.lid..string.format("Carrier steaming into the wind (%.1f kts). Distance=%.1f NM, Speed=%.1f knots, Time=%d sec.", UTILS.MpsToKnots(vwind), distNM, speedknots, time)) -- Get heading into the wind accounting for angled runway. local hiw=self:GetHeadingIntoWind() - + -- Current heading. local hdg=self:GetHeading() - + -- Heading difference. local deltaH=self:_GetDeltaHeading(hdg, hiw) - + local Cv=self:GetCoordinate() - + local Ctiw=nil --Core.Point#COORDINATE local Csoo=nil --Core.Point#COORDINATE - + -- Define path depending on turn angle. if deltaH<45 then -- Small turn. - + -- Point in the right direction to help turning. Csoo=Cv:Translate(750, hdg):Translate(750, hiw) - + -- Heading into wind from Csoo. local hsw=self:GetHeadingIntoWind(false, Csoo) - + -- Into the wind coord. Ctiw=Csoo:Translate(dist, hsw) - + elseif deltaH<90 then -- Medium turn. - + -- Point in the right direction to help turning. Csoo=Cv:Translate(900, hdg):Translate(900, hiw) - + -- Heading into wind from Csoo. local hsw=self:GetHeadingIntoWind(false, Csoo) - + -- Into the wind coord. Ctiw=Csoo:Translate(dist, hsw) - + elseif deltaH<135 then -- Large turn backwards. - - -- Point in the right direction to help turning. + + -- Point in the right direction to help turning. Csoo=Cv:Translate(1100, hdg-90):Translate(1000, hiw) - + -- Heading into wind from Csoo. local hsw=self:GetHeadingIntoWind(false, Csoo) @@ -13234,25 +13261,25 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) -- Point in the right direction to help turning. Csoo=Cv:Translate(1200, hdg-90):Translate(1000, hiw) - + -- Heading into wind from Csoo. local hsw=self:GetHeadingIntoWind(false, Csoo) -- Into the wind coord. Ctiw=Csoo:Translate(dist, hsw) - + end - - + + -- Return to coordinate if collision is detected. self.Creturnto=self:GetCoordinate() - + -- Next waypoint. local nextwp=self:_GetNextWaypoint() - + -- For downwind, we take the velocity at the next WP. local vdownwind=UTILS.MpsToKnots(nextwp:GetVelocity()) - + -- Make sure we move at all in case the speed at the waypoint is zero. if vdownwind<1 then vdownwind=10 @@ -13260,7 +13287,7 @@ function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) -- Let the carrier make a detour from its route but return to its current position. self:CarrierDetour(Ctiw, speedknots, uturn, vdownwind, Csoo) - + -- Set switch that we are currently turning into the wind. self.turnintowind=true @@ -13273,18 +13300,18 @@ end -- @return #number Number of waypoint. function AIRBOSS:_GetNextWaypoint() - -- Next waypoint. + -- Next waypoint. local Nextwp=nil if self.currentwp==#self.waypoints then Nextwp=1 else Nextwp=self.currentwp+1 end - + -- Debug output local text=string.format("Current WP=%d/%d, next WP=%d", self.currentwp, #self.waypoints, Nextwp) self:T2(self.lid..text) - + -- Next waypoint. local nextwp=self.waypoints[Nextwp] --Core.Point#COORDINATE @@ -13305,23 +13332,23 @@ function AIRBOSS:_InitWaypoints() -- Set waypoint table. for i,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 - + end - + return self end @@ -13335,48 +13362,48 @@ function AIRBOSS:_PatrolRoute(n) -- 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 - + -- Init array. self.waypoints={} -- Set waypoint table. for i,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 - + end - + -- Current waypoint is 1. self.currentwp=n or 1 -- Route carrier group. CarrierGroup:Route(Waypoints) - + return self end @@ -13390,32 +13417,32 @@ function AIRBOSS:_PatrolRoute(n) -- Get next waypoint coordinate and number. local nextWP, N=self:_GetNextWaypoint() - + -- Default resume is to next waypoint. n=n or N -- Get carrier group. local CarrierGroup=self.carrier:GetGroup() - + -- Waypoints table. local Waypoints={} - + -- Create a waypoint from the current coordinate. local wp=self:GetCoordinate():WaypointGround(CarrierGroup:GetVelocityKMH()) - + -- Add current position as first waypoint. table.insert(Waypoints, wp) - + -- Loop over waypoints. for i=n,#self.waypoints do local coord=self.waypoints[i] --Core.Point#COORDINATE - + -- Create a waypoint from the coordinate. local wp=coord:WaypointGround(UTILS.MpsToKmph(coord.Velocity)) - + -- Passing waypoint taskfunction local TaskPassingWP=CarrierGroup:TaskFunction("AIRBOSS._PassingWaypoint", self, i, #self.waypoints) - + -- Call task function when carrier arrives at waypoint. CarrierGroup:SetTaskWaypoint(wp, TaskPassingWP) @@ -13425,7 +13452,7 @@ function AIRBOSS:_PatrolRoute(n) -- Route carrier group. CarrierGroup:Route(Waypoints) - + return self end @@ -13439,34 +13466,34 @@ 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() - + -- Next waypoint. local nextWP=self:_GetNextWaypoint() - + -- Distance to next waypoint. local s=p:Get2DDistance(nextWP) - + -- 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 @@ -13476,36 +13503,36 @@ function AIRBOSS:_CheckCarrierTurning() -- Current orientation of carrier. local vNew=self.carrier:GetOrientationX() - + -- Last orientation from 30 seconds ago. local vLast=self.Corientlast - + -- We only need the X-Z plane. vNew.y=0 ; vLast.y=0 - + -- 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.Corientlast=vNew - + -- Carrier is turning when its heading changed by at least one degree since last check. local turning=math.abs(deltaLast)>=1 - + -- Check if turning stopped. (Carrier was turning but is not any more.) if self.turning and not turning then - + -- Get final bearing. local FB=self:GetFinalBearing(true) - + -- Marshal radio call: "99, new final bearing XYZ degrees." self:_MarshalCallNewFinalBearing(FB) - - end - + + end + -- Check if turning started. (Carrier was not turning and is now.) if turning and not self.turning then - + -- Get heading. local hdg if self.turnintowind then @@ -13515,17 +13542,17 @@ function AIRBOSS:_CheckCarrierTurning() -- We turn towards the next waypoint. hdg=self:GetCoordinate():HeadingTo(self:_GetNextWaypoint()) end - + -- Magnetic! hdg=hdg-self.magvar if hdg<0 then hdg=360+hdg - end - + end + -- Radio call: "99, Carrier starting turn to heading XYZ degrees". self:_MarshalCallCarrierTurnTo(hdg) end - + -- Update turning. self.turning=turning end @@ -13540,40 +13567,40 @@ function AIRBOSS:_CheckPatternUpdate() -- 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°. local Hupdate=5 - + ----------------------- -- Time Update Check -- ----------------------- - + -- Time since last pattern update local dt=timer.getTime()-self.Tpupdate - + -- Check whether at least 10 min between updates and not turning currently. if dt=Dupdate then self:T(self.lid..string.format("Carrier position changed by %.1f NM.", UTILS.MetersToNM(dist))) Dchange=true end - + ---------------------------- -- Update Marshal Flights -- ---------------------------- -- 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) end - + end - + -- Reset parameters for next update check. self.Corientation=vNew self.Cposition=pos @@ -13634,27 +13661,27 @@ 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 - + -- Passing Waypoint event. airboss:PassingWaypoint(i) - + -- Reactivate beacons. --airboss:_ActivateBeacons() - + -- If final waypoint reached, do route all over again. if i==final and final>1 and airboss.adinfinitum then airboss:_PatrolRoute() @@ -13669,106 +13696,106 @@ function AIRBOSS._ResumeRoute(group, airboss, gotocoord) -- Get next waypoint local nextwp,Nextwp=airboss:_GetNextWaypoint() - + -- Speed set at waypoint. local speedkmh=nextwp.Velocity*3.6 - + -- If speed at waypoint is zero, we set it to 10 knots. if speedkmh<1 then speedkmh=UTILS.KnotsToKmph(10) end - + -- Waypoints array. local waypoints={} - + -- Current position. local c0=group:GetCoordinate() - + -- Current positon as first waypoint. local wp0=c0:WaypointGround(speedkmh) table.insert(waypoints, wp0) - + -- First goto this coordinate. if gotocoord then - + --gotocoord:MarkToAll(string.format("Goto waypoint speed=%.1f km/h", speedkmh)) - + local headingto=c0:HeadingTo(gotocoord) - + local hdg1=airboss:GetHeading() local hdg2=c0:HeadingTo(gotocoord) local delta=airboss:_GetDeltaHeading(hdg1, hdg2) - + --env.info(string.format("FF hdg1=%d, hdg2=%d, delta=%d", hdg1, hdg2, delta)) - - -- Add additional turn points + + -- Add additional turn points if delta>90 then - + -- Turn radius 3 NM. local turnradius=UTILS.NMToMeters(3) local gotocoordh=c0:Translate(turnradius, hdg1+45) --gotocoordh:MarkToAll(string.format("Goto help waypoint 1 speed=%.1f km/h", speedkmh)) - + local wp=gotocoordh:WaypointGround(speedkmh) table.insert(waypoints, wp) gotocoordh=c0:Translate(turnradius, hdg1+90) --gotocoordh:MarkToAll(string.format("Goto help waypoint 2 speed=%.1f km/h", speedkmh)) - + wp=gotocoordh:WaypointGround(speedkmh) table.insert(waypoints, wp) - + end - + local wp1=gotocoord:WaypointGround(speedkmh) - table.insert(waypoints, wp1) - + table.insert(waypoints, wp1) + end - + -- Debug message. local text=string.format("Carrier is resuming route. Next waypoint %d, Speed=%.1f knots.", Nextwp, UTILS.KmphToKnots(speedkmh)) - + -- Debug message. MESSAGE:New(text,10):ToAllIf(airboss.Debug) airboss:I(airboss.lid..text) - + -- 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 - + -- If speed is zero we set it to 10 knots. if speed<1 then speed=UTILS.KnotsToKmph(10) end - + --coord:MarkToAll(string.format("Resume route WP %d, speed=%.1f km/h", i, speed)) - + -- 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) + group:Route(waypoints) end --- Function called when a group has reached the holding zone. @@ -13781,12 +13808,12 @@ function AIRBOSS._ReachedHoldingZone(group, airboss, flight) 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 @@ -13804,32 +13831,32 @@ function AIRBOSS._TaskFunctionMarshalAI(group, airboss, flight) local text=string.format("Flight %s is send to marshal.", group:GetName()) MESSAGE:New(text,10):ToAllIf(airboss.Debug) airboss:T(airboss.lid..text) - + -- Get the next free stack for current recovery case. local stack=airboss:_GetFreeStack(flight.ai) - + if stack then - + -- Send AI to marshal stack. airboss:_MarshalAI(flight, stack) - + else - + -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available. if not airboss:_InQueue(airboss.Qwaiting, flight.group) then airboss:_WaitAI(flight) end - + end - + -- If it came from refueling. if flight.refueling==true then airboss:I(airboss.lid..string.format("Flight group %s finished refueling task.", flight.groupname)) end - + -- Not refueling any more in case it was. flight.refueling=false - + end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -13856,7 +13883,7 @@ function AIRBOSS:_GetACNickname(actype) elseif actype==AIRBOSS.AircraftCarrier.S3B or actype==AIRBOSS.AircraftCarrier.S3BTANKER then nickname="Viking" end - + return nickname end @@ -13875,20 +13902,20 @@ end -- @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 @@ -13901,14 +13928,14 @@ function AIRBOSS:_GetOnboardNumbers(group, playeronly) -- 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:T2(self.lid..text) - + return numbers end @@ -13922,7 +13949,7 @@ function AIRBOSS:_GetTowerFrequency() -- Get Template of Strike Group local striketemplate=self.carrier:GetGroup():GetTemplate() - + -- Find the carrier unit. for _,unit in pairs(striketemplate.units) do if self.carrier:GetName()==unit.name then @@ -13933,11 +13960,11 @@ function AIRBOSS:_GetTowerFrequency() end --- 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 Error margin for still being okay. @@ -13948,15 +13975,15 @@ function AIRBOSS:_GetGoodBadScore(playerData) local badscore if playerData.difficulty==AIRBOSS.Difficulty.EASY then lowscore=10 - badscore=20 + badscore=20 elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then lowscore=5 - badscore=10 + badscore=10 elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then lowscore=2.5 badscore=5 end - + return lowscore, badscore end @@ -13965,7 +13992,7 @@ end -- @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) - + -- Get aircraft type name local aircrafttype=unit:GetTypeName() @@ -13977,23 +14004,23 @@ function AIRBOSS:_IsCarrierAircraft(unit) return false end end - + -- Also only Harriers can land on the Tarawa. if self.carriertype==AIRBOSS.CarrierType.TARAWA then if aircrafttype~=AIRBOSS.AircraftCarrier.AV8B then return false end end - + -- Loop over all other known carrier capable aircraft. for _,actype in pairs(AIRBOSS.AircraftCarrier) do - + -- Check if this is a carrier capable aircraft type. if actype==aircrafttype then return true end end - + -- No carrier carrier aircraft. return false end @@ -14003,10 +14030,10 @@ end -- @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 @@ -14022,7 +14049,7 @@ function AIRBOSS:_IsHuman(group) -- Get all units of the group. local units=group:GetUnits() - + -- Loop over all units. for _,_unit in pairs(units) do -- Check if unit is human. @@ -14043,16 +14070,16 @@ 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 + + -- Fuel state, i.e. what let's local fuelstate=fuel*maxfuel - + -- Debug info. 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 @@ -14068,7 +14095,7 @@ function AIRBOSS:_GetAngels(alt) else return 0 end - + end --- Get unit masses especially fuel from DCS descriptor values. @@ -14085,19 +14112,19 @@ function AIRBOSS:_GetUnitMasses(unit) -- 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: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 @@ -14132,7 +14159,7 @@ function AIRBOSS:_GetPlayerDataGroup(group) return nil end ---- Returns the unit of a player and the player name from the self.players table if it exists. +--- Returns the unit of a player and the player name from the self.players table if it exists. -- @param #AIRBOSS self -- @param #string _unitName Name of the player unit. -- @return Wrapper.Unit#UNIT Unit of player or nil. @@ -14140,20 +14167,20 @@ end function AIRBOSS:_GetPlayerUnit(_unitName) for _,_player in pairs(self.players) do - + local player=_player --#AIRBOSS.PlayerData - + if player.unit and player.unit:GetName()==_unitName then self:T(self.lid..string.format("Found player=%s unit=%s in players table.", tostring(player.name), tostring(_unitName))) return player.unit, player.name end - + end return nil,nil 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 #AIRBOSS self -- @param #string _unitName Name of the player unit. -- @return Wrapper.Unit#UNIT Unit of player or nil. @@ -14162,39 +14189,39 @@ function AIRBOSS:_GetPlayerUnitAndName(_unitName) self:F2(_unitName) if _unitName ~= nil then - + -- First, let's look up all current players. local u,pn=self:_GetPlayerUnit(_unitName) - + -- Return if u and pn then return u, pn end - + -- Get DCS unit from its name. local DCSunit=Unit.getByName(_unitName) - + if DCSunit then - + -- Get player name if any. local playername=DCSunit:getPlayerName() - + -- Unit object. local unit=UNIT:Find(DCSunit) - + -- Debug. self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) - + -- Check if enverything is there. if DCSunit and unit and playername then self:T(self.lid..string.format("Found DCS unit %s with player %s.", tostring(_unitName), tostring(playername))) return unit, playername end - + end - + end - + -- Return nil if we could not find a player. return nil,nil end @@ -14227,14 +14254,14 @@ function AIRBOSS:_GetStaticWeather() -- Clouds --[[ - ["clouds"] = + ["clouds"] = { ["thickness"] = 430, ["density"] = 7, ["base"] = 0, ["iprecptns"] = 1, }, -- end of ["clouds"] - ]] + ]] local clouds=weather.clouds -- Visibilty distance in meters. @@ -14249,11 +14276,11 @@ function AIRBOSS:_GetStaticWeather() if weather.enable_dust==true then dust=weather.dust_density end - + -- Fog --[[ - ["enable_fog"] = false, - ["fog"] = + ["enable_fog"] = false, + ["fog"] = { ["thickness"] = 0, ["visibility"] = 25, @@ -14263,7 +14290,7 @@ function AIRBOSS:_GetStaticWeather() if weather.enable_fog==true then fog=weather.fog end - + return clouds, visibility, fog, dust end @@ -14295,87 +14322,87 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) -- Get current abs time. local time=timer.getAbsTime() - + 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 - + 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.call.duration then - + -- Transmission over. transmission.isplaying=false remove=i - + if transmission.radio.alias=="LSO" then self.TQLSO=time elseif transmission.radio.alias=="MARSHAL" then self.TQMarshal=time - end - + end + else -- still playing - + -- Transmission is still playing. playing=true - + end - + else -- not playing yet - + local Tlast=nil if transmission.interval then if transmission.radio.alias=="LSO" then - Tlast=self.TQLSO + Tlast=self.TQLSO elseif transmission.radio.alias=="MARSHAL" then Tlast=self.TQMarshal end end - + if transmission.interval==nil then - + -- Not playing ==> this will be next. if next==nil then next=transmission end - + else - + if time-Tlast>=transmission.interval then - next=transmission + next=transmission else - + end end - + -- We got a transmission or one with an interval that is not due yet. No need for anything else. if next or Tlast then break end - + end - + else - + -- Transmission not due yet. - - end + + end end - + -- Found a new transmission. if next~=nil and not playing then self:Broadcast(next.radio, next.call, next.loud) next.isplaying=true next.Tstarted=time end - + -- Remove completed calls from queue. if remove then table.remove(radioqueue, remove) @@ -14394,15 +14421,15 @@ end -- @param #boolean pilotcall If true, it's a pilot call. function AIRBOSS:RadioTransmission(radio, call, loud, delay, interval, click, pilotcall) self:F2({radio=radio, call=call, loud=loud, delay=delay, interval=interval, click=click}) - + -- Nil check. if radio==nil or call==nil then return end - + -- Create a new radio transmission item. local transmission={} --#AIRBOSS.Radioitem - + transmission.radio=radio transmission.call=call transmission.Tplay=timer.getAbsTime()+(delay or 0) @@ -14410,33 +14437,33 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay, interval, click, pi transmission.isplaying=false 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, 0.3, pilotcall) end - + -- Play onboard number if receiver has one. if self:_IsOnboard(call.modexreceiver) then self:_Number2Radio(radio, call.modexreceiver, delay, 0.3, pilotcall) - end - + end + -- Add transmission to the right queue. local caller="" if radio.alias=="LSO" then - + table.insert(self.RQLSO, transmission) - + caller="LSOCall" - + elseif radio.alias=="MARSHAL" then - + table.insert(self.RQMarshal, transmission) - + caller="MarshalCall" - + end - + -- Append radio click sound at the end of the transmission. if click then self:RadioTransmission(radio, self[caller].CLICK, false, delay) @@ -14452,7 +14479,7 @@ function AIRBOSS:_NeedsSubtitle(call) -- Currently we play the noise file. if call.file==self.MarshalCall.NOISE.file or call.file==self.LSOCall.NOISE.file then return true - else + else return false end end @@ -14464,31 +14491,31 @@ end -- @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(radio) - + -- Construct file name and subtitle. local filename=self:_RadioFilename(call, loud, radio.alias) - + -- Create subtitle for transmission. local subtitle=self:_RadioSubtitle(radio, call, loud) - - -- Debug. + + -- 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", @@ -14496,7 +14523,7 @@ function AIRBOSS:Broadcast(radio, call, loud) frequency=radio.frequency*1000000, -- Frequency in Hz. modulation=radio.modulation, }} - + -- Command to tranmit the call. local commandTransmit={ id = "TransmitMessage", @@ -14506,70 +14533,70 @@ function AIRBOSS:Broadcast(radio, call, loud) subtitle=subtitle, loop=false, }} - + -- Set commend for frequency sender:SetCommand(commandFrequency) - - -- Set command for radio transmission. + + -- 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 + end --- Player user sound to player if he is inside the CCA. @@ -14586,18 +14613,18 @@ function AIRBOSS:Sound2Player(playerData, radio, call, loud, delay) -- Construct file name. local filename=self:_RadioFilename(call, loud, radio.alias) - + -- 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, false, delay) end - + end end @@ -14613,13 +14640,13 @@ function AIRBOSS:_RadioSubtitle(radio, call, loud) 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 "" @@ -14628,10 +14655,10 @@ function AIRBOSS:_RadioSubtitle(radio, call, loud) 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 @@ -14648,7 +14675,7 @@ function AIRBOSS:_RadioSubtitle(radio, call, loud) end end - return subtitle + return subtitle end --- Get full file name for radio call. @@ -14662,23 +14689,23 @@ function AIRBOSS:_RadioFilename(call, loud, channel) -- Construct file name and subtitle. local prefix=call.file or "" local suffix=call.suffix or "ogg" - + -- Path to sound files. Default is in the ME local path=self.soundfolder or "l10n/DEFAULT/" - - -- Check for special LSO and Marshal sound folders. + + -- Check for special LSO and Marshal sound folders. if string.find(call.file, "LSO-") and channel and (channel=="LSO" or channel=="LSOCall") then path=self.soundfolderLSO or path end if string.find(call.file, "MARSHAL-") and channel and (channel=="MARSHAL" or channel=="MarshalCall") then path=self.soundfolderMSH or path end - + -- 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) @@ -14698,42 +14725,42 @@ end function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay) if playerData and message and message~="" then - + -- Default duration. duration=duration or self.Tmessage - -- Format message. + -- Format message. local text if receiver and receiver=="" then -- No (blank) receiver. - text=string.format("%s", message) + 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:T(self.lid..text) - + if delay and delay>0 then -- Delayed call. SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) else - + -- Wait until previous sound finished. local wait=0 - + -- Onboard number to get the attention. if receiver==playerData.onboard then - + -- Which voice over number to use. if sender and (sender=="LSO" or sender=="MARSHAL" or sender=="AIRBOSS") then - + -- User sound of board number. wait=wait+self:_Number2Sound(playerData, sender, receiver) - - end + + end end - + -- Negative. if string.find(text:lower(), "negative") then local filename=self:_RadioFilename(self.MarshalCall.NEGATIVE, false, "MARSHAL") @@ -14747,27 +14774,27 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration USERSOUND:New(filename):ToGroup(playerData.group, wait) wait=wait+self.MarshalCall.AFFIRMATIVE.duration end - + -- Roger. if string.find(text:lower(), "roger") then local filename=self:_RadioFilename(self.MarshalCall.ROGER, false, "MARSHAL") USERSOUND:New(filename):ToGroup(playerData.group, wait) wait=wait+self.MarshalCall.ROGER.duration end - + -- Play click sound to end message. if wait>0 then local filename=self:_RadioFilename(self.MarshalCall.CLICK) USERSOUND:New(filename):ToGroup(playerData.group, wait) - end - + end + -- Text message to player client. if playerData.client then MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) end - + end - + end end @@ -14785,10 +14812,10 @@ function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, de -- Create new (fake) radio call to show the subtitile. local call=self:_NewRadioCall(self.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, nil, true) - + end --- Send text message to all players in the marshal queue. @@ -14801,12 +14828,12 @@ end -- @param #boolean clear If true, clear screen from previous messages. -- @param #number delay Delay in seconds, before the message is displayed. function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay) - + -- Create new (fake) radio call to show the subtitile. local call=self:_NewRadioCall(self.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, nil, true) + self:RadioTransmission(self.MarshalRadio, call, false, delay, nil, true) end @@ -14822,26 +14849,26 @@ function AIRBOSS:_NewRadioCall(call, sender, subtitle, subduration, modexreceive -- 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 self.Tmessage - + -- Tail number of the receiver. if self:_IsOnboard(modexreceiver) then newcall.modexreceiver=modexreceiver end - + -- Tail number of the sender. if self:_IsOnboard(modexsender) then newcall.modexsender=modexsender end - + return newcall end @@ -14865,19 +14892,19 @@ function AIRBOSS:_GetRadioSender(radio) sender=UNIT:FindByName(self.radiorelayMSH) end end - + -- Try the specific LSO unit. if radio.alias=="LSO" then if self.radiorelayLSO then sender=UNIT:FindByName(self.radiorelayLSO) - end + end end - + -- Check that sender is alive and an aircraft. if sender and sender:IsAlive() and sender:IsAir() then return sender end - + return nil end @@ -14891,7 +14918,7 @@ function AIRBOSS:_IsOnboard(text) if text==nil then return false end - + -- Message to all. if text=="99" then return true @@ -14900,16 +14927,16 @@ function AIRBOSS:_IsOnboard(text) -- 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 @@ -14935,7 +14962,7 @@ function AIRBOSS:_Number2Sound(playerData, sender, number, delay) return chars end - -- Sender + -- Sender local Sender if sender=="LSO" then Sender="LSOCall" @@ -14945,32 +14972,32 @@ function AIRBOSS:_Number2Sound(playerData, sender, number, delay) 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 + 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=self[Sender][N] --#AIRBOSS.RadioCall - + -- Create file name. local filename=self:_RadioFilename(call, false, Sender) - + -- 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 @@ -14994,7 +15021,7 @@ function AIRBOSS:_Number2Radio(radio, number, delay, interval, pilotcall) end return chars end - + -- Sender. local Sender="" if radio.alias=="LSO" then @@ -15004,37 +15031,37 @@ function AIRBOSS:_Number2Radio(radio, number, delay, interval, pilotcall) else self:E(self.lid..string.format("ERROR: Unknown radio alias %s!", tostring(radio.alias))) end - + if pilotcall then Sender="PilotCall" end - + -- Split string into characters. local numbers=_split(number) - local wait=0 + 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=self[Sender][N] --#AIRBOSS.RadioCall - + if interval and i==1 then -- Transmit. self:RadioTransmission(radio, call, false, delay, interval) else self:RadioTransmission(radio, call, false, delay) end - + -- Add up duration of the number. wait=wait+call.duration end - + -- Return the total duration of the call. return wait end @@ -15049,19 +15076,19 @@ function AIRBOSS:_LSOCallAircraftBall(modex, nickname, fuelstate) -- Pilot: "405, Hornet Ball, 3.2" local text=string.format("%s Ball, %.1f.", nickname, fuelstate) - + -- Debug message. self:I(self.lid..text) - + -- Nickname UPPERCASE. local NICKNAME=nickname:upper() - + -- Fuel state. local FS=UTILS.Split(string.format("%.1f", fuelstate), ".") - + -- Create new call to display complete subtitle. local call=self:_NewRadioCall(self.PilotCall[NICKNAME], modex, text, self.Tmessage, nil, modex) - + -- Hornet .. self:RadioTransmission(self.LSORadio, call, nil, nil, nil, nil, true) -- Ball, @@ -15071,21 +15098,21 @@ function AIRBOSS:_LSOCallAircraftBall(modex, nickname, fuelstate) -- Point.. self:RadioTransmission(self.LSORadio, self.PilotCall.POINT, nil, nil, nil, nil, true) -- Y. - self:_Number2Radio(self.LSORadio, FS[2], nil, nil, true) - + self:_Number2Radio(self.LSORadio, FS[2], nil, nil, true) + -- CLICK! self:RadioTransmission(self.LSORadio, self.LSOCall.CLICK) - + end --- AI is bingo and goes to the recovery tanker. -- @param #AIRBOSS self -- @param #string modex Tail number. function AIRBOSS:_MarshalCallGasAtTanker(modex) - + -- Subtitle. local text=string.format("Bingo fuel! Going for gas at the recovery tanker.") - + -- Debug message. self:I(self.lid..text) @@ -15094,10 +15121,10 @@ function AIRBOSS:_MarshalCallGasAtTanker(modex) -- MODEX, bingo fuel! self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, nil, true) - + -- Going for fuel at the recovery tanker. Click! - self:RadioTransmission(self.MarshalRadio, self.PilotCall.GASATTANKER, nil, nil, nil, true, true) - + self:RadioTransmission(self.MarshalRadio, self.PilotCall.GASATTANKER, nil, nil, nil, true, true) + end --- AI is bingo and goes to the divert field. @@ -15105,10 +15132,10 @@ end -- @param #string modex Tail number. -- @param #string divertname Name of the divert field. function AIRBOSS:_MarshalCallGasAtDivert(modex, divertname) - + -- Subtitle. local text=string.format("Bingo fuel! Going for gas at divert field %s.", divertname) - + -- Debug message. self:I(self.lid..text) @@ -15117,10 +15144,10 @@ function AIRBOSS:_MarshalCallGasAtDivert(modex, divertname) -- MODEX, bingo fuel! self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, nil, true) - + -- Going for fuel at the divert field. Click! - self:RadioTransmission(self.MarshalRadio, self.PilotCall.GASATDIVERT, nil, nil, nil, true, true) - + self:RadioTransmission(self.MarshalRadio, self.PilotCall.GASATDIVERT, nil, nil, nil, true, true) + end @@ -15131,7 +15158,7 @@ function AIRBOSS:_MarshalCallRecoveryStopped(case) -- Subtitle. local text=string.format("Case %d recovery ops are stopped. Deck is closed.", case) - + -- Debug message. self:I(self.lid..text) @@ -15172,7 +15199,7 @@ function AIRBOSS:_MarshalCallRecoveryPausedResumedAt(clock) -- Subtitle. local text=string.format("aircraft recovery is paused and will be resumed at %s.", clock) - + -- Debug message. self:I(self.lid..text) @@ -15181,14 +15208,14 @@ function AIRBOSS:_MarshalCallRecoveryPausedResumedAt(clock) -- 99, aircraft recovery is paused and will resume at... self:RadioTransmission(self.MarshalRadio, call) - + -- XY.. (hours) self:_Number2Radio(self.MarshalRadio, CT[1]) -- XY (minutes).. self:_Number2Radio(self.MarshalRadio, CT[2]) -- hours. Click! self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOURS, nil, nil, nil, true) - + end @@ -15200,13 +15227,13 @@ function AIRBOSS:_MarshalCallClearedForRecovery(modex, case) -- Subtitle. local text=string.format("you're cleared for Case %d recovery.", case) - + -- Debug message. self:I(self.lid..text) - + -- Create new call with full subtitle. local call=self:_NewRadioCall(self.MarshalCall.CLEAREDFORRECOVERY, "MARSHAL", text, self.Tmessage, modex) - + -- Two second delay. local delay=2 @@ -15216,7 +15243,7 @@ function AIRBOSS:_MarshalCallClearedForRecovery(modex, case) self:_Number2Radio(self.MarshalRadio, tostring(case), delay) -- recovery. Click! self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RECOVERY, nil, delay, nil, true) - + end --- Inform everyone that recovery is resumed after pause. @@ -15238,20 +15265,20 @@ function AIRBOSS:_MarshalCallNewFinalBearing(FB) -- Subtitle. local text=string.format("new final bearing %03d°.", FB) - + -- Debug message. self:I(self.lid..text) -- Create new call with full subtitle. local call=self:_NewRadioCall(self.MarshalCall.NEWFB, "AIRBOSS", text, self.Tmessage, "99") - + -- 99, new final bearing.. - self:RadioTransmission(self.MarshalRadio, call) + self:RadioTransmission(self.MarshalRadio, call) -- XYZ.. self:_Number2Radio(self.MarshalRadio, string.format("%03d", FB), nil, 0.2) -- Degrees. Click! self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) - + end --- Compile a radio call when Marshal tells a flight the holding alitude. @@ -15261,20 +15288,20 @@ function AIRBOSS:_MarshalCallCarrierTurnTo(hdg) -- Subtitle. local text=string.format("carrier is now starting turn to heading %03d°.", hdg) - + -- Debug message. self:I(self.lid..text) -- Create new call with full subtitle. local call=self:_NewRadioCall(self.MarshalCall.CARRIERTURNTOHEADING, "AIRBOSS", text, self.Tmessage, "99") - + -- 99, turning to heading... self:RadioTransmission(self.MarshalRadio, call) -- XYZ.. self:_Number2Radio(self.MarshalRadio, string.format("%03d", hdg), nil, 0.2) -- Degrees. Click! - self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) - + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + end --- Compile a radio call when Marshal tells a flight the holding alitude. @@ -15292,13 +15319,13 @@ function AIRBOSS:_MarshalCallStackFull(modex, nwaiting) else text=text..string.format("You are next in line.") end - + -- Debug message. self:I(self.lid..text) - + -- Create new call with full subtitle. local call=self:_NewRadioCall(self.MarshalCall.STACKFULL, "AIRBOSS", text, self.Tmessage, modex) - + -- XYZ, Marshal stack is currently full. self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) end @@ -15312,23 +15339,23 @@ function AIRBOSS:_MarshalCallRecoveryStart(case) -- Debug output. local text=string.format("Starting aircraft recovery Case %d ops.", case) - if case>1 then + if case>1 then text=text..string.format(" Marshal radial %03d°.", radial) end self:T(self.lid..text) - + -- New call including the subtitle. - local call=self:_NewRadioCall(self.MarshalCall.STARTINGRECOVERY, "AIRBOSS", text, self.Tmessage, "99") + local call=self:_NewRadioCall(self.MarshalCall.STARTINGRECOVERY, "AIRBOSS", text, self.Tmessage, "99") -- 99, Starting aircraft recovery case.. self:RadioTransmission(self.MarshalRadio, call) -- X.. - self:_Number2Radio(self.MarshalRadio,tostring(case), nil, 0.1) + self:_Number2Radio(self.MarshalRadio,tostring(case), nil, 0.1) -- ops. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.OPS) - + --Marshal Radial - if case>1 then + if case>1 then -- Marshal radial.. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.MARSHALRADIAL) -- XYZ.. @@ -15349,7 +15376,7 @@ end -- @param #number qfe Alitmeter inHg. function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) self:F({modex=modex,case=case,brc=brc,altitude=altitude,charlie=charlie,qfe=qfe}) - + -- Split strings etc. local angels=self:_GetAngels(altitude) --local QFE=UTILS.Split(tostring(UTILS.Round(qfe,2)), ".") @@ -15359,7 +15386,7 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) -- Subtitle text. local text=string.format("Case %d, expected BRC %03d°, hold at angels %d. Expected Charlie Time %s. Altimeter %.2f. Report see me.", case, brc, angels, charlie, qfe) - + -- Debug message. self:I(self.lid..text) @@ -15370,7 +15397,7 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) self:RadioTransmission(self.MarshalRadio, casecall) -- X. self:_Number2Radio(self.MarshalRadio, tostring(case)) - + -- Expected.. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5) -- BRC.. @@ -15379,13 +15406,13 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) self:_Number2Radio(self.MarshalRadio, string.format("%03d", brc)) -- Degrees. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES) - - + + -- Hold at.. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOLDATANGELS, nil, nil, 0.5) -- X. self:_Number2Radio(self.MarshalRadio, tostring(angels)) - + -- Expected.. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5) -- Charlie time.. @@ -15396,8 +15423,8 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) self:_Number2Radio(self.MarshalRadio, CT[2]) -- hours. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOURS) - - + + -- Altimeter.. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ALTIMETER, nil, nil, 0.5) -- XY.. @@ -15406,10 +15433,10 @@ function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) self:RadioTransmission(self.MarshalRadio, self.MarshalCall.POINT) -- XY. self:_Number2Radio(self.MarshalRadio, QFE[2]) - + -- Report see me. Click! self:RadioTransmission(self.MarshalRadio, self.MarshalCall.REPORTSEEME, nil, nil, 0.5, true) - + end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -15421,31 +15448,31 @@ end -- @param #string _unitName Name of player unit. function AIRBOSS:_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 - + -- Set menu root path. local _rootPath=nil if AIRBOSS.MenuF10Root then ------------------------ -- MISSON LEVEL MENUE -- - ------------------------ - + ------------------------ + if self.menusingle then -- F10/Airboss/... _rootPath=AIRBOSS.MenuF10Root @@ -15453,18 +15480,18 @@ function AIRBOSS:_AddF10Commands(_unitName) -- 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] @@ -15472,11 +15499,11 @@ function AIRBOSS:_AddF10Commands(_unitName) -- F10/Airboss//... _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) end - + end - - - -------------------------------- + + + -------------------------------- -- F10/Airboss//F1 Help -------------------------------- local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) @@ -15488,7 +15515,7 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 end missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 - if self.menusmokezones then + 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 @@ -15511,14 +15538,14 @@ function AIRBOSS:_AddF10Commands(_unitName) ------------------------------------- -- F10/Airboss//F2 Kneeboard ------------------------------------- - local _kneeboardPath=missionCommands.addSubMenuForGroup(gid, "Kneeboard", _rootPath) + local _kneeboardPath=missionCommands.addSubMenuForGroup(gid, "Kneeboard", _rootPath) -- F10/Airboss//F2 Kneeboard/F1 Results local _resultsPath=missionCommands.addSubMenuForGroup(gid, "Results", _kneeboardPath) -- 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//F2 Kneeboard/F2 Skipper/ if self.skipperMenu then local _skipperPath =missionCommands.addSubMenuForGroup(gid, "Skipper", _kneeboardPath) @@ -15545,8 +15572,8 @@ function AIRBOSS:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(gid, "Start CASE II", _skipperPath, self._SkipperStartRecovery, self, _unitName, 2) missionCommands.addCommandForGroup(gid, "Start CASE III",_skipperPath, self._SkipperStartRecovery, self, _unitName, 3) missionCommands.addCommandForGroup(gid, "Stop Recovery", _skipperPath, self._SkipperStopRecovery, self, _unitName) - end - + end + -- F10/Airboss/1 then @@ -15610,10 +15637,10 @@ function AIRBOSS:_SkipperStartRecovery(_unitName, case) local t9=t0+self.skipperTime*60 local C0=UTILS.SecondsToClock(t0) local C9=UTILS.SecondsToClock(t9) - + -- Carrier will turn into the wind. Wind on deck 25 knots. U-turn on. self:AddRecoveryWindow(C0, C9, case, self.skipperOffset, true, self.skipperSpeed, self.skipperUturn) - + end end end @@ -15625,22 +15652,22 @@ function AIRBOSS:_SkipperStopRecovery(_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 - + -- Inform player. local text="roger, stopping recovery right away." if not self:IsRecovering() then text="negative, carrier is currently not recovering." self:MessageToPlayer(playerData, text, "AIRBOSS") - return + return end self:MessageToPlayer(playerData, text, "AIRBOSS") - + self:RecoveryStop() end end @@ -15654,21 +15681,21 @@ function AIRBOSS:_SkipperRecoveryOffset(_unitName, offset) -- 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 - + -- Inform player. local text=string.format("roger, relative CASE II/III Marshal radial set to %d°.", offset) self:MessageToPlayer(playerData, text, "AIRBOSS") - + self.skipperOffset=offset end end -end +end --- Skipper set recovery time. -- @param #AIRBOSS self @@ -15678,19 +15705,19 @@ function AIRBOSS:_SkipperRecoveryTime(_unitName, time) -- 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 - + -- Inform player. local text=string.format("roger, manual recovery time set to %d min.", time) self:MessageToPlayer(playerData, text, "AIRBOSS") - + self.skipperTime=time - + end end end @@ -15703,21 +15730,21 @@ function AIRBOSS:_SkipperRecoverySpeed(_unitName, speed) -- 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 - + -- Inform player. local text=string.format("roger, wind on deck set to %d knots.", speed) self:MessageToPlayer(playerData, text, "AIRBOSS") - + self.skipperSpeed=speed end end -end +end --- Skipper set recovery speed. -- @param #AIRBOSS self @@ -15726,22 +15753,22 @@ function AIRBOSS:_SkipperRecoveryUturn(_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.skipperUturn=not self.skipperUturn - + -- Inform player. local text=string.format("roger, U-turn is now %s.", tostring(self.skipperUturn)) self:MessageToPlayer(playerData, text, "AIRBOSS") - + end end -end +end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -15753,27 +15780,27 @@ end -- @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 - + -- Inform player. local text="roger, status reset executed! You have been removed from all queues." self:MessageToPlayer(playerData, text, "AIRBOSS") - + -- Remove flight from queues. Collapse marshal stack if necessary. -- Section members are removed from the Spinning queue. If flight is member, he is removed from the section. self:_RemoveFlight(playerData) - + -- Initialize player data. self:_InitPlayer(playerData) - + end end end @@ -15783,77 +15810,77 @@ end -- @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 - + -- Check if player is in CCA local inCCA=playerData.unit:IsInZone(self.zoneCCA) - + if inCCA then - + if self:_InQueue(self.Qmarshal, playerData.group) then - + -- Flight group is already in marhal queue. local text=string.format("negative, you are already in the Marshal queue. New marshal request denied!") - self:MessageToPlayer(playerData, text, "MARSHAL") - + self:MessageToPlayer(playerData, text, "MARSHAL") + elseif self:_InQueue(self.Qpattern, playerData.group) then - + -- Flight group is already in pattern queue. local text=string.format("negative, 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("negative, 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 + + elseif not _unit:InAir() then -- Flight group is already in pattern queue. local text=string.format("negative, 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") - + self:MessageToPlayer(playerData, text, "MARSHAL") + else - + -- 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("negative, you are not inside CCA. Marshal request denied!") self:MessageToPlayer(playerData, text, "MARSHAL") - + end end end @@ -15864,48 +15891,48 @@ end -- @param #string _unitName Name fo the player unit. function AIRBOSS:_RequestEmergency(_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="" if not self.emergency then - + -- Mission designer did not allow emergency landing. text="negative, no emergency landings on my carrier. We are currently busy. See how you get along!" elseif not _unit:InAir() then - + -- Carrier zone. local zone=self:_GetZoneCarrierBox() - + -- Check if player is on the carrier. if playerData.unit:IsInZone(zone) then - + -- Bolter pattern. text="roger, you are now technically in the bolter pattern. Your next step after takeoff is abeam!" - + -- Get flight lead. local lead=self:_GetFlightLead(playerData) - + -- Set set for lead. self:_SetPlayerStep(lead, AIRBOSS.PatternStep.BOLTER) - + -- Also set bolter pattern for all members. for _,sec in pairs(lead.section) do local sectionmember=sec --#AIRBOSS.PlayerData - self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.BOLTER) + self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.BOLTER) end - + -- Remove flight from waiting queue just in case. self:_RemoveFlightFromQueue(self.Qwaiting, lead) - + if self:_InQueue(self.Qmarshal, lead.group) then -- Remove flight from Marshal queue and add to pattern. self:_RemoveFlightFromMarshalQueue(lead) @@ -15919,31 +15946,31 @@ function AIRBOSS:_RequestEmergency(_unitName) else -- Flight group is not in air. text=string.format("negative, you are not airborne. Request denied!") - end - + end + else - + -- Cleared. text="affirmative, you can bypass the pattern and are cleared for final approach!" - + -- Now, if player is in the marshal or waiting queue he will be removed. But the new leader should stay in or not. local lead=self:_GetFlightLead(playerData) - + -- Set set for lead. self:_SetPlayerStep(lead, AIRBOSS.PatternStep.EMERGENCY) - + -- Also set emergency landing for all members. for _,sec in pairs(lead.section) do local sectionmember=sec --#AIRBOSS.PlayerData self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.EMERGENCY) - + -- Remove flight from spinning queue just in case (everone can spin on his own). self:_RemoveFlightFromQueue(self.Qspinning, sectionmember) end - + -- Remove flight from waiting queue just in case. self:_RemoveFlightFromQueue(self.Qwaiting, lead) - + if self:_InQueue(self.Qmarshal, lead.group) then -- Remove flight from Marshal queue and add to pattern. self:_RemoveFlightFromMarshalQueue(lead) @@ -15953,14 +15980,14 @@ function AIRBOSS:_RequestEmergency(_unitName) self:_AddFlightToPatternQueue(lead) end end - + end - + -- Send message. self:MessageToPlayer(playerData, text, "AIRBOSS") - + end - + end end @@ -15969,59 +15996,59 @@ end -- @param #string _unitName Name fo the player unit. function AIRBOSS:_RequestSpinning(_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="" if not self:_InQueue(self.Qpattern, playerData.group) then - + -- Player not in pattern queue. text="negative, you have to be in the pattern to spin it!" - + elseif playerData.step==AIRBOSS.PatternStep.SPINNING then - + -- Player is already spinning. text="negative, you are already spinning." - + -- Check if player is in the right step. - elseif not (playerData.step==AIRBOSS.PatternStep.BREAKENTRY or + elseif not (playerData.step==AIRBOSS.PatternStep.BREAKENTRY or playerData.step==AIRBOSS.PatternStep.EARLYBREAK or playerData.step==AIRBOSS.PatternStep.LATEBREAK) then - + -- Player is not in the right step. text="negative, you have to be in the right step to spin it!" - + else - + -- Set player step. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.SPINNING) - + -- Add player to spinning queue. table.insert(self.Qspinning, playerData) - + -- 405, Spin it! Click. - local call=self:_NewRadioCall(self.LSOCall.SPINIT, "AIRBOSS", "Spin it!", self.Tmessage, playerData.onboard) + local call=self:_NewRadioCall(self.LSOCall.SPINIT, "AIRBOSS", "Spin it!", self.Tmessage, playerData.onboard) self:RadioTransmission(self.LSORadio, call, nil, nil, nil, true) - + -- Some advice. if playerData.difficulty==AIRBOSS.Difficulty.EASY then local text="Climb to 1200 feet and proceed to the initial again." self:MessageToPlayer(playerData, text, "INSTRUCTOR", "") end - + return end - + -- Send message. self:MessageToPlayer(playerData, text, "AIRBOSS") - + end end end @@ -16031,54 +16058,54 @@ end -- @param #string _unitName Name fo the player unit. function AIRBOSS:_RequestCommence(_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 - + -- Check if unit is in CCA. local text="" local cleared=false if _unit:IsInZone(self.zoneCCA) then - + -- Get stack value. local stack=playerData.flag - + -- Number of airborne aircraft currently in pattern. - local _,npattern=self:_GetQueueInfo(self.Qpattern) - + 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("negative, %s, you are already in the Pattern queue.", playerData.name) - - elseif not _unit:InAir() then + + elseif not _unit:InAir() then -- Flight group is already in pattern queue. 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) - + 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 - + -- 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. @@ -16088,27 +16115,27 @@ function AIRBOSS:_RequestCommence(_unitName) else 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="negative, you have to request Marshal before you can commence." - + else - + ----------------------- -- Positive Response -- ----------------------- - + text=text.."roger." -- Carrier is not recovering but Airboss has a good day. if not self:IsRecovering() then text=text.." Carrier is not recovering currently! However, you are cleared anyway as I have a nice day." 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 @@ -16122,33 +16149,33 @@ function AIRBOSS:_RequestCommence(_unitName) end text=text..string.format("\nSelect 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. self:_AddFlightToPatternQueue(playerData) - end - + end + -- Clear player for commence. - cleared=true + cleared=true end - + else -- This flight is not yet registered! text=string.format("negative, %s, you are not inside the CCA!", playerData.name) end - + -- Debug self:T(self.lid..text) - + -- 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. @@ -16158,75 +16185,75 @@ function AIRBOSS:_RequestCommence(_unitName) end end ---- Player requests refueling. +--- 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 - + -- Check if there is a recovery tanker defined. local text if self.tanker then - + -- 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) local angels=self:_GetAngels(self.tanker.altitude) - + -- Tanker is up and running. text=string.format("affirmative, 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("\nRadio frequency %.3f MHz AM.", self.tanker.RadioFreq) end - + -- Tanker is currently refueling. Inform player. if self.tanker:IsRefueling() then text=text.."\nTanker is currently refueling. You might have to queue up." end - + -- Collapse marshal stack if player is in queue. self:_RemoveFlightFromMarshalQueue(playerData, true) -- Set step to refueling. self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.REFUELING) - + -- Inform section and set step. for _,sec in pairs(playerData.section) do local sectext="follow your 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. text="negative, tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." end - + else text="negative, you are not inside the CCA yet." end else text="negative, no refueling tanker available." end - + -- Send message. - self:MessageToPlayer(playerData, text, "MARSHAL") + self:MessageToPlayer(playerData, text, "MARSHAL") end end end @@ -16234,7 +16261,7 @@ end --- Remove a member from the player's section. -- @param #AIRBOSS self --- @param #AIRBOSS.PlayerData playerData Player +-- @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) @@ -16256,77 +16283,77 @@ 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() - + -- 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.NmaxSection==0 then - text=string.format("negative, setting sections is disabled in this mission. You stay alone.") + text=string.format("negative, setting sections is disabled in this mission. You stay alone.") elseif self:_InQueue(self.Qmarshal,playerData.group) then text=string.format("negative, you are already in the Marshal queue. Setting section not possible any more!") elseif self:_InQueue(self.Qpattern, playerData.group) then text=string.format("negative, you are already in the Pattern queue. Setting section not possible any more!") else - + -- 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 - + -- 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 - + -- 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) @@ -16342,7 +16369,7 @@ function AIRBOSS:_SetSection(_unitName) 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 @@ -16356,7 +16383,7 @@ function AIRBOSS:_SetSection(_unitName) -- 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) @@ -16368,32 +16395,32 @@ function AIRBOSS:_SetSection(_unitName) -- 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 @@ -16407,27 +16434,27 @@ end -- @param #string _unitName Name fo the player unit. function AIRBOSS:_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={} - + -- Calculate average points for all players. for playerName,playerGrades in pairs(self.playerscores) do - + if playerGrades then - + -- Loop over all grades local Paverage=0 local n=0 for _,_grade in pairs(playerGrades) do local grade=_grade --#AIRBOSS.LSOgrade - + -- Add up only final scores for the average. if grade.finalscore then --grade.points>=0 then Paverage=Paverage+grade.finalscore @@ -16435,28 +16462,28 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) else -- 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 end - + -- We dont want to devide by zero. if n>0 then _playerResults[playerName]=Paverage/n end - + end end - + -- Message text. local text = string.format("Greenie Board (top ten):") local i=1 for _playerName,_points in UTILS.spairs(_playerResults, function(t, a, b) return t[b] < t[a] end) do - + -- Text. text=text..string.format("\n[%d] %s %.1f||", i,_playerName, _points) - + -- All player grades. local playerGrades=self.playerscores[_playerName] - + -- 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 @@ -16466,14 +16493,14 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) 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 no results yet. if i==1 then text=text.."\nNo results yet." @@ -16484,7 +16511,7 @@ function AIRBOSS:_DisplayScoreBoard(_unitName) if playerData.client then MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) end - + end end @@ -16493,22 +16520,22 @@ end -- @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 last 10 grades, %s:", _playername) - + -- All player grades. local playerGrades=self.playerscores[_playername] or {} - + local p=0 -- Average points. local n=0 -- Number of final passes. local m=0 -- Number of total passes. @@ -16516,46 +16543,46 @@ function AIRBOSS:_DisplayPlayerGrades(_unitName) 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. + + -- Check if points >=0. For foul deck WO we give -1 and pass is not counted. if grade.points>=0 then - + -- Show final points or points of pass. local points=grade.finalscore or grade.points - + -- 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<=360 then text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) end end - + -- Add up final points. if grade.finalscore then p=p+grade.finalscore n=n+1 end - + -- Total passes m=m+1 end end - + if n>0 then text=text..string.format("\nAverage points = %.1f", p/n) else text=text..string.format("\nNo data available.") end - + -- Send message. if playerData.client then MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) @@ -16569,20 +16596,20 @@ end -- @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. + + -- Check if data is present. if #playerData.lastdebrief>0 then text=text..string.format("\n================================\n") for _,_data in pairs(playerData.lastdebrief) do @@ -16594,10 +16621,10 @@ function AIRBOSS:_DisplayDebriefing(_unitName) else text=text.." Nothing to show yet." end - + -- Send debrief message to player self:MessageToPlayer(playerData, text, nil , "", 30, true) - + end end end @@ -16614,28 +16641,28 @@ function AIRBOSS:_DisplayQueue(_unitname, qname) -- 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. + + -- Player data. local playerData=self.players[playername] --#AIRBOSS.PlayerData - + if playerData then - + -- Queue to display. local queue=nil if qname=="Marshal" then queue=self.Qmarshal elseif qname=="Pattern" then queue=self.Qpattern - elseif qname=="Waiting" then + elseif qname=="Waiting" then queue=self.Qwaiting end - + -- 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" @@ -16665,7 +16692,7 @@ function AIRBOSS:_DisplayQueue(_unitname, qname) end text=text..string.format("\nTotal AC: %d (airborne %d)", N, nqueue) end - + -- Send message. self:MessageToPlayer(playerData, text, nil, "", nil, true) end @@ -16678,25 +16705,25 @@ end -- @param #string _unitname Name of the player unit. function AIRBOSS:_DisplayCarrierInfo(_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. + + -- Player data. local playerData=self.players[playername] --#AIRBOSS.PlayerData - + if playerData then - + -- Current coordinates. - local coord=self:GetCoordinate() - + local coord=self: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" @@ -16706,20 +16733,20 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) if self.ICLSon and self.ICLSchannel~=nil then icls=string.format("%d (%s)", self.ICLSchannel, self.ICLSmorse) end - + -- Wind on flight deck local wind=UTILS.MpsToKnots(select(1, self:GetWindOnDeck())) - + -- Get groups, units in queues. local Nmarshal,nmarshal = self:_GetQueueInfo(self.Qmarshal, playerData.case) local Npattern,npattern = self:_GetQueueInfo(self.Qpattern) local Nspinning,nspinning = self:_GetQueueInfo(self.Qspinning) local Nwaiting,nwaiting = self:_GetQueueInfo(self.Qwaiting) local Ntotal,ntotal = self:_GetQueueInfo(self.flights) - + -- Current abs time. local Tabs=timer.getAbsTime() - + -- Get recovery times of carrier. local recoverytext="Recovery time windows (max 5):" if #self.recoverytimes==0 then @@ -16744,7 +16771,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) end end end - + -- Recovery tanker TACAN text. local tankertext=nil if self.tanker then @@ -16753,9 +16780,9 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) tankertext=tankertext..string.format("Recovery tanker TACAN %d%s (%s)",self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) else tankertext=tankertext.."Recovery tanker TACAN n/a" - end + end end - + -- Carrier FSM state. Idle is not clear enough. local state=self:GetState() if state=="Idle" then @@ -16764,15 +16791,15 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) if self.turning then state=state.." (turning currently)" 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", state) if self.case==1 then text=text..string.format("Case %d recovery ops\n", self.case) else - local radial=self:GetRadial(self.case, true, true, true) + 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° - FB %03d°\n", self:GetBRC(), self:GetFinalBearing(true)) @@ -16791,15 +16818,15 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) text=text..string.format("# A/C waiting %d (%d)\n", Nwaiting, nwaiting) text=text..string.format(recoverytext) self:T2(self.lid..text) - + -- Send message. self:MessageToPlayer(playerData, text, nil, "", 30, true) - + else self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) - end - end - + end + end + end @@ -16811,38 +16838,38 @@ function AIRBOSS:_DisplayCarrierWeather(_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:GetCoordinate() - + -- Get atmospheric data at carrier location. local T=coord:GetTemperature() local P=coord:GetPressure() - + -- Get wind direction (magnetic) and strength. local Wd,Ws=self:GetWind(nil, true) - + -- Get Beaufort wind scale. local Bn,Bd=UTILS.BeaufortScale(Ws) - + -- Wind on flight deck. local WodPA,WodPP=self:GetWindOnDeck() local WodPA=UTILS.MpsToKnots(WodPA) local WodPP=UTILS.MpsToKnots(WodPP) - + local WD=string.format('%03d°', Wd) local Ts=string.format("%d°C",T) - + 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)) - + 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") @@ -16850,7 +16877,7 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) text=text..string.format("Wind from %s at %s (%s)\n", WD, tW, Bd) text=text..string.format("Wind on deck || %.1f kts, == %.1f kts\n", WodPA, WodPP) text=text..string.format("QFE %.1f hPa = %s", P, tP) - + -- More info only reliable if Mission uses static weather. if self.staticweather then local clouds, visibility, fog, dust=self:_GetStaticWeather() @@ -16858,7 +16885,7 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) 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) + 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)) @@ -16871,16 +16898,16 @@ function AIRBOSS:_DisplayCarrierWeather(_unitname) text=text..string.format("\nNo dust") end end - + -- Debug output. self:T2(self.lid..text) - + -- Send message to player group. 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)) - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -16893,16 +16920,16 @@ end -- @param #AIRBOSS.Difficulty difficulty Difficulty level. function AIRBOSS:_SetDifficulty(_unitname, difficulty) self:T2({difficulty=difficulty, unitname=_unitname}) - + -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) - + -- Check if we have a player. - if unit and playername then - + if unit and playername then + -- Player data. local playerData=self.players[playername] --#AIRBOSS.PlayerData - + if playerData then playerData.difficulty=difficulty local text=string.format("roger, your skill level is now: %s.", difficulty) @@ -16910,15 +16937,15 @@ function AIRBOSS:_SetDifficulty(_unitname, difficulty) else self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) end - + -- Set hints as well. if playerData.difficulty==AIRBOSS.Difficulty.HARD then playerData.showhints=false else playerData.showhints=true end - - end + + end end --- Turn player's aircraft attitude display on or off. @@ -16926,21 +16953,21 @@ end -- @param #string _unitname Name of the player unit. function AIRBOSS:_SetHintsOnOff(_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. + + -- Player data. local playerData=self.players[playername] --#AIRBOSS.PlayerData - + if playerData then - + -- Invert hints. playerData.showhints=not playerData.showhints - + -- Inform player. local text="" if playerData.showhints==true then @@ -16949,7 +16976,7 @@ function AIRBOSS:_SetHintsOnOff(_unitname) text=string.format("affirm, hints are now OFF.") end self:MessageToPlayer(playerData, text, nil, playerData.name, 5) - + end end end @@ -16959,21 +16986,21 @@ end -- @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. + + -- Player data. local playerData=self.players[playername] --#AIRBOSS.PlayerData - + if playerData then playerData.attitudemonitor=not playerData.attitudemonitor end end - + end --- Turn radio subtitles of player on or off. @@ -16981,16 +17008,16 @@ end -- @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. + + -- Player data. local playerData=self.players[playername] --#AIRBOSS.PlayerData - + if playerData then playerData.subtitles=not playerData.subtitles -- Inform player. @@ -17003,7 +17030,7 @@ function AIRBOSS:_SubtitlesOnOff(_unitname) self:MessageToPlayer(playerData, text, nil, playerData.name, 5) end end - + end --- Turn radio subtitles of player on or off. @@ -17011,41 +17038,41 @@ end -- @param #string _unitname Name of the player unit. function AIRBOSS:_TrapsheetOnOff(_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. + + -- Player data. local playerData=self.players[playername] --#AIRBOSS.PlayerData - + if playerData then - + -- Check if option is enabled at all. local text="" if self.trapsheet then - + -- Invert current setting. playerData.trapon=not playerData.trapon - + -- Inform player. if playerData.trapon==true then text=string.format("roger, your trapsheets are now SAVED.") else text=string.format("affirm, your trapsheets are NOT SAVED.") end - + else text="negative, trap sheet data recorder is broken on this carrier." end - + -- Message to player. self:MessageToPlayer(playerData, text, nil, playerData.name, 5) end end - + end @@ -17056,13 +17083,13 @@ 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 - + -- Pattern step text. local steptext=playerData.step if playerData.step==AIRBOSS.PatternStep.HOLDING then @@ -17077,7 +17104,7 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) -- Stack. local stack=playerData.flag - + -- Stack text. local stacktext=nil if stack>0 then @@ -17093,18 +17120,18 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) 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) - + -- Number of units in group. 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) - text=text..string.format("================================\n") + text=text..string.format("================================\n") text=text..string.format("Step: %s\n", steptext) if stacktext then text=text..stacktext @@ -17119,46 +17146,46 @@ function AIRBOSS:_DisplayPlayerStatus(_unitName) local sec=_sec --#AIRBOSS.PlayerData text=text..string.format("\n- %s", sec.name) end - + 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(zoneinitial) local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneinitial)) local brc=self:GetBRC() - -- Help player to find its way to the initial zone. + -- Help player to find its way to the initial zone. 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 - + -- Coordinate of the platform zone. local zoneplatform=self:_GetZonePlatform(playerData.case):GetCoordinate() -- Heading and distance to platform zone. 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. + -- Help player to find its way to the initial zone. text=text..string.format("\nTo Platform: Fly heading %03d° for %.1f NM and turn to %03d°", flyhdg, flydist, hdg) - + end - + -- Send message. self:MessageToPlayer(playerData, text, nil, "", 30, true) else self:E(self.lid..string.format("ERROR: playerData=nil. Unit name=%s, player name=%s", _unitName, _playername)) - end + end else self:E(self.lid..string.format("ERROR: could not find player for unit %s", _unitName)) end - + end --- Mark current marshal zone of player by either smoke or flares. @@ -17169,65 +17196,65 @@ function AIRBOSS:_MarkMarshalZone(_unitName, flare) -- 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 player stack and recovery case. local stack=playerData.flag local case=playerData.case - + local text="" if stack>0 then - + -- Get current holding zone. 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) - + -- Flare and smoke at the ground. patternalt=5 - + -- Roger! text="roger, marking" if flare then - - -- Marshal WHITE flares. + + -- Marshal WHITE flares. text=text..string.format("\n* Marshal zone stack %d with WHITE flares.", stack) zoneHolding:FlareZone(FLARECOLOR.White, 45, nil, patternalt) - + -- Commence RED flares. text=text.."\n* Commence zone with RED flares." zoneThree:FlareZone(FLARECOLOR.Red, 45, nil, patternalt) - + else - + -- Marshal WHITE smoke. text=text..string.format("\n* Marshal zone stack %d with WHITE smoke.", stack) zoneHolding:SmokeZone(SMOKECOLOR.White, 45, patternalt) - + -- Commence RED smoke text=text.."\n* Commence zone with RED smoke." zoneThree:SmokeZone(SMOKECOLOR.Red, 45, patternalt) - + end - + else text="negative, you are currently not in a Marshal stack. No zones will be marked!" end - + -- Send message to player. self:MessageToPlayer(playerData, text, "MARSHAL", playerData.name) end end - + end @@ -17239,22 +17266,22 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) -- 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's recovery case. local case=playerData.case - - -- Initial + + -- Initial local text=string.format("affirm, marking CASE %d zones", case) - + -- Flare or smoke? if flare then - + ----------- -- Flare -- ----------- @@ -17264,25 +17291,25 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) text=text.."\n* initial with GREEN flares" self:_GetZoneInitial(case):FlareZone(FLARECOLOR.Green, 45) end - + -- Case II/III: approach corridor if case==2 or case==3 then text=text.."\n* approach corridor with GREEN flares" self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) end - + -- Case II/III: platform if case==2 or case==3 then text=text.."\n* platform with RED flares" self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) end - + -- Case III: dirty up if case==3 then text=text.."\n* dirty up with YELLOW flares" self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow, 45) end - + -- Case II/III: arc in/out if case==2 or case==3 then if math.abs(self.holdingoffset)>0 then @@ -17292,7 +17319,7 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) text=text.."\n* arc trun out with WHITE flares" end end - + -- Case III: bullseye if case==3 then text=text.."\n* bullseye with GREEN flares" @@ -17310,19 +17337,19 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) local LSPT=self:_GetZoneLandingSpot() LSPT:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) end - + else ----------- -- Smoke -- ----------- - -- Case I/II: Initial + -- Case I/II: Initial if case==1 or case==2 then text=text.."\n* initial with GREEN smoke" self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Green, 45) end - + -- Case II/III: Approach Corridor if case==2 or case==3 then text=text.."\n* approach corridor with GREEN smoke" @@ -17352,18 +17379,18 @@ function AIRBOSS:_MarkCaseZones(_unitName, flare) end -- Case III: bullseye - if case==3 then + if case==3 then text=text.."\n* bullseye with GREEN smoke" self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Green, 45) end - + end - + -- Send message to player. self:MessageToPlayer(playerData, text, "MARSHAL", playerData.name) end end - + end --- LSO radio check. Will broadcase LSO message at given LSO frequency. @@ -17371,14 +17398,14 @@ end -- @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 + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then -- Broadcase LSO radio check message on LSO radio. self:RadioTransmission(self.LSORadio, self.LSOCall.RADIOCHECK, nil, nil, nil, true) end @@ -17390,14 +17417,14 @@ end -- @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 + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then -- Broadcase Marshal radio check message on Marshal radio. self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RADIOCHECK, nil, nil, nil, true) end @@ -17430,7 +17457,7 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) self:E(self.lid..string.format("ERROR: could not save trap sheet to file %s.\nFile may contain invalid characters.", tostring(filename))) end end - + -- Set path or default. local path=self.trappath if lfs then @@ -17441,7 +17468,7 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) -- Create unused file name. local filename=nil for i=1,9999 do - + -- Create file name if self.trapprefix then filename=string.format("%s_%s-%04d.csv", self.trapprefix, playerData.actype, i) @@ -17453,14 +17480,14 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) if path~=nil then filename=path.."\\"..filename end - + -- Check if file exists. local _exists=UTILS.FileExists(filename) if not _exists then break - end + end end - + -- Info local text=string.format("Saving player %s trapsheet to file %s", playerData.name, filename) @@ -17468,10 +17495,10 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) -- Header line local data="#Time,Rho,X,Z,Alt,AoA,GSE,LUE,Vtot,Vy,Gamma,Pitch,Roll,Yaw,Step,Grade,Points,Details\n" - + local g0=playerData.trapsheet[1] --#AIRBOSS.GrooveData local T0=g0.Time - + --for _,_groove in ipairs(playerData.trapsheet) do for i=1,#playerData.trapsheet do --local groove=_groove --#AIRBOSS.GrooveData @@ -17497,7 +17524,7 @@ function AIRBOSS:_SaveTrapSheet(playerData, grade) -- t a b c d e f g h i j k l m n o p q data=data..string.format("%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s,%s,%.1f,%s\n",t,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q) end - + -- Save file. _savefile(filename, data) end @@ -17516,7 +17543,7 @@ function AIRBOSS:onbeforeSave(From, Event, To, path, filename) 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.") @@ -17540,7 +17567,7 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) f:write(data) f:close() end - + -- Set path or default. if lfs then path=path or lfs.writedir() @@ -17556,31 +17583,31 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) -- Header line local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type,Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n" - + -- Loop over all players. local n=0 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<=360 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,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case, @@ -17588,11 +17615,11 @@ function AIRBOSS:onafterSave(From, Event, To, path, filename) n=n+1 end end - + -- Info local text=string.format("Saving %d player LSO grades to file %s", n, filename) - self:I(self.lid..text) - + self:I(self.lid..text) + -- Save file. _savefile(filename, scores) end @@ -17603,7 +17630,7 @@ end -- @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 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) @@ -17617,18 +17644,18 @@ function AIRBOSS:onbeforeLoad(From, Event, To, path, filename) return false end end - + -- Check io module is available. if not io then self:E(self.lid.."WARNING: 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() @@ -17671,7 +17698,7 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) f:close() return data end - + -- Set path or default. if lfs then path=path or lfs.writedir() @@ -17695,7 +17722,7 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) -- Split by line break. local playergrades=UTILS.Split(data,"\n") - + -- Remove first header line. table.remove(playergrades, 1) @@ -17708,13 +17735,13 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) -- 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, -- time, wind, airframe, modex, carriertype, carriername, theatre, date @@ -17741,24 +17768,24 @@ function AIRBOSS:onafterLoad(From, Event, To, path, filename) grade.theatre=gradedata[15] or "n/a" grade.mitime=gradedata[16] or "n/a" grade.midate=gradedata[17] or "n/a" - grade.osdate=gradedata[18] or "n/a" - + grade.osdate=gradedata[18] or "n/a" + -- Init player table if necessary. self.playerscores[playername]=self.playerscores[playername] or {} - + -- Add grade to table. table.insert(self.playerscores[playername], grade) - + n=n+1 - + -- Debug info. - self:T2({playername, self.playerscores[playername]}) + self:T2({playername, self.playerscores[playername]}) end - + -- Info message. local text=string.format("Loaded %d player LSO grades from file %s", n, filename) - self:I(self.lid..text) - + self:I(self.lid..text) + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 6f8c5fd79..16b0fd7c0 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -59,7 +59,6 @@ Functional/Suppression.lua Functional/PseudoATC.lua Functional/Warehouse.lua Functional/Fox.lua -Functional/SWAPR.lua Ops/Airboss.lua Ops/RecoveryTanker.lua From f32094aa43aa939ab8c7b1f326e0ed157a4e51f4 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 12 Aug 2019 22:55:56 +0200 Subject: [PATCH 345/485] up --- Moose Development/Moose/Ops/RecoveryTanker.lua | 7 +++++++ Moose Development/Moose/Ops/RescueHelo.lua | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index e77f91ad2..a3af32cb9 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -418,6 +418,13 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) -- @param #RECOVERYTANKER self -- @param #number delay Delay in seconds. + --- On after "Start" event function. Called when FSM is started. + -- @function [parent=#RECOVERYTANKER] OnAfterStart + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + --- Triggers the FSM event "RefuelStart" when the tanker starts refueling another aircraft. -- @function [parent=#RECOVERYTANKER] RefuelStart diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index d033713b5..350722c50 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -346,6 +346,12 @@ function RESCUEHELO:New(carrierunit, helogroupname) -- @param #RESCUEHELO self -- @param #number delay Delay in seconds. + --- On after "Start" event function. Called when FSM is started. + -- @function [parent=#RESCUEHELO] OnAfterStart + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. --- Triggers the FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. -- @function [parent=#RESCUEHELO] Rescue From 28a23c21f458b1ce9a07a684c2c5732f1b111f34 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 14 Aug 2019 00:35:54 +0200 Subject: [PATCH 346/485] Update Airboss.lua --- Moose Development/Moose/Ops/Airboss.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 181b4ff08..079536a50 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -5973,7 +5973,7 @@ function AIRBOSS:_ScanCarrierZone() local awacs2marshal = isawacs and self.awacs:IsReturning() and self.awacs.airbase:GetName()==self.airbase:GetName() and knownflight.flag==-100 and self.awacs.recovery==true -- Put flight into Marshal. - local putintomarshal=closein>UTILS.NMToMeters(5) and knownflight.flag==-100 and iscarriersquad and istanker==false and isawacs==false + local putintomarshal=closein>UTILS.NMToMeters(5) and knownflight.flag==-100 and iscarriersquad and (not istanker) and (not isawacs) -- Send AI flight to marshal stack if group closes in more than 5 and has initial flag value. if putintomarshal or tanker2marshal or awacs2marshal then From e8a7def26e8c45937108fe36ec9c50da83175969 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 14 Aug 2019 21:51:55 +0200 Subject: [PATCH 347/485] Update Airboss.lua --- Moose Development/Moose/Ops/Airboss.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 079536a50..960ea2510 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -12737,7 +12737,7 @@ function AIRBOSS:_Debrief(playerData) table.insert(self.playerscores[playerData.name], mygrade) -- Trigger grading event. - self:LSOGrade(playerdata, mygrade) + self:LSOGrade(playerData, mygrade) -- LSO grade: (OK) 3.0 PT - LURIM local text=string.format("%s %.1f PT - %s", grade, Points, analysis) From 23497dddd44b9cf91d35b6a117d0f6e163bb6b13 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 14 Aug 2019 22:32:45 +0200 Subject: [PATCH 348/485] ARTY v1.1.3 Unfixed workaround for wrong markpoint coordinates. Was fixed in DCS 2.5.5.34644 --- Moose Development/Moose/Functional/Artillery.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index e5d30114c..dac84c96d 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -695,7 +695,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #string version -ARTY.version="1.1.2" +ARTY.version="1.1.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2474,9 +2474,10 @@ function ARTY:_OnEventMarkChange(Event) if Event.text~=nil and Event.text:lower():find("arty") then -- Convert (wrong x-->z, z-->x) vec3 - -- TODO: This needs to be "fixed", once DCS gives the correct numbers for x and z. - -- local vec3={y=Event.pos.y, x=Event.pos.x, z=Event.pos.z} - local vec3={y=Event.pos.y, x=Event.pos.z, z=Event.pos.x} + -- DONE: This needs to be "fixed", once DCS gives the correct numbers for x and z. + -- Was fixed in DCS 2.5.5.34644! + local vec3={y=Event.pos.y, x=Event.pos.x, z=Event.pos.z} + --local vec3={y=Event.pos.y, x=Event.pos.z, z=Event.pos.x} -- Get coordinate from vec3. local _coord=COORDINATE:NewFromVec3(vec3) From 97e1a65b522cc4ff7604ba5019201aa624ba5ced Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 14 Aug 2019 22:36:39 +0200 Subject: [PATCH 349/485] WAREHOUSE v0.9.6 - respawn after destroyed keeps assets --- Moose Development/Moose/Functional/Warehouse.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 087852348..cc03d7b1b 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1749,7 +1749,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.9.5" +WAREHOUSE.version="0.9.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. From fd1eb5f601e432b0e6e2da21fce3afe20d9071be Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 16 Aug 2019 22:15:29 +0200 Subject: [PATCH 350/485] AI_A2A_DISPATCHER and AI_A2G_DISPATCHER ... Fixed in-air refuelling problem. Now refuelling is executed instateneously when patrolling in the air and the fuel treshold has been reached. --- Moose Development/Moose/AI/AI_A2A.lua | 2 +- Moose Development/Moose/AI/AI_Air.lua | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A.lua b/Moose Development/Moose/AI/AI_A2A.lua index 2b3269b74..afd7cc70d 100644 --- a/Moose Development/Moose/AI/AI_A2A.lua +++ b/Moose Development/Moose/AI/AI_A2A.lua @@ -669,7 +669,7 @@ function AI_A2A:onafterRefuel( AIGroup, From, Event, To ) local ToRefuelSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) --- Create a route point of type air. - local ToRefuelRoutePoint = ToRefuelCoord:WaypointAir( + local ToRefuelRoutePoint = CurrentCoord:WaypointAir( self.PatrolAltType, 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 fe62ff71f..0bee1f1ab 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -704,7 +704,7 @@ function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) ) --- Create a route point of type air. - local ToRefuelRoutePoint = ToRefuelCoord:WaypointAir( + local ToRefuelRoutePoint = FromRefuelCoord:WaypointAir( self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, @@ -715,7 +715,7 @@ function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) self:F( { ToRefuelSpeed = ToRefuelSpeed } ) RefuelRoute[#RefuelRoute+1] = FromRefuelRoutePoint - RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint + --RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint AIGroup:OptionROEHoldFire() AIGroup:OptionROTEvadeFire() From 94476e418e8891e5dfba8858b771fcab5ad139da Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 17 Aug 2019 10:25:26 +0200 Subject: [PATCH 351/485] Messaging and Tactical Menu --- .../Moose/AI/AI_A2A_Dispatcher.lua | 101 ++++++++++++++++++ .../Moose/AI/AI_A2G_Dispatcher.lua | 16 +-- .../Moose/Tasking/DetectionManager.lua | 27 ++++- 3 files changed, 134 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index c19b2fec7..eb7c8d004 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -3104,18 +3104,26 @@ do -- AI_A2A_DISPATCHER self:F({"CAP Birth", Defender:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + local DefenderName = Defender:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) if Squadron then + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne. Starting Patrol." ) Fsm:__Patrol( 2 ) -- Start Patrolling end end function Fsm:onafterRTB( Defender, From, Event, To ) + self:F({"CAP RTB", Defender:GetName()}) + self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + + local DefenderName = Defender:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) Dispatcher:ClearDefenderTaskTarget( Defender ) end @@ -3291,11 +3299,13 @@ do -- AI_A2A_DISPATCHER self:F({"GCI Birth", Defender:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + local DefenderName = Defender:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) if DefenderTarget then + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne. Engaging target!" ) Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit end end @@ -3304,7 +3314,10 @@ do -- AI_A2A_DISPATCHER self:F({"GCI RTB", Defender:GetName()}) self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + local DefenderName = Defender:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) Dispatcher:ClearDefenderTaskTarget( Defender ) end @@ -3326,8 +3339,11 @@ do -- AI_A2A_DISPATCHER self:F({"GCI Home", Defender:GetName()}) self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + local DefenderName = Defender:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) @@ -3412,6 +3428,91 @@ do -- AI_A2A_DISPATCHER return nil, nil end + --- Shows the tactical display. + -- @param #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:ShowTacticalDisplay( Detection ) + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local TaskReport = REPORT:New() + + local Report = REPORT:New( "Tactical Overviews" ) + + local DefenderGroupCount = 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 + + -- 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 and Defender:IsAlive() then + DefenderGroupCount = DefenderGroupCount + 1 + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", + Defender:GetName(), + Defender:GetSize(), + Defender:GetInitialSize(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + end + + 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*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", + Defender:GetName(), + Defender:GetSize(), + Defender:GetInitialSize(), + 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 ) + + return true + + end --- Assigns A2A AI Tasks in relation to the detected items. -- @param #AI_A2A_DISPATCHER self diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 498c7a574..fa6177178 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3521,15 +3521,15 @@ do -- AI_A2G_DISPATCHER end end - function Fsm:onafterPatrolRoute( Defender, From, Event, To, AttackSetUnit ) + function Fsm:onafterPatrolRoute( Defender, From, Event, To ) self:F({"Defender PatrolRoute", Defender:GetName()}) - self:GetParent(self).onafterPatrolRoute( self, Defender, From, Event, To, AttackSetUnit ) + self:GetParent(self).onafterPatrolRoute( self, Defender, From, Event, To ) local DefenderName = Defender:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) if Squadron then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " patrolling." ) end Dispatcher:ClearDefenderTaskTarget( Defender ) @@ -3627,7 +3627,7 @@ do -- AI_A2G_DISPATCHER self:F( { DefenderTarget = DefenderTarget } ) if DefenderTarget then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne." ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne. Engaging!" ) Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end @@ -3644,7 +3644,7 @@ do -- AI_A2G_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route, bearing " .. Coordinate:ToString( Defender ) ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route to ground target at " .. Coordinate:ToStringA2G( Defender ) ) end end @@ -3659,7 +3659,7 @@ do -- AI_A2G_DISPATCHER if FirstUnit then local Coordinate = FirstUnit:GetCoordinate() - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging target, bearing " .. Coordinate:ToString( Defender ) ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging ground target at " .. Coordinate:ToStringA2G( Defender ) ) end end @@ -3669,7 +3669,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = Defender:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " RTB." ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) @@ -3684,7 +3684,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = Defender:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) + --Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) if Defender:IsAboveRunway() then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index b641ac1c3..9f827e3d4 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -221,6 +221,26 @@ 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:SetTacticalMenu( DispatcherMainMenuText, DispatcherMenuText ) + + local DispatcherMainMenu = MENU_MISSION:New( DispatcherMainMenuText, nil ) + local DispatcherMenu = MENU_MISSION_COMMAND:New( DispatcherMenuText, DispatcherMainMenu, + function() + self:ShowTacticalDisplay( self.Detection ) + end + ) + + return self + end + + + --- Set a command center to communicate actions to the players reporting to the command center. -- @param #DETECTION_MANAGER self @@ -242,8 +262,11 @@ do -- DETECTION MANAGER self:F( { Message = Message } ) - if self.CC then - self.CC:MessageToCoalition( Message ) + if not self.PreviousMessage or self.PreviousMessage ~= Message then + self.PreviousMessage = Message + if self.CC then + self.CC:MessageToCoalition( Message ) + end end return self From a216eb4e749901a3a01eb2245e97ee9ebae2a899 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 18 Aug 2019 16:44:20 +0200 Subject: [PATCH 352/485] Defense test for TASK_CAPTURE_ZONE --- .../Moose/AI/AI_A2G_Dispatcher.lua | 101 ++++++++++++++++++ Moose Development/Moose/Tasking/TaskInfo.lua | 2 +- .../Moose/Tasking/Task_Capture_Dispatcher.lua | 22 ++++ .../Moose/Tasking/Task_Capture_Zone.lua | 21 +++- 4 files changed, 143 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index fa6177178..b9552e00b 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -4043,6 +4043,107 @@ do -- AI_A2G_DISPATCHER return ShortestDistance end + + --- Shows the tactical display. + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ShowTacticalDisplay( Detection ) + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local TaskReport = REPORT:New() + + local DefenseTotal = 0 + + local Report = REPORT:New( "\nTactical Overview" ) + + local DefenderGroupCount = 0 + local DefendersTotal = 0 + + -- Now that all obsolete tasks are removed, loop through the detected targets. + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + + if not self.Detection:IsDetectedItemLocked( DetectedItem ) == true then + 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 } ) + + self:F( { DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + -- Show tactical situation + local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() + Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "â– ", ThreatLevel ) ) ) + 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 + + 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 ) ) + + 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 + + Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) ) + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + Report:Add( string.format( " - %s - %d", DefenderSquadronName, DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount or "n/a" ) ) + end + + self:F( Report:Text( "\n" ) ) + trigger.action.outText( Report:Text( "\n" ), 25 ) + + end --- Assigns A2G AI Tasks in relation to the detected items. -- @param #AI_A2G_DISPATCHER self diff --git a/Moose Development/Moose/Tasking/TaskInfo.lua b/Moose Development/Moose/Tasking/TaskInfo.lua index 8f2d6231a..54c59410d 100644 --- a/Moose Development/Moose/Tasking/TaskInfo.lua +++ b/Moose Development/Moose/Tasking/TaskInfo.lua @@ -52,7 +52,7 @@ end --- Add taskinfo. -- @param #TASKINFO self --- @param #string The info key. +-- @param #string Key The info key. -- @param Data The data of the info. -- @param #number Order The display order, which is a number from 0 to 100. -- @param #TASKINFO.Detail Detail The detail Level. diff --git a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua index 74dff588e..c7c3227da 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua @@ -194,6 +194,25 @@ do -- TASK_CAPTURE_DISPATCHER end + --- Link a task capture dispatcher from the other coalition to understand its plan for defenses. + -- This is used for the tactical overview, so the players also know the zones attacked by the other coalition! + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param #TASK_CAPTURE_DISPATCHER DefenseTaskCaptureDispatcher + function TASK_CAPTURE_DISPATCHER:SetDefenseTaskCaptureDispatcher( DefenseTaskCaptureDispatcher ) + + self.DefenseTaskCaptureDispatcher = DefenseTaskCaptureDispatcher + end + + + --- Get the linked task capture dispatcher from the other coalition to understand its plan for defenses. + -- This is used for the tactical overview, so the players also know the zones attacked by the other coalition! + -- @param #TASK_CAPTURE_DISPATCHER self + -- @return #TASK_CAPTURE_DISPATCHER + function TASK_CAPTURE_DISPATCHER:GetDefenseTaskCaptureDispatcher() + + return self.DefenseTaskCaptureDispatcher + end + --- Add a capture zone task. -- @param #TASK_CAPTURE_DISPATCHER self @@ -274,6 +293,9 @@ do -- TASK_CAPTURE_DISPATCHER CaptureZone.Task.TaskPrefix = CaptureZone.TaskPrefix -- We keep the TaskPrefix for further reference! Mission:AddTask( CaptureZone.Task ) TaskReport:Add( TaskName ) + + -- Link the Task Dispatcher to the capture zone task, because it is used on the UpdateTaskInfo. + CaptureZone.Task:SetDispatcher( self ) CaptureZone.Task:UpdateTaskInfo() function CaptureZone.Task.OnEnterAssigned( Task, From, Event, To ) diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index 9ab0f35c7..73032a60d 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -228,11 +228,28 @@ do -- TASK_CAPTURE_ZONE local ZoneCoordinate = self.ZoneGoal:GetZone():GetCoordinate() self.TaskInfo:AddTaskName( 0, "MSOD", Persist ) self.TaskInfo:AddCoordinate( ZoneCoordinate, 1, "SOD", Persist ) - self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD", Persist ) - self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD", Persist ) +-- self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD", Persist ) +-- self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD", Persist ) local SetUnit = self.ZoneGoal.Zone:GetScannedSetUnit() local ThreatLevel, ThreatText = SetUnit:CalculateThreatLevelA2G() self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 20, "MOD", Persist ) + + if self.Dispatcher then + local DefenseTaskCaptureDispatcher = self.Dispatcher:GetDefenseTaskCaptureDispatcher() -- Tasking.Task_Capture_Dispatcher#TASK_CAPTURE_DISPATCHER + + if DefenseTaskCaptureDispatcher then + -- Loop through all zones of the Defenses, and check which zone has an assigned task! + for TaskName, CaptureZone in pairs( DefenseTaskCaptureDispatcher.Zones or {} ) do + local Task = CaptureZone.Task -- Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE + if Task then + if Task:IsStateAssigned() then + self.TaskInfo:AddInfo( "Defense", Task.ZoneGoal:GetName() .. ", " .. Task.ZoneGoal:GetZone():GetCoordinate(), 30, "MOD", Persist ) + end + end + end + end + end + end From ec85b42a388fef0cd8febae4d55fedced59b36d3 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 18 Aug 2019 22:21:41 +0200 Subject: [PATCH 353/485] OPS AIRBOSS v1.0.8 - Fixed bug that status reset after wave off causes a unitcorn grade. - Optimized radio scheduler calls ==> leads to less mem consumtion according to collectgarbage("count"). RESCUEHELO v1.0.9 - Adjusted default follow time interval to 1.0 sec. - Added SetFollowTimeInterval() function. - Respawn after returned delayed by 5 sec. GROUP - Added nil check in RespawnAtCurrentAirbase() function. --- Moose Development/Moose/Ops/Airboss.lua | 167 ++++++++++++----- Moose Development/Moose/Ops/RescueHelo.lua | 25 ++- Moose Development/Moose/Wrapper/Group.lua | 199 +++++++++++---------- 3 files changed, 250 insertions(+), 141 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 960ea2510..e8a88ec21 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -48,11 +48,15 @@ -- -- Heatblur's mighty F-14B Tomcat has been added (March 13th 2019) as well. -- --- -- ## Discussion -- -- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-airboss channel. -- There you also find an example mission and the necessary voice over sound files. Check out the **pinned messages**. +-- +-- ## Example Missions +-- +-- Example missions can be found [here](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Airboss). +-- They contain the latest development Moose.lua file. -- -- ## IMPORTANT -- @@ -1671,6 +1675,7 @@ AIRBOSS.Difficulty={ -- @field #boolean showhints If true, show step hints. -- @field #table trapsheet Groove data table recorded every 0.5 seconds. -- @field #boolean trapon If true, save trap sheets. +-- @field #string debriefschedulerID Debrief scheduler ID. -- @extends #AIRBOSS.FlightGroup --- Main group level radio menu: F10 Other/Airboss. @@ -1683,7 +1688,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.0.7" +AIRBOSS.version="1.0.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1944,7 +1949,7 @@ function AIRBOSS:New(carriername, alias) self.Debug=true BASE:TraceOnOff(true) BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) + BASE:TraceLevel(3) --self.dTstatus=0.1 end @@ -2265,6 +2270,29 @@ function AIRBOSS:New(carriername, alias) -- @param #AIRBOSS.LSOgrade grade LSO grade. + --- Triggers the FSM event "LSOGrade". Called when the LSO grades a player + -- @function [parent=#AIRBOSS] LSOGrade + -- @param #AIRBOSS self + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- Triggers the FSM event "LSOGrade". Delayed called when the LSO grades a player. + -- @function [parent=#AIRBOSS] __LSOGrade + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- On after "LSOGrade" user function. Called when the carrier passes a waypoint of its route. + -- @function [parent=#AIRBOSS] OnAfterLSOGrade + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. -- @function [parent=#AIRBOSS] Stop -- @param #AIRBOSS self @@ -2494,7 +2522,8 @@ end function AIRBOSS:CloseCurrentRecoveryWindow(delay) if delay and delay>0 then - SCHEDULER:New(nil, self.CloseCurrentRecoveryWindow, {self}, delay) + --SCHEDULER:New(nil, self.CloseCurrentRecoveryWindow, {self}, delay) + self:ScheduleOnce(delay, self.CloseCurrentRecoveryWindow, self) else if self:IsRecovering() and self.recoverywindow and self.recoverywindow.OPEN then self:RecoveryStop() @@ -2544,7 +2573,8 @@ function AIRBOSS:DeleteRecoveryWindow(window, delay) if delay and delay>0 then -- Delayed call. - SCHEDULER:New(nil, self.DeleteRecoveryWindow, {self, window}, delay) + --SCHEDULER:New(nil, self.DeleteRecoveryWindow, {self, window}, delay) + self:ScheduleOnce(delay, self.DeleteRecoveryWindow, self, window) else for i,_recovery in pairs(self.recoverytimes) do @@ -2964,7 +2994,8 @@ function AIRBOSS:SoundCheckLSO(delay) if delay and delay>0 then -- Delayed call. - SCHEDULER:New(nil, AIRBOSS.SoundCheckLSO, {self}, delay) + --SCHEDULER:New(nil, AIRBOSS.SoundCheckLSO, {self}, delay) + self:ScheduleOnce(delay, AIRBOSS.SoundCheckLSO, self) else @@ -2999,7 +3030,8 @@ function AIRBOSS:SoundCheckMarshal(delay) if delay and delay>0 then -- Delayed call. - SCHEDULER:New(nil, AIRBOSS.SoundCheckMarshal, {self}, delay) + --SCHEDULER:New(nil, AIRBOSS.SoundCheckMarshal, {self}, delay) + self:ScheduleOnce(delay, AIRBOSS.SoundCheckMarshal, self) else @@ -3249,9 +3281,13 @@ function AIRBOSS:onafterStart(From, Event, To) self:_ActivateBeacons() -- 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) - + --self.RQLid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQLSO, "LSO"}, 1, 0.1) + --self.RQMid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQMarshal, "MARSHAL"}, 1, 0.1) + + --self:I("FF: starting timer.scheduleFunction") + --timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQLSO, name="LSO"}, timer.getTime()+1) + --timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQMarshal, name="MARSHAL"}, timer.getTime()+1) + -- Initial carrier position and orientation. self.Cposition=self:GetCoordinate() self.Corientation=self.carrier:GetOrientationX() @@ -3687,7 +3723,7 @@ function AIRBOSS:_CheckRecoveryTimes() -- Closed. recovery.OPEN=false - + -- Window just closed. recovery.OVER=true @@ -4023,6 +4059,8 @@ end -- @param #string Event Event. -- @param #string To To state. function AIRBOSS:onafterStop(From, Event, To) + self:I(self.lid..string.format("Stopping airboss script.")) + -- Unhandle events. self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.Land) @@ -4032,6 +4070,8 @@ function AIRBOSS:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Ejection) self:UnHandleEvent(EVENTS.PlayerLeaveUnit) self:UnHandleEvent(EVENTS.MissionEnd) + + self.CallScheduler:Clear() end ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5848,7 +5888,8 @@ function AIRBOSS:_SetPlayerStep(playerData, step, delay) if delay and delay>0 then -- Delayed call. - SCHEDULER:New(nil, self._SetPlayerStep, {self, playerData, step}, delay) + --SCHEDULER:New(nil, self._SetPlayerStep, {self, playerData, step}, delay) + self:ScheduleOnce(delay, self._SetPlayerStep, self, playerData, step) else -- Check if player still exists after possible delay. @@ -7316,6 +7357,7 @@ function AIRBOSS:_InitPlayer(playerData, step) playerData.TIG0=nil playerData.wire=nil playerData.flag=-100 + playerData.debriefschedulerID=nil -- 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 @@ -7796,6 +7838,14 @@ function AIRBOSS:_RemoveFlight(flight, completely) self:_RemoveFlightFromQueue(self.flights, flight) else + + -- Remove all grades until a final grade is reached. + local grades=self.playerscores[flight.name] + if grades and #grades>0 then + while #grades>0 and grades[#grades].finalscore==nil do + table.remove(grades, #grades) + end + end -- Check if flight should be completely removed, e.g. after the player died or simply left the slot. if completely then @@ -7806,15 +7856,6 @@ function AIRBOSS:_RemoveFlight(flight, completely) -- Remove completely. self:_RemoveFlightFromQueue(self.flights, flight) - -- Remove all grades until a final grade is reached. - local grades=self.playerscores[flight.name] - if grades and #grades>0 then - while #grades>0 and grades[#grades].finalscore==nil do - table.remove(grades, #grades) - end - end - - -- Remove player from players table. local playerdata=self.players[flight.name] if playerdata then @@ -8005,7 +8046,8 @@ function AIRBOSS:_CheckPlayerStatus() elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then -- Debriefing in 5 seconds. - SCHEDULER:New(nil, self._Debrief, {self, playerData}, 5) + --SCHEDULER:New(nil, self._Debrief, {self, playerData}, 5) + playerData.debriefschedulerID=self:ScheduleOnce(5, self._Debrief, self, playerData) -- Undefined status. playerData.step=AIRBOSS.PatternStep.UNDEFINED @@ -8186,7 +8228,8 @@ function AIRBOSS:OnEventBirth(EventData) self:_AddF10Commands(_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) + --SCHEDULER:New(nil, self._NewPlayer, {self, _unitName}, 1) + self:ScheduleOnce(1, self._NewPlayer, self, _unitName) end end @@ -8339,7 +8382,8 @@ function AIRBOSS:OnEventLand(EventData) self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.UNDEFINED) -- Call trapped function in 1 second to make sure we did not bolter. - SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1) + --SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1) + self:ScheduleOnce(1, self._Trapped, self, playerData) end @@ -10233,7 +10277,8 @@ function AIRBOSS:_Trapped(playerData) end -- Call function again and check if converged or back in air. - SCHEDULER:New(nil, self._Trapped, {self, playerData}, 0.1) + --SCHEDULER:New(nil, self._Trapped, {self, playerData}, 0.1) + self:ScheduleOnce(0.1, self._Trapped, self, playerData) return end @@ -12672,6 +12717,9 @@ end -- @param #AIRBOSS.PlayerData playerData Player data. function AIRBOSS:_Debrief(playerData) self:F(self.lid..string.format("Debriefing of player %s.", playerData.name)) + + -- Delete scheduler ID. + playerData.debriefschedulerID=nil -- Switch attitude monitor off if on. playerData.attitudemonitor=false @@ -12735,7 +12783,7 @@ function AIRBOSS:_Debrief(playerData) -- Add LSO grade to player grades table. table.insert(self.playerscores[playerData.name], mygrade) - + -- Trigger grading event. self:LSOGrade(playerData, mygrade) @@ -14299,6 +14347,14 @@ end -- RADIO MESSAGE Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Function called by DCS timer. Unused. +-- @param #table param Parameters. +-- @param #number time Time. +function AIRBOSS._CheckRadioQueueT(param, time) + AIRBOSS._CheckRadioQueue(param.airboss, param.radioqueue, param.name) + return time+0.05 +end + --- Radio queue item. -- @type AIRBOSS.Radioitem -- @field #number Tplay Abs time when transmission should be played. @@ -14315,37 +14371,50 @@ end -- @param #string name Name of the queue. function AIRBOSS:_CheckRadioQueue(radioqueue, name) + --env.info(string.format("FF %s #radioqueue %d", name, #radioqueue)) + -- Check if queue is empty. if #radioqueue==0 then + + if name=="LSO" then + self:T(self.lid..string.format("Stopping LSO radio queue.")) + self.radiotimer:Stop(self.RQLid) + self.RQLid=nil + elseif name=="MARSHAL" then + self:T(self.lid..string.format("Stopping Marshal radio queue.")) + self.radiotimer:Stop(self.RQMid) + self.RQMid=nil + end + return end -- Get current abs time. - local time=timer.getAbsTime() + local _time=timer.getAbsTime() local playing=false local next=nil --#AIRBOSS.Radioitem - local remove=nil + 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 + 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.call.duration then + if _time>=transmission.Tstarted+transmission.call.duration then -- Transmission over. transmission.isplaying=false - remove=i + _remove=i if transmission.radio.alias=="LSO" then - self.TQLSO=time + self.TQLSO=_time elseif transmission.radio.alias=="MARSHAL" then - self.TQMarshal=time + self.TQMarshal=_time end else -- still playing @@ -14375,7 +14444,7 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) else - if time-Tlast>=transmission.interval then + if _time-Tlast>=transmission.interval then next=transmission else @@ -14400,14 +14469,15 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) if next~=nil and not playing then self:Broadcast(next.radio, next.call, next.loud) next.isplaying=true - next.Tstarted=time + next.Tstarted=_time end -- Remove completed calls from queue. - if remove then - table.remove(radioqueue, remove) + if _remove then + table.remove(radioqueue, _remove) end - + + return end --- Add Radio transmission to radio queue. @@ -14455,12 +14525,23 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay, interval, click, pi table.insert(self.RQLSO, transmission) caller="LSOCall" - + + -- Schedule radio queue checks. + if not self.RQLid then + self:T(self.lid..string.format("Starting LSO radio queue.")) + self.RQLid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQLSO, "LSO"}, 0.02, 0.05) + end + elseif radio.alias=="MARSHAL" then table.insert(self.RQMarshal, transmission) caller="MarshalCall" + + if not self.RQMid then + self:T(self.lid..string.format("Starting Marhal radio queue.")) + self.RQMid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQMarshal, "MARSHAL"}, 0.02, 0.05) + end end @@ -14743,7 +14824,8 @@ function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration if delay and delay>0 then -- Delayed call. - SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) + --SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) + self:ScheduleOnce(delay, self.MessageToPlayer, self, playerData, message, sender, receiver, duration, clear) else -- Wait until previous sound finished. @@ -15797,6 +15879,11 @@ function AIRBOSS:_ResetPlayerStatus(_unitName) -- Remove flight from queues. Collapse marshal stack if necessary. -- Section members are removed from the Spinning queue. If flight is member, he is removed from the section. self:_RemoveFlight(playerData) + + -- Stop pending debrief scheduler. + if playerData.debriefschedulerID and self.Scheduler then + self.Scheduler:Stop(playerData.debriefschedulerID) + end -- Initialize player data. self:_InitPlayer(playerData) diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 350722c50..0a040f32e 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -57,6 +57,7 @@ -- @field #string alias Alias of the spawn group. -- @field #number uid Unique ID of this helo. -- @field #number modex Tail number of the helo. +-- @field #number dtFollow Follow time update interval in seconds. Default 1.0 sec. -- @extends Core.Fsm#FSM --- Rescue Helo @@ -227,6 +228,7 @@ RESCUEHELO = { alias = nil, uid = 0, modex = nil, + dtFollow = nil, } --- Unique ID (global). @@ -235,7 +237,7 @@ _RESCUEHELOID=0 --- Class version. -- @field #string version -RESCUEHELO.version="1.0.8" +RESCUEHELO.version="1.0.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -303,6 +305,7 @@ function RESCUEHELO:New(carrierunit, helogroupname) self:SetRescueZone() self:SetRescueHoverSpeed() self:SetRescueDuration() + self:SetFollowTimeInterval() self:SetRescueStopBoatOff() -- Some more. @@ -644,6 +647,15 @@ function RESCUEHELO:SetModex(modex) return self end +--- Set follow time update interval. +-- @param #RESCUEHELO self +-- @param #number dt Time interval in seconds. Default 1.0 sec. +-- @return #RESCUEHELO self +function RESCUEHELO:SetFollowTimeInterval(dt) + self.dtFollow=dt or 1.0 + 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. @@ -895,7 +907,7 @@ function RESCUEHELO:onafterStart(From, Event, To) -- Use an uncontrolled aircraft group. self.helo=GROUP:FindByName(self.helogroupname) - if self.helo:IsAlive() then + if self.helo and self.helo:IsAlive() then -- Start uncontrolled group. self.helo:StartUncontrolled() @@ -940,6 +952,9 @@ function RESCUEHELO:onafterStart(From, Event, To) -- Formation parameters. self.formation:FormationCenterWing(-self.offsetX, 50, math.abs(self.altitude), 50, self.offsetZ, 50) + -- Set follow time interval. + self.formation:SetFollowTimeInterval(self.dtFollow) + -- Formation mode. self.formation:SetFlightModeFormation(self.helo) @@ -1005,7 +1020,7 @@ function RESCUEHELO:onafterStatus(From, Event, To) else -- Send helo back to base. - self:RTB() + self:RTB(self.airbase) end @@ -1196,7 +1211,9 @@ function RESCUEHELO:onafterReturned(From, Event, To, airbase) self.helo:InitModex(self.modex) -- Respawn helo at current airbase. - self.helo:RespawnAtCurrentAirbase() + if self.helo and self.helo:IsAlive() then + self:ScheduleOnce(5, self.helo.RespawnAtCurrentAirbase, self.helo) + end -- Restart the formation. self:__Run(10) diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index a6f372e1a..8c855be45 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1773,110 +1773,115 @@ end function GROUP:RespawnAtCurrentAirbase(SpawnTemplate, Takeoff, Uncontrolled) -- R2.4 self:F2( { SpawnTemplate, Takeoff, Uncontrolled} ) - -- Get closest airbase. Should be the one we are currently on. - local airbase=self:GetCoordinate():GetClosestAirbase() - - if airbase then - self:F2("Closest airbase = "..airbase:GetName()) - else - self:E("ERROR: could not find closest airbase!") - return nil - end - -- Takeoff type. Default hot. - Takeoff = Takeoff or SPAWN.Takeoff.Hot - - -- Coordinate of the airbase. - local AirbaseCoord=airbase:GetCoordinate() - - -- Spawn template. - SpawnTemplate = SpawnTemplate or self:GetTemplate() + if self and self:IsAlive() then - if SpawnTemplate then - - local SpawnPoint = SpawnTemplate.route.points[1] - - -- These are only for ships. - SpawnPoint.linkUnit = nil - SpawnPoint.helipadId = nil - SpawnPoint.airdromeId = nil - - -- Aibase id and category. - local AirbaseID = airbase:GetID() - local AirbaseCategory = airbase:GetDesc().category + -- Get closest airbase. Should be the one we are currently on. + local airbase=self:GetCoordinate():GetClosestAirbase() - if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then - SpawnPoint.linkUnit = AirbaseID - SpawnPoint.helipadId = AirbaseID - elseif AirbaseCategory == Airbase.Category.AIRDROME then - SpawnPoint.airdromeId = AirbaseID + if airbase then + self:F2("Closest airbase = "..airbase:GetName()) + else + self:E("ERROR: could not find closest airbase!") + return nil end - + -- Takeoff type. Default hot. + Takeoff = Takeoff or SPAWN.Takeoff.Hot - SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type - SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + -- Coordinate of the airbase. + local AirbaseCoord=airbase:GetCoordinate() - -- Get the units of the group. - local units=self:GetUnits() - - local x - local y - for UnitID=1,#units do + -- Spawn template. + SpawnTemplate = SpawnTemplate or self:GetTemplate() + + if SpawnTemplate then + + local SpawnPoint = SpawnTemplate.route.points[1] + + -- These are only for ships. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Aibase id and category. + local AirbaseID = airbase:GetID() + local AirbaseCategory = airbase:GetDesc().category + + if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.AIRDROME then + SpawnPoint.airdromeId = AirbaseID + end + + + SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + + -- Get the units of the group. + local units=self:GetUnits() + + local x + local y + for UnitID=1,#units do + + local unit=units[UnitID] --Wrapper.Unit#UNIT + + -- Get closest parking spot of current unit. Note that we look for occupied spots since the unit is currently sitting on it! + local Parkingspot, TermialID, Distance=unit:GetCoordinate():GetClosestParkingSpot(airbase) - local unit=units[UnitID] --Wrapper.Unit#UNIT - - -- Get closest parking spot of current unit. Note that we look for occupied spots since the unit is currently sitting on it! - local Parkingspot, TermialID, Distance=unit:GetCoordinate():GetClosestParkingSpot(airbase) - - --Parkingspot:MarkToAll("parking spot") - self:T2(string.format("Closest parking spot distance = %s, terminal ID=%s", tostring(Distance), tostring(TermialID))) - - -- Get unit coordinates for respawning position. - local uc=unit:GetCoordinate() - --uc:MarkToAll(string.format("re-spawnplace %s terminal %d", unit:GetName(), TermialID)) - - SpawnTemplate.units[UnitID].x = uc.x --Parkingspot.x - SpawnTemplate.units[UnitID].y = uc.z --Parkingspot.z - SpawnTemplate.units[UnitID].alt = uc.y --Parkingspot.y - - SpawnTemplate.units[UnitID].parking = TermialID - SpawnTemplate.units[UnitID].parking_id = nil - - --SpawnTemplate.units[UnitID].unitId=nil - end - - --SpawnTemplate.groupId=nil - - SpawnPoint.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x - SpawnPoint.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z - SpawnPoint.alt = SpawnTemplate.units[1].alt --AirbaseCoord:GetLandHeight() - - SpawnTemplate.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x - SpawnTemplate.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z - - -- 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) + --Parkingspot:MarkToAll("parking spot") + self:T2(string.format("Closest parking spot distance = %s, terminal ID=%s", tostring(Distance), tostring(TermialID))) - -- Reset events. - self:ResetEvents() - - return self + -- Get unit coordinates for respawning position. + local uc=unit:GetCoordinate() + --uc:MarkToAll(string.format("re-spawnplace %s terminal %d", unit:GetName(), TermialID)) + + SpawnTemplate.units[UnitID].x = uc.x --Parkingspot.x + SpawnTemplate.units[UnitID].y = uc.z --Parkingspot.z + SpawnTemplate.units[UnitID].alt = uc.y --Parkingspot.y + + SpawnTemplate.units[UnitID].parking = TermialID + SpawnTemplate.units[UnitID].parking_id = nil + + --SpawnTemplate.units[UnitID].unitId=nil + end + + --SpawnTemplate.groupId=nil + + SpawnPoint.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x + SpawnPoint.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z + SpawnPoint.alt = SpawnTemplate.units[1].alt --AirbaseCoord:GetLandHeight() + + SpawnTemplate.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x + SpawnTemplate.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z + + -- 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) + + -- Reset events. + self:ResetEvents() + + return self + end + else + self:E("WARNING: GROUP is not alive!") end return nil From f951aae3ee7e2b84463b36b55ba5018bdcc843c7 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 21 Aug 2019 22:04:11 +0300 Subject: [PATCH 354/485] - Inherit ZONE_BASE from FSM instead of BASE. Opens a range of possibilities. - Remove from ZONE_GOAL the Zone field, and make it inherit from ZONE_BASE instead of FSM! - Rework the new inheritance tree in the code. (Remove .Zone fields). - Implement the determination of attack and defense zones. - Reworked the TaskInfo to include Type and ShowKey. - Flash A2G Tasking Details. Added menu option. --- Moose Development/Moose/Core/Database.lua | 34 ++ Moose Development/Moose/Core/Event.lua | 48 ++- Moose Development/Moose/Core/Set.lua | 322 +++++++++++++++++- Moose Development/Moose/Core/Zone.lua | 2 +- .../Moose/Functional/ZoneCaptureCoalition.lua | 21 +- .../Moose/Functional/ZoneGoal.lua | 18 +- .../Moose/Functional/ZoneGoalCoalition.lua | 2 +- .../Moose/Tasking/CommandCenter.lua | 2 +- Moose Development/Moose/Tasking/Task.lua | 23 ++ Moose Development/Moose/Tasking/TaskInfo.lua | 19 +- .../Moose/Tasking/Task_Capture_Dispatcher.lua | 26 ++ .../Moose/Tasking/Task_Capture_Zone.lua | 24 +- 12 files changed, 504 insertions(+), 37 deletions(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 6b281d920..3b85d0d6b 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -81,6 +81,7 @@ DATABASE = { HITS = {}, DESTROYS = {}, ZONES = {}, + ZONES_GOAL = {}, } local _DATABASECoalition = @@ -338,6 +339,39 @@ do -- Zones end -- zone +do -- Zone_Goal + + --- Finds a @{Zone} based on the zone name. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + -- @return Core.Zone#ZONE_BASE The found ZONE. + function DATABASE:FindZoneGoal( ZoneName ) + + local ZoneFound = self.ZONES_GOAL[ZoneName] + return ZoneFound + end + + --- Adds a @{Zone} based on the zone name in the DATABASE. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + -- @param Core.Zone#ZONE_BASE Zone The zone. + function DATABASE:AddZoneGoal( ZoneName, Zone ) + + if not self.ZONES_GOAL[ZoneName] then + self.ZONES_GOAL[ZoneName] = Zone + end + end + + + --- Deletes a @{Zone} from the DATABASE based on the zone name. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + function DATABASE:DeleteZoneGoal( ZoneName ) + + self.ZONES_GOAL[ZoneName] = nil + end + +end -- Zone_Goal do -- cargo --- Adds a Cargo based on the Cargo Name in the DATABASE. diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 0729dd498..4a6b96448 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -189,7 +189,9 @@ world.event.S_EVENT_NEW_CARGO = world.event.S_EVENT_MAX + 1000 world.event.S_EVENT_DELETE_CARGO = world.event.S_EVENT_MAX + 1001 world.event.S_EVENT_NEW_ZONE = world.event.S_EVENT_MAX + 1002 world.event.S_EVENT_DELETE_ZONE = world.event.S_EVENT_MAX + 1003 -world.event.S_EVENT_REMOVE_UNIT = world.event.S_EVENT_MAX + 1004 +world.event.S_EVENT_NEW_ZONE_GOAL = world.event.S_EVENT_MAX + 1004 +world.event.S_EVENT_DELETE_ZONE_GOAL = world.event.S_EVENT_MAX + 1005 +world.event.S_EVENT_REMOVE_UNIT = world.event.S_EVENT_MAX + 1006 --- The different types of events supported by MOOSE. @@ -226,6 +228,8 @@ EVENTS = { DeleteCargo = world.event.S_EVENT_DELETE_CARGO, NewZone = world.event.S_EVENT_NEW_ZONE, DeleteZone = world.event.S_EVENT_DELETE_ZONE, + NewZoneGoal = world.event.S_EVENT_NEW_ZONE_GOAL, + DeleteZoneGoal = world.event.S_EVENT_DELETE_ZONE_GOAL, RemoveUnit = world.event.S_EVENT_REMOVE_UNIT, } @@ -462,6 +466,16 @@ local _EVENTMETA = { Event = "OnEventDeleteZone", Text = "S_EVENT_DELETE_ZONE" }, + [EVENTS.NewZoneGoal] = { + Order = 1, + Event = "OnEventNewZoneGoal", + Text = "S_EVENT_NEW_ZONE_GOAL" + }, + [EVENTS.DeleteZoneGoal] = { + Order = 1, + Event = "OnEventDeleteZoneGoal", + Text = "S_EVENT_DELETE_ZONE_GOAL" + }, [EVENTS.RemoveUnit] = { Order = -1, Event = "OnEventRemoveUnit", @@ -800,6 +814,38 @@ do -- Event Creation world.onEvent( Event ) end + --- Creation of a New ZoneGoal Event. + -- @param #EVENT self + -- @param Core.Functional#ZONE_GOAL ZoneGoal The ZoneGoal created. + function EVENT:CreateEventNewZoneGoal( ZoneGoal ) + self:F( { ZoneGoal } ) + + local Event = { + id = EVENTS.NewZoneGoal, + time = timer.getTime(), + ZoneGoal = ZoneGoal, + } + + world.onEvent( Event ) + end + + + --- Creation of a ZoneGoal Deletion Event. + -- @param #EVENT self + -- @param Core.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal created. + function EVENT:CreateEventDeleteZoneGoal( ZoneGoal ) + self:F( { ZoneGoal } ) + + local Event = { + id = EVENTS.DeleteZoneGoal, + time = timer.getTime(), + ZoneGoal = ZoneGoal, + } + + world.onEvent( Event ) + end + + --- Creation of a S_EVENT_PLAYER_ENTER_UNIT Event. -- @param #EVENT self -- @param Wrapper.Unit#UNIT PlayerUnit. diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 8834dcb0b..4084e6deb 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -5497,4 +5497,324 @@ do -- SET_ZONE return nil end -end \ No newline at end of file +end + +do -- SET_ZONE_GOAL + + --- @type SET_ZONE_GOAL + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the @{Core.Set#SET_ZONE_GOAL} class to build sets of zones of various types. + -- + -- ## SET_ZONE_GOAL constructor + -- + -- Create a new SET_ZONE_GOAL object with the @{#SET_ZONE_GOAL.New} method: + -- + -- * @{#SET_ZONE_GOAL.New}: Creates a new SET_ZONE_GOAL object. + -- + -- ## Add or Remove ZONEs from SET_ZONE_GOAL + -- + -- ZONEs can be added and removed using the @{Core.Set#SET_ZONE_GOAL.AddZonesByName} and @{Core.Set#SET_ZONE_GOAL.RemoveZonesByName} respectively. + -- These methods take a single ZONE name or an array of ZONE names to be added or removed from SET_ZONE_GOAL. + -- + -- ## SET_ZONE_GOAL filter criteria + -- + -- You can set filter criteria to build the collection of zones in SET_ZONE_GOAL. + -- Filter criteria are defined by: + -- + -- * @{#SET_ZONE_GOAL.FilterPrefixes}: Builds the SET_ZONE_GOAL with the zones having a certain text pattern of prefix. + -- + -- Once the filter criteria have been set for the SET_ZONE_GOAL, you can start filtering using: + -- + -- * @{#SET_ZONE_GOAL.FilterStart}: Starts the filtering of the zones within the SET_ZONE_GOAL. + -- + -- ## SET_ZONE_GOAL iterators + -- + -- Once the filters have been defined and the SET_ZONE_GOAL has been built, you can iterate the SET_ZONE_GOAL with the available iterator methods. + -- The iterator methods will walk the SET_ZONE_GOAL 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_GOAL: + -- + -- * @{#SET_ZONE_GOAL.ForEachZone}: Calls a function for each zone it finds within the SET_ZONE_GOAL. + -- + -- === + -- @field #SET_ZONE_GOAL SET_ZONE_GOAL + SET_ZONE_GOAL = { + ClassName = "SET_ZONE_GOAL", + Zones = {}, + Filter = { + Prefixes = nil, + }, + FilterMeta = { + }, + } + + + --- Creates a new SET_ZONE_GOAL object, building a set of zones. + -- @param #SET_ZONE_GOAL self + -- @return #SET_ZONE_GOAL self + -- @usage + -- -- Define a new SET_ZONE_GOAL Object. The DatabaseSet will contain a reference to all Zones. + -- DatabaseSet = SET_ZONE_GOAL:New() + function SET_ZONE_GOAL:New() + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.ZONES_GOAL ) ) + + return self + end + + --- Add ZONEs to SET_ZONE_GOAL. + -- @param Core.Set#SET_ZONE_GOAL self + -- @param Core.Zone#ZONE_BASE Zone A ZONE_BASE object. + -- @return self + function SET_ZONE_GOAL:AddZone( Zone ) + + self:Add( Zone:GetName(), Zone ) + + return self + end + + + --- Remove ZONEs from SET_ZONE_GOAL. + -- @param Core.Set#SET_ZONE_GOAL self + -- @param Core.Zone#ZONE_BASE RemoveZoneNames A single name or an array of ZONE_BASE names. + -- @return self + function SET_ZONE_GOAL: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_GOAL self + -- @param #string ZoneName + -- @return Core.Zone#ZONE_BASE The found Zone. + function SET_ZONE_GOAL:FindZone( ZoneName ) + + local ZoneFound = self.Set[ZoneName] + return ZoneFound + end + + + --- Get a random zone from the set. + -- @param #SET_ZONE_GOAL self + -- @return Core.Zone#ZONE_BASE The random Zone. + -- @return #nil if no zone in the collection. + function SET_ZONE_GOAL: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! + while not ZoneFound do + local ZoneRandom = math.random( 1, #Index ) + ZoneFound = self.Set[Index[ZoneRandom]]:GetZoneMaybe() + end + + return ZoneFound + end + + return nil + end + + + --- Set a zone probability. + -- @param #SET_ZONE_GOAL self + -- @param #string ZoneName The name of the zone. + function SET_ZONE_GOAL:SetZoneProbability( ZoneName, ZoneProbability ) + 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_GOAL self + -- @param #string Prefixes The prefix of which the zone name starts with. + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:FilterPrefixes( Prefixes ) + if not self.Filter.Prefixes then + self.Filter.Prefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.Prefixes[Prefix] = Prefix + end + return self + end + + + --- Starts the filtering. + -- @param #SET_ZONE_GOAL self + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:FilterStart() + + if _DATABASE then + + -- We initialize the first set. + for ObjectName, Object in pairs( self.Database ) do + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + else + self:RemoveZonesByName( ObjectName ) + end + end + end + + self:HandleEvent( EVENTS.NewZoneGoal ) + self:HandleEvent( EVENTS.DeleteZoneGoal ) + + return self + end + + --- Stops the filtering for the defined collection. + -- @param #SET_ZONE_GOAL self + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:FilterStop() + + self:UnHandleEvent( EVENTS.NewZoneGoal ) + self:UnHandleEvent( EVENTS.DeleteZoneGoal ) + + 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_GOAL self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the AIRBASE + -- @return #table The AIRBASE + function SET_ZONE_GOAL: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_GOAL self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the AIRBASE + -- @return #table The AIRBASE + function SET_ZONE_GOAL:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Iterate the SET_ZONE_GOAL and call an interator function for each ZONE, providing the ZONE and optional parameters. + -- @param #SET_ZONE_GOAL self + -- @param #function IteratorFunction The function that will be called when there is an alive ZONE in the SET_ZONE_GOAL. The function needs to accept a AIRBASE parameter. + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:ForEachZone( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + + --- + -- @param #SET_ZONE_GOAL self + -- @param Core.Zone#ZONE_BASE MZone + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL: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 + self:T3( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } ) + if string.find( MZoneName, ZonePrefix, 1 ) then + MZonePrefix = true + end + end + self:T( { "Evaluated Prefix", MZonePrefix } ) + MZoneInclude = MZoneInclude and MZonePrefix + end + end + + self:T2( MZoneInclude ) + return MZoneInclude + end + + --- Handles the OnEventNewZone event for the Set. + -- @param #SET_ZONE_GOAL self + -- @param Core.Event#EVENTDATA EventData + function SET_ZONE_GOAL:OnEventNewZoneGoal( EventData ) + + self:I( { "New Zone Capture Coalition", EventData } ) + self:I( { "Zone Capture Coalition", EventData.ZoneGoal } ) + + if EventData.ZoneGoal then + if EventData.ZoneGoal and self:IsIncludeObject( EventData.ZoneGoal ) then + self:I( { "Adding Zone Capture Coalition", EventData.ZoneGoal.ZoneName, EventData.ZoneGoal } ) + self:Add( EventData.ZoneGoal.ZoneName , EventData.ZoneGoal ) + end + end + end + + --- Handles the OnDead or OnCrash event for alive units set. + -- @param #SET_ZONE_GOAL self + -- @param Core.Event#EVENTDATA EventData + function SET_ZONE_GOAL:OnEventDeleteZoneGoal( EventData ) --R2.1 + self:F3( { EventData } ) + + if EventData.ZoneGoal then + local Zone = _DATABASE:FindZone( EventData.ZoneGoal.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_ZONE_GOALs. + -- To prevent this from happening, the Zone object has a flag NoDestroy. + -- When true, the SET_ZONE_GOAL won't Remove the Zone object from the set. + -- This flag is switched off after the event handlers have been called in the EVENT class. + self:F( { ZoneNoDestroy=Zone.NoDestroy } ) + if Zone.NoDestroy then + else + self:Remove( Zone.ZoneName ) + end + 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. + -- @param #SET_ZONE_GOAL self + -- @param Core.Point#COORDINATE Coordinate The coordinate to be searched. + -- @return Core.Zone#ZONE_BASE The zone that validates the coordinate location. + -- @return #nil No zone has been found. + function SET_ZONE_GOAL: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 diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 52c27fd05..37608d7f9 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -120,7 +120,7 @@ ZONE_BASE = { -- @param #string ZoneName Name of the zone. -- @return #ZONE_BASE self function ZONE_BASE:New( ZoneName ) - local self = BASE:Inherit( self, BASE:New() ) + local self = BASE:Inherit( self, FSM:New() ) self:F( ZoneName ) self.ZoneName = ZoneName diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 32a860aa8..ce32338cc 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -549,6 +549,9 @@ do -- ZONE_CAPTURE_COALITION -- If it is, then we must move the zone to attack state. self:HandleEvent( EVENTS.Hit, self.OnEventHit ) + -- ZoneGoal objects are added to the _DATABASE.ZONES_GOAL and SET_ZONE_GOAL sets. + _EVENTDISPATCHER:CreateEventNewZoneGoal( self ) + return self end @@ -566,7 +569,7 @@ do -- ZONE_CAPTURE_COALITION function ZONE_CAPTURE_COALITION:IsGuarded() - local IsGuarded = self.Zone:IsAllInZoneOfCoalition( self.Coalition ) + local IsGuarded = self:IsAllInZoneOfCoalition( self.Coalition ) self:F( { IsGuarded = IsGuarded } ) return IsGuarded end @@ -574,7 +577,7 @@ do -- ZONE_CAPTURE_COALITION function ZONE_CAPTURE_COALITION:IsEmpty() - local IsEmpty = self.Zone:IsNoneInZone() + local IsEmpty = self:IsNoneInZone() self:F( { IsEmpty = IsEmpty } ) return IsEmpty end @@ -582,7 +585,7 @@ do -- ZONE_CAPTURE_COALITION function ZONE_CAPTURE_COALITION:IsCaptured() - local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition ) + local IsCaptured = self:IsAllInZoneOfOtherCoalition( self.Coalition ) self:F( { IsCaptured = IsCaptured } ) return IsCaptured end @@ -590,7 +593,7 @@ do -- ZONE_CAPTURE_COALITION function ZONE_CAPTURE_COALITION:IsAttacked() - local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) + local IsAttacked = self:IsSomeInZoneOfCoalition( self.Coalition ) self:F( { IsAttacked = IsAttacked } ) return IsAttacked end @@ -601,7 +604,7 @@ do -- ZONE_CAPTURE_COALITION -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:Mark() - local Coord = self.Zone:GetCoordinate() + local Coord = self:GetCoordinate() local ZoneName = self:GetZoneName() local State = self:GetState() @@ -640,7 +643,7 @@ do -- ZONE_CAPTURE_COALITION --self:GetParent( self ):onenterCaptured() - local NewCoalition = self.Zone:GetScannedCoalition() + local NewCoalition = self:GetScannedCoalition() self:F( { NewCoalition = NewCoalition } ) self:SetCoalition( NewCoalition ) @@ -680,7 +683,7 @@ do -- ZONE_CAPTURE_COALITION function ZONE_CAPTURE_COALITION:IsCaptured() - local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition ) + local IsCaptured = self:IsAllInZoneOfOtherCoalition( self.Coalition ) self:F( { IsCaptured = IsCaptured } ) return IsCaptured end @@ -688,7 +691,7 @@ do -- ZONE_CAPTURE_COALITION function ZONE_CAPTURE_COALITION:IsAttacked() - local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) + local IsAttacked = self:IsSomeInZoneOfCoalition( self.Coalition ) self:F( { IsAttacked = IsAttacked } ) return IsAttacked end @@ -803,7 +806,7 @@ do -- ZONE_CAPTURE_COALITION local UnitHit = EventData.TgtUnit if UnitHit then - if UnitHit:IsInZone( self.Zone ) then + if UnitHit:IsInZone( self ) then self:Attack() end end diff --git a/Moose Development/Moose/Functional/ZoneGoal.lua b/Moose Development/Moose/Functional/ZoneGoal.lua index 0caabc8a3..67f6a872b 100644 --- a/Moose Development/Moose/Functional/ZoneGoal.lua +++ b/Moose Development/Moose/Functional/ZoneGoal.lua @@ -44,14 +44,13 @@ do -- Zone --- ZONE_GOAL Constructor. -- @param #ZONE_GOAL self - -- @param Core.Zone#ZONE_BASE Zone A @{Zone} object with the goal to be achieved. + -- @param Core.Zone#ZONE_RADIUS Zone A @{Zone} object with the goal to be achieved. -- @return #ZONE_GOAL function ZONE_GOAL:New( Zone ) - local self = BASE:Inherit( self, FSM:New() ) -- #ZONE_GOAL + local self = BASE:Inherit( self, ZONE_RADIUS:New( Zone:GetName(), Zone:GetVec2(), Zone:GetRadius() ) ) -- #ZONE_GOAL self:F( { Zone = Zone } ) - self.Zone = Zone -- Core.Zone#ZONE_BASE self.Goal = GOAL:New() self.SmokeTime = nil @@ -67,6 +66,7 @@ do -- Zone -- @param Wrapper.Unit#UNIT DestroyedUnit The destroyed unit. -- @param #string PlayerName The name of the player. + return self end @@ -74,7 +74,7 @@ do -- Zone -- @param #ZONE_GOAL self -- @return Core.Zone#ZONE_BASE function ZONE_GOAL:GetZone() - return self.Zone + return self end @@ -82,7 +82,7 @@ do -- Zone -- @param #ZONE_GOAL self -- @return #string function ZONE_GOAL:GetZoneName() - return self.Zone:GetName() + return self:GetName() end @@ -101,7 +101,7 @@ do -- Zone -- @param #ZONE_GOAL self -- @param #SMOKECOLOR.Color FlareColor function ZONE_GOAL:Flare( FlareColor ) - self.Zone:FlareZone( FlareColor, math.random( 1, 360 ) ) + self:FlareZone( FlareColor, math.random( 1, 360 ) ) end @@ -130,7 +130,7 @@ do -- Zone if self.SmokeTime == nil or self.SmokeTime + 300 <= CurrentTime then if self.SmokeColor then - self.Zone:GetCoordinate():Smoke( self.SmokeColor ) + self:GetCoordinate():Smoke( self.SmokeColor ) --self.SmokeColor = nil self.SmokeTime = CurrentTime end @@ -147,11 +147,9 @@ do -- Zone local Vec3 = EventData.IniDCSUnit:getPosition().p self:F( { Vec3 = Vec3 } ) - local ZoneGoal = self:GetZone() - self:F({ZoneGoal}) if EventData.IniDCSUnit then - if ZoneGoal:IsVec3InZone(Vec3) then + if self:IsVec3InZone(Vec3) then local PlayerHits = _DATABASE.HITS[EventData.IniUnitName] if PlayerHits then for PlayerName, PlayerHit in pairs( PlayerHits.Players or {} ) do diff --git a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua index ba86ffe95..1de5218f3 100644 --- a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua @@ -104,7 +104,7 @@ do -- ZoneGoal local State = self:GetState() self:F( { State = self:GetState() } ) - self.Zone:Scan( { Object.Category.UNIT, Object.Category.STATIC } ) + self:Scan( { Object.Category.UNIT, Object.Category.STATIC } ) end diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index dcef1bb72..2ec90ab27 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -180,7 +180,7 @@ COMMANDCENTER = { COMMANDCENTER.AutoAssignMethods = { ["Random"] = 1, ["Distance"] = 2, - ["Priority"] = 3 + ["Priority"] = 3, } --- The constructor takes an IDENTIFIABLE as the HQ command center. diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index 68c8a4803..1a4f8b876 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -1111,6 +1111,11 @@ function TASK:SetAssignedMenuForGroup( TaskGroup, MenuTime ) end local MarkMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Mark Task Location on Map" ), TaskControl, self.MenuMarkToGroup, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) local TaskTypeMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Report Task Details" ), TaskControl, self.MenuTaskStatus, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) + if not self.FlashTaskStatus then + local TaskFlashStatusMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Flash Task Details" ), TaskControl, self.MenuFlashTaskStatus, self, TaskGroup, true ):SetTime( MenuTime ):SetTag( "Tasking" ) + else + local TaskFlashStatusMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Stop Flash Task Details" ), TaskControl, self.MenuFlashTaskStatus, self, TaskGroup, nil ):SetTime( MenuTime ):SetTag( "Tasking" ) + end end end @@ -1234,6 +1239,24 @@ function TASK:MenuTaskStatus( TaskGroup ) end +--- Report the task status. +-- @param #TASK self +function TASK:MenuFlashTaskStatus( TaskGroup, Flash ) + + self.FlashTaskStatus = Flash + + if self.FlashTaskStatus then + self.FlashTaskScheduler, self.FlashTaskScheduleID = SCHEDULER:New( self, self.MenuTaskStatus, { TaskGroup }, 0, 60 ) + else + if self.FlashTaskScheduler then + self.FlashTaskScheduler:Stop( self.FlashTaskScheduleID ) + self.FlashTaskScheduler = nil + self.FlashTaskScheduleID = nil + end + end + +end + --- Report the task status. -- @param #TASK self function TASK:MenuTaskAbort( TaskGroup ) diff --git a/Moose Development/Moose/Tasking/TaskInfo.lua b/Moose Development/Moose/Tasking/TaskInfo.lua index 54c59410d..5ece5f005 100644 --- a/Moose Development/Moose/Tasking/TaskInfo.lua +++ b/Moose Development/Moose/Tasking/TaskInfo.lua @@ -58,10 +58,10 @@ end -- @param #TASKINFO.Detail Detail The detail Level. -- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. -- @return #TASKINFO self -function TASKINFO:AddInfo( Key, Data, Order, Detail, Keep ) - self.VolatileInfo:Add( Key, { Data = Data, Order = Order, Detail = Detail } ) +function TASKINFO:AddInfo( Key, Data, Order, Detail, Keep, ShowKey, Type ) + self.VolatileInfo:Add( Key, { Data = Data, Order = Order, Detail = Detail, ShowKey = ShowKey, Type = Type } ) if Keep == true then - self.PersistentInfo:Add( Key, { Data = Data, Order = Order, Detail = Detail } ) + self.PersistentInfo:Add( Key, { Data = Data, Order = Order, Detail = Detail, ShowKey = ShowKey, Type = Type } ) end return self end @@ -124,8 +124,8 @@ end -- @param #TASKINFO.Detail Detail The detail Level. -- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. -- @return #TASKINFO self -function TASKINFO:AddCoordinate( Coordinate, Order, Detail, Keep ) - self:AddInfo( "Coordinate", Coordinate, Order, Detail, Keep ) +function TASKINFO:AddCoordinate( Coordinate, Order, Detail, Keep, ShowKey, Name ) + self:AddInfo( Name or "Coordinate", Coordinate, Order, Detail, Keep, ShowKey, "Coordinate" ) return self end @@ -133,8 +133,8 @@ end --- Get the Coordinate. -- @param #TASKINFO self -- @return Core.Point#COORDINATE Coordinate -function TASKINFO:GetCoordinate() - return self:GetData( "Coordinate" ) +function TASKINFO:GetCoordinate( Name ) + return self:GetData( Name or "Coordinate" ) end @@ -308,10 +308,11 @@ function TASKINFO:Report( Report, Detail, ReportGroup, Task ) if Data.Detail:find( Detail ) then local Text = "" + local ShowKey = ( Data.ShowKey == nil or Data.ShowKey == true ) if Key == "TaskName" then Key = nil Text = Data.Data - elseif Key == "Coordinate" then + elseif Data.Type and Data.Type == "Coordinate" then local Coordinate = Data.Data -- Core.Point#COORDINATE Text = Coordinate:ToString( ReportGroup:GetUnit(1), nil, Task ) elseif Key == "Threat" then @@ -357,7 +358,7 @@ function TASKINFO:Report( Report, Detail, ReportGroup, Task ) end if Text ~= "" then - LineReport:Add( ( Key and ( Key .. ":" ) or "" ) .. Text ) + LineReport:Add( ( ( Key and ShowKey == true ) and ( Key .. ": " ) or "" ) .. Text ) end end diff --git a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua index c7c3227da..7f1fa7d08 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua @@ -214,6 +214,26 @@ do -- TASK_CAPTURE_DISPATCHER end + --- Link an AI A2G dispatcher from the other coalition to understand its plan for defenses. + -- This is used for the tactical overview, so the players also know the zones attacked by the other AI A2G dispatcher! + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER DefenseAIA2GDispatcher + function TASK_CAPTURE_DISPATCHER:SetDefenseAIA2GDispatcher( DefenseAIA2GDispatcher ) + + self.DefenseAIA2GDispatcher = DefenseAIA2GDispatcher + end + + + --- Get the linked AI A2G dispatcher from the other coalition to understand its plan for defenses. + -- This is used for the tactical overview, so the players also know the zones attacked by the AI A2G dispatcher! + -- @param #TASK_CAPTURE_DISPATCHER self + -- @return AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER + function TASK_CAPTURE_DISPATCHER:GetDefenseAIA2GDispatcher() + + return self.DefenseAIA2GDispatcher + end + + --- Add a capture zone task. -- @param #TASK_CAPTURE_DISPATCHER self -- @param #string TaskPrefix (optional) The prefix of the capture zone task. @@ -303,6 +323,7 @@ do -- TASK_CAPTURE_DISPATCHER self.AI_A2G_Dispatcher:Unlock( Task.TaskZoneName ) -- This will unlock the zone to be defended by AI. end CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = true end function CaptureZone.Task.OnEnterSuccess( Task, From, Event, To ) @@ -311,6 +332,7 @@ do -- TASK_CAPTURE_DISPATCHER self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. end CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false end function CaptureZone.Task.OnEnterCancelled( Task, From, Event, To ) @@ -319,6 +341,7 @@ do -- TASK_CAPTURE_DISPATCHER self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. end CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false end function CaptureZone.Task.OnEnterFailed( Task, From, Event, To ) @@ -327,6 +350,7 @@ do -- TASK_CAPTURE_DISPATCHER self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. end CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false end function CaptureZone.Task.OnEnterAborted( Task, From, Event, To ) @@ -335,6 +359,7 @@ do -- TASK_CAPTURE_DISPATCHER self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. end CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false end -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher. @@ -344,6 +369,7 @@ do -- TASK_CAPTURE_DISPATCHER self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. end CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false end end diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index 73032a60d..59e7d988f 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -230,9 +230,11 @@ do -- TASK_CAPTURE_ZONE self.TaskInfo:AddCoordinate( ZoneCoordinate, 1, "SOD", Persist ) -- self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD", Persist ) -- self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD", Persist ) - local SetUnit = self.ZoneGoal.Zone:GetScannedSetUnit() + local SetUnit = self.ZoneGoal:GetScannedSetUnit() local ThreatLevel, ThreatText = SetUnit:CalculateThreatLevelA2G() + local ThreatCount = SetUnit:Count() self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 20, "MOD", Persist ) + self.TaskInfo:AddInfo( "Remaining Units", ThreatCount, 21, "MOD", Persist, true) if self.Dispatcher then local DefenseTaskCaptureDispatcher = self.Dispatcher:GetDefenseTaskCaptureDispatcher() -- Tasking.Task_Capture_Dispatcher#TASK_CAPTURE_DISPATCHER @@ -242,8 +244,22 @@ do -- TASK_CAPTURE_ZONE for TaskName, CaptureZone in pairs( DefenseTaskCaptureDispatcher.Zones or {} ) do local Task = CaptureZone.Task -- Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE if Task then - if Task:IsStateAssigned() then - self.TaskInfo:AddInfo( "Defense", Task.ZoneGoal:GetName() .. ", " .. Task.ZoneGoal:GetZone():GetCoordinate(), 30, "MOD", Persist ) + self.TaskInfo:AddInfo( "Defense Player Zone", Task.ZoneGoal:GetName(), 30, "MOD", Persist ) + self.TaskInfo:AddCoordinate( Task.ZoneGoal:GetZone():GetCoordinate(), 31, "MOD", Persist, false, "Defense Player Coordinate" ) + end + end + end + local DefenseAIA2GDispatcher = self.Dispatcher:GetDefenseAIA2GDispatcher() -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER + + if DefenseAIA2GDispatcher then + -- Loop through all zones of the Defenses, and check which zone has an assigned task! + for Defender, Task in pairs( DefenseAIA2GDispatcher:GetDefenderTasks() or {} ) do + local DetectedItem = DefenseAIA2GDispatcher:GetDefenderTaskTarget( Defender ) + if DetectedItem then + local DetectedZone = DefenseAIA2GDispatcher.Detection:GetDetectedItemZone( DetectedItem ) + if DetectedZone then + self.TaskInfo:AddInfo( "Defense AI Zone", DetectedZone:GetName(), 40, "MOD", Persist ) + self.TaskInfo:AddCoordinate( DetectedZone:GetCoordinate(), 41, "MOD", Persist, false, "Defense AI Coordinate" ) end end end @@ -291,7 +307,7 @@ do -- TASK_CAPTURE_ZONE -- @param #number AutoAssignMethod The method to be applied to the task. -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. -- @param Wrapper.Group#GROUP TaskGroup The player group. - function TASK_CAPTURE_ZONE:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup ) + function TASK_CAPTURE_ZONE:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup, AutoAssignReference ) if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then return math.random( 1, 9 ) From 96a904c077d295cb646ae400f25e6dd546a2d197 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 24 Aug 2019 08:56:50 +0200 Subject: [PATCH 355/485] Implemented an important fix. Refuel airplanes also when they are engaging targets and the fuel treshold has been reached. --- Moose Development/Moose/AI/AI_A2A_Cap.lua | 2 ++ Moose Development/Moose/AI/AI_A2A_Gci.lua | 2 ++ Moose Development/Moose/AI/AI_A2G_BAI.lua | 15 ++++++++++++++- Moose Development/Moose/AI/AI_A2G_CAS.lua | 12 ++++++++++++ Moose Development/Moose/AI/AI_A2G_Engage.lua | 18 ++++++------------ Moose Development/Moose/AI/AI_A2G_Patrol.lua | 4 ++-- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 12 ++++++++++++ Moose Development/Moose/AI/AI_Air.lua | 6 ++++-- 8 files changed, 54 insertions(+), 17 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Cap.lua b/Moose Development/Moose/AI/AI_A2A_Cap.lua index 49b87b3c0..ba41a3bab 100644 --- a/Moose Development/Moose/AI/AI_A2A_Cap.lua +++ b/Moose Development/Moose/AI/AI_A2A_Cap.lua @@ -278,6 +278,8 @@ function AI_A2A_CAP:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAl -- @param #AI_A2A_CAP self -- @param #number Delay The delay in seconds. + self:AddTransition( { "Patrolling", "Engaging" }, "Refuel", "Refuelling" ) + return self end diff --git a/Moose Development/Moose/AI/AI_A2A_Gci.lua b/Moose Development/Moose/AI/AI_A2A_Gci.lua index 53b7141ab..a6642a9ce 100644 --- a/Moose Development/Moose/AI/AI_A2A_Gci.lua +++ b/Moose Development/Moose/AI/AI_A2A_Gci.lua @@ -279,6 +279,8 @@ function AI_A2A_GCI:New( AIIntercept, EngageMinSpeed, EngageMaxSpeed ) -- @param #AI_A2A_GCI self -- @param #number Delay The delay in seconds. + self:AddTransition( { "Patrolling", "Engaging" }, "Refuel", "Refuelling" ) + return self end diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index d572347ba..140199efe 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -62,12 +62,14 @@ end -- @param #string To The To State string. function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) - self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + self:I( { DefenderGroup, From, Event, To, AttackSetUnit} ) local DefenderGroupName = DefenderGroup:GetName() local AttackCount = AttackSetUnit:Count() + self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air! + if AttackCount > 0 then if DefenderGroup:IsAlive() then @@ -151,3 +153,14 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit self:__RTB( self.TaskDelay ) end end + +--- @param Wrapper.Group#GROUP AIEngage +function AI_A2G_BAI.Resume( AIEngage, Fsm ) + + AIEngage:F( { "AI_A2G_BAI.Resume:", AIEngage:GetName() } ) + if AIEngage:IsAlive() then + Fsm:__Reset( Fsm.TaskDelay ) + Fsm:__EngageRoute( Fsm.TaskDelay, Fsm.AttackSetUnit ) + end + +end \ No newline at end of file diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index c8773b3f5..7caecae02 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -66,6 +66,8 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit local DefenderGroupName = DefenderGroup:GetName() + self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air! + local AttackCount = AttackSetUnit:Count() if AttackCount > 0 then @@ -148,3 +150,13 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit end end +--- @param Wrapper.Group#GROUP AIEngage +function AI_A2G_CAS.Resume( AIEngage, Fsm ) + + AIEngage:F( { "AI_A2G_CAS.Resume:", AIEngage:GetName() } ) + if AIEngage:IsAlive() then + Fsm:__Reset( Fsm.TaskDelay ) + Fsm:__EngageRoute( Fsm.TaskDelay, Fsm.AttackSetUnit ) + end + +end \ No newline at end of file diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua index 71a2eae6f..53ac7d21b 100644 --- a/Moose Development/Moose/AI/AI_A2G_Engage.lua +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -304,6 +304,8 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @param #AI_A2G_ENGAGE self -- @param #number Delay The delay in seconds. + self:AddTransition( { "Patrolling", "Engaging" }, "Refuel", "Refuelling" ) + return self end @@ -389,17 +391,6 @@ function AI_A2G_ENGAGE:onafterAbort( AIGroup, From, Event, To ) end ---- @param #AI_A2G_ENGAGE 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_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) - - self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) - -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. @@ -446,6 +437,8 @@ function AI_A2G_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, Attac local DefenderGroupName = DefenderGroup:GetName() + self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air! + local AttackCount = AttackSetUnit:Count() if AttackCount > 0 then @@ -517,4 +510,5 @@ function AI_A2G_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, Attac self:Return() self:__RTB( self.TaskDelay ) end -end \ No newline at end of file +end + diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua index 86b5d49dc..8c7ae67a9 100644 --- a/Moose Development/Moose/AI/AI_A2G_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -316,8 +316,8 @@ function AI_A2G_PATROL.Resume( AIPatrol, Fsm ) AIPatrol:F( { "AI_A2G_PATROL.Resume:", AIPatrol:GetName() } ) if AIPatrol:IsAlive() then - Fsm:__Reset( self.TaskDelay ) - Fsm:__PatrolRoute( self.TaskDelay ) + Fsm:__Reset( Fsm.TaskDelay ) + Fsm:__PatrolRoute( Fsm.TaskDelay ) end end diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index ae2f2dad1..8923e7917 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -117,6 +117,8 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni local DefenderGroupName = DefenderGroup:GetName() + self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air! + local AttackCount = AttackSetUnit:Count() if AttackCount > 0 then @@ -206,3 +208,13 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni end end +--- @param Wrapper.Group#GROUP AIEngage +function AI_A2G_SEAD.Resume( AIEngage, Fsm ) + + AIEngage:F( { "AI_A2G_SEAD.Resume:", AIEngage:GetName() } ) + if AIEngage:IsAlive() then + Fsm:__Reset( Fsm.TaskDelay ) + Fsm:__EngageRoute( Fsm.TaskDelay, Fsm.AttackSetUnit ) + end + +end \ No newline at end of file diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 0bee1f1ab..6259e105c 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -461,7 +461,7 @@ function AI_AIR:onafterStatus() -- end - if not self:Is( "Fuel" ) and not self:Is( "Home" ) then + if not self:Is( "Fuel" ) and not self:Is( "Home" ) and not self:is( "Refuelling" )then local Fuel = self.Controllable:GetFuelMin() @@ -680,12 +680,14 @@ end 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 + self:E( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. "), at tanker " .. self.TankerName ) + local RefuelRoute = {} --- Calculate the target route point. From c716ba16b3156ceed20608edb9bd6130a58cfee2 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 24 Aug 2019 22:28:44 +0200 Subject: [PATCH 356/485] - Bugfix when enemy plane has crashed, AttackerDetection is not existing anymore. --- Moose Development/Moose/AI/AI_A2G_Dispatcher.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index b9552e00b..c8effaeee 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -4292,7 +4292,7 @@ do -- AI_A2G_DISPATCHER for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem - if DefenseQueueItem.AttackerDetection.Index == DetectedItem.Index then + if DefenseQueueItem.AttackerDetection and DefenseQueueItem.AttackerDetection.Index and DefenseQueueItem.AttackerDetection.Index == DetectedItem.Index then DefenseTotal = DefenseTotal + 1 end end From 9e0f2c22b6e503ad6a3bcf93cb2e69a0745f8ea6 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 25 Aug 2019 22:06:51 +0200 Subject: [PATCH 357/485] DATABASE fix - Fix bug for RED templates being registered as NEUTRAL --- Moose Development/Moose/Core/Database.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 3b85d0d6b..3188675d2 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -1246,7 +1246,7 @@ function DATABASE:_RegisterTemplates() local CoalitionSide = coalition.side[string.upper(CoalitionName)] if CoalitionName=="red" then - CoalitionSide=coalition.side.NEUTRAL + CoalitionSide=coalition.side.RED elseif CoalitionName=="blue" then CoalitionSide=coalition.side.BLUE else From c1b240857f5c58dc25b0651cfeeb1e4a761cc70b Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 26 Aug 2019 08:25:46 +0200 Subject: [PATCH 358/485] - Remove unnecessary trace lines - Implement Scheduled Trace - Now source and line number are shown for scheduled calls. - Info lines are now shown where appropriate. - The width of trace for the class name is now 25 characters instead of 20. --- Moose Development/Moose/AI/AI_A2A.lua | 22 ++++++------ Moose Development/Moose/AI/AI_A2G_BAI.lua | 4 +-- Moose Development/Moose/AI/AI_A2G_CAS.lua | 4 +-- Moose Development/Moose/AI/AI_A2G_Engage.lua | 2 +- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 4 +-- Moose Development/Moose/AI/AI_Air.lua | 20 +++++------ Moose Development/Moose/AI/AI_Escort.lua | 2 +- .../Moose/AI/AI_Escort_Dispatcher_Request.lua | 12 ------- Moose Development/Moose/AI/AI_Patrol.lua | 5 ++- Moose Development/Moose/Core/Base.lua | 28 ++++++++------- Moose Development/Moose/Core/Database.lua | 3 +- Moose Development/Moose/Core/Fsm.lua | 4 +-- Moose Development/Moose/Core/Point.lua | 2 +- .../Moose/Core/ScheduleDispatcher.lua | 34 ++++++++++++++----- Moose Development/Moose/Core/Scheduler.lua | 7 ++-- Moose Development/Moose/Core/Set.lua | 1 - Moose Development/Moose/Core/Zone.lua | 2 +- .../Moose/Functional/Detection.lua | 4 +-- .../Moose/Tasking/CommandCenter.lua | 2 +- .../Moose/Tasking/DetectionManager.lua | 1 - .../Moose/Tasking/Task_Manager.lua | 1 - .../Moose/Wrapper/Controllable.lua | 5 ++- 22 files changed, 85 insertions(+), 84 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A.lua b/Moose Development/Moose/AI/AI_A2A.lua index afd7cc70d..f23fc006e 100644 --- a/Moose Development/Moose/AI/AI_A2A.lua +++ b/Moose Development/Moose/AI/AI_A2A.lua @@ -433,7 +433,7 @@ function AI_A2A:onafterStatus() self:F({DistanceFromHomeBase=DistanceFromHomeBase}) if DistanceFromHomeBase > self.DisengageRadius then - self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) + self:I( self.Controllable:GetName() .. " is too far from home base, RTB!" ) self:Hold( 300 ) RTB = false end @@ -453,10 +453,10 @@ function AI_A2A:onafterStatus() self:F({Fuel=Fuel, PatrolFuelThresholdPercentage=self.PatrolFuelThresholdPercentage}) if Fuel < self.PatrolFuelThresholdPercentage then if self.TankerName then - self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) + self:I( 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!" ) + self:I( 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 ) @@ -475,7 +475,7 @@ function AI_A2A:onafterStatus() 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:I( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) self:Damaged() RTB = true self:SetStatusOff() @@ -493,7 +493,7 @@ function AI_A2A:onafterStatus() if Damage ~= InitialLife then self:Damaged() else - self:E( self.Controllable:GetName() .. " control lost! " ) + self:I( self.Controllable:GetName() .. " control lost! " ) self:LostControl() end else @@ -549,7 +549,7 @@ function AI_A2A:onafterRTB( AIGroup, From, Event, To ) if AIGroup and AIGroup:IsAlive() then - self:E( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) + self:I( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) self:ClearTargetDistance() AIGroup:ClearTasks() @@ -567,7 +567,7 @@ function AI_A2A:onafterRTB( AIGroup, From, Event, To ) local ToAirbaseCoord = CurrentCoord:Translate( 5000, ToAirbaseAngle ) if Distance < 5000 then - self:E( "RTB and near the airbase!" ) + self:I( "RTB and near the airbase!" ) self:Home() return end @@ -608,7 +608,7 @@ end function AI_A2A:onafterHome( AIGroup, From, Event, To ) self:F( { AIGroup, From, Event, To } ) - self:E( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) + self:I( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) if AIGroup and AIGroup:IsAlive() then end @@ -622,7 +622,7 @@ end function AI_A2A:onafterHold( AIGroup, From, Event, To, HoldTime ) self:F( { AIGroup, From, Event, To } ) - self:E( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) + self:I( "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 ) @@ -654,7 +654,7 @@ end function AI_A2A:onafterRefuel( AIGroup, From, Event, To ) self:F( { AIGroup, From, Event, To } ) - self:E( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. " )" ) + self:I( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. " )" ) if AIGroup and AIGroup:IsAlive() then local Tanker = GROUP:FindByName( self.TankerName ) @@ -711,7 +711,7 @@ end function AI_A2A:OnCrash( EventData ) if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:E( self.Controllable:GetUnits() ) + self:I( self.Controllable:GetUnits() ) if #self.Controllable:GetUnits() == 1 then self:__Crash( 1, EventData ) end diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 140199efe..ac8efbb4f 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -131,7 +131,7 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit end if #AttackUnitTasks == 0 then - self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:I( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() self:__RTB( self.TaskDelay ) else @@ -148,7 +148,7 @@ function AI_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit DefenderGroup:Route( EngageRoute, self.TaskDelay ) end else - self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:I( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() self:__RTB( self.TaskDelay ) end diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 7caecae02..8ed9ee29d 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -126,7 +126,7 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit if #AttackUnitTasks == 0 then - self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:I( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() self:__RTB( self.TaskDelay ) else @@ -144,7 +144,7 @@ function AI_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit DefenderGroup:Route( EngageRoute, self.TaskDelay ) end else - self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:I( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() self:__RTB( self.TaskDelay ) end diff --git a/Moose Development/Moose/AI/AI_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua index 53ac7d21b..f74f82f80 100644 --- a/Moose Development/Moose/AI/AI_A2G_Engage.lua +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -506,7 +506,7 @@ function AI_A2G_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, Attac end else - self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:I( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() self:__RTB( self.TaskDelay ) end diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index 8923e7917..cd2883772 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -183,7 +183,7 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni end if #AttackUnitTasks == 0 then - self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:I( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() self:__RTB( self.TaskDelay ) else @@ -202,7 +202,7 @@ function AI_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUni DefenderGroup:Route( EngageRoute, self.TaskDelay ) end else - self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:I( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() self:__RTB( self.TaskDelay ) end diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 6259e105c..8c6dbb9e0 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -446,7 +446,7 @@ function AI_AIR:onafterStatus() local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) if DistanceFromHomeBase > self.DisengageRadius then - self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) + self:I( self.Controllable:GetName() .. " is too far from home base, RTB!" ) self:Hold( 300 ) RTB = false end @@ -470,10 +470,10 @@ function AI_AIR:onafterStatus() if Fuel < self.FuelThresholdPercentage then if self.TankerName then - self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) + self:I( 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!" ) + self:I( 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 ) @@ -495,7 +495,7 @@ function AI_AIR:onafterStatus() -- 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:I( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) self:Damaged() RTB = true self:SetStatusOff() @@ -513,7 +513,7 @@ function AI_AIR:onafterStatus() if Damage ~= InitialLife then self:Damaged() else - self:E( self.Controllable:GetName() .. " control lost! " ) + self:I( self.Controllable:GetName() .. " control lost! " ) self:LostControl() end @@ -570,7 +570,7 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) if AIGroup and AIGroup:IsAlive() then - self:E( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) + self:I( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) self:ClearTargetDistance() --AIGroup:ClearTasks() @@ -588,7 +588,7 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) local ToAirbaseCoord = FromCoord:Translate( 5000, ToAirbaseAngle ) if Distance < 5000 then - self:E( "RTB and near the airbase!" ) + self:I( "RTB and near the airbase!" ) self:Home() return end @@ -634,7 +634,7 @@ end function AI_AIR:onafterHome( AIGroup, From, Event, To ) self:F( { AIGroup, From, Event, To } ) - self:E( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) + self:I( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) if AIGroup and AIGroup:IsAlive() then end @@ -648,7 +648,7 @@ end function AI_AIR:onafterHold( AIGroup, From, Event, To, HoldTime ) self:F( { AIGroup, From, Event, To } ) - self:E( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) + self:I( "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 ) @@ -686,7 +686,7 @@ function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) if Tanker:IsAlive() and Tanker:IsAirPlane() then - self:E( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. "), at tanker " .. self.TankerName ) + self:I( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. "), at tanker " .. self.TankerName ) local RefuelRoute = {} diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index c7e9645f7..af3a4c304 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -545,7 +545,7 @@ function AI_ESCORT:SetFlightMenuFormation( Formation ) if MenuFormation then local Arguments = MenuFormation.Arguments - self:I({Arguments=unpack(Arguments)}) + --self:I({Arguments=unpack(Arguments)}) local FlightMenuFormation = MENU_GROUP:New( self.PlayerGroup, "Formation", self.MainMenu ) local MenuFlightFormationID = MENU_GROUP_COMMAND:New( self.PlayerGroup, Formation, FlightMenuFormation, function ( self, Formation, ... ) diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua index 0bf3d1a6b..8735f7064 100644 --- a/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua @@ -83,13 +83,6 @@ function AI_ESCORT_DISPATCHER_REQUEST:OnEventExit( EventData ) local PlayerGroup = EventData.IniGroup local PlayerUnit = EventData.IniUnit - self.CarrierSet:Flush(self) - self:I({EscortAirbase= self.EscortAirbase } ) - self:I({PlayerGroupName = PlayerGroupName } ) - self:I({PlayerGroup = PlayerGroup}) - self:I({FirstGroup = self.CarrierSet:GetFirst()}) - self:I({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) - if self.CarrierSet:FindGroup( PlayerGroupName ) then if self.AI_Escorts[PlayerGroupName] then self.AI_Escorts[PlayerGroupName]:Stop() @@ -107,11 +100,6 @@ function AI_ESCORT_DISPATCHER_REQUEST:OnEventBirth( EventData ) local PlayerGroup = EventData.IniGroup local PlayerUnit = EventData.IniUnit - self:I({PlayerGroupName = PlayerGroupName } ) - self:I({PlayerGroup = PlayerGroup}) - self:I({FirstGroup = self.CarrierSet:GetFirst()}) - self:I({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) - if self.CarrierSet:FindGroup( PlayerGroupName ) then if not self.AI_Escorts[PlayerGroupName] then local LeaderUnit = PlayerUnit diff --git a/Moose Development/Moose/AI/AI_Patrol.lua b/Moose Development/Moose/AI/AI_Patrol.lua index c4c0330bf..6e2b6ff67 100644 --- a/Moose Development/Moose/AI/AI_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Patrol.lua @@ -825,7 +825,7 @@ function AI_PATROL_ZONE:onafterStatus() local Fuel = self.Controllable:GetFuelMin() if Fuel < self.PatrolFuelThresholdPercentage then - self:E( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) + self:I( 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 ) @@ -839,7 +839,7 @@ function AI_PATROL_ZONE:onafterStatus() -- TODO: Check GROUP damage function. local Damage = self.Controllable:GetLife() if Damage <= self.PatrolDamageThreshold then - self:E( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) + self:I( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) RTB = true end @@ -900,7 +900,6 @@ end function AI_PATROL_ZONE: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 diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index db2f5ddaf..5b766d10f 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -805,15 +805,17 @@ do -- Scheduling end self.Scheduler.SchedulerObject = self.Scheduler + --self.MasterObject = self - local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( + local ScheduleID = self.Scheduler:Schedule( self, SchedulerFunction, { ... }, Start, Repeat, RandomizeFactor, - Stop + Stop, + 4 ) self._.Schedules[#self._.Schedules+1] = ScheduleID @@ -924,7 +926,7 @@ end -- @param #number Level function BASE:TraceLevel( Level ) _TraceLevel = Level - self:E( "Tracing level " .. Level ) + self:I( "Tracing level " .. Level ) end --- Trace all methods in MOOSE @@ -935,9 +937,9 @@ function BASE:TraceAll( TraceAll ) _TraceAll = TraceAll if _TraceAll then - self:E( "Tracing all methods in MOOSE " ) + self:I( "Tracing all methods in MOOSE " ) else - self:E( "Switched off tracing all methods in MOOSE" ) + self:I( "Switched off tracing all methods in MOOSE" ) end end @@ -947,7 +949,7 @@ end function BASE:TraceClass( Class ) _TraceClass[Class] = true _TraceClassMethod[Class] = {} - self:E( "Tracing class " .. Class ) + self:I( "Tracing class " .. Class ) end --- Set tracing for a specific method of class @@ -960,7 +962,7 @@ function BASE:TraceClassMethod( Class, Method ) _TraceClassMethod[Class].Method = {} end _TraceClassMethod[Class].Method[Method] = true - self:E( "Tracing method " .. Method .. " of class " .. Class ) + self:I( "Tracing method " .. Method .. " of class " .. Class ) end --- Trace a function call. This function is private. @@ -987,7 +989,7 @@ function BASE:_F( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) if DebugInfoFrom then LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%25s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) end end end @@ -1062,7 +1064,7 @@ function BASE:_T( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) if DebugInfoFrom then LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%25s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end end end @@ -1133,9 +1135,9 @@ function BASE:E( Arguments ) LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%25s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) else - env.info( string.format( "%1s:%20s%05d(%s)" , "E", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%1s:%25s%05d(%s)" , "E", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end end @@ -1161,9 +1163,9 @@ function BASE:I( Arguments ) LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "I", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%25s%05d.%s(%s)" , LineCurrent, LineFrom, "I", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) else - env.info( string.format( "%1s:%20s%05d(%s)" , "I", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%1s:%25s%05d(%s)" , "I", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end end diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 3188675d2..3a4dccefd 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -1051,7 +1051,8 @@ function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) return false end - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + --local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + Schedule() return self end diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index ba8e3c4de..22489bc7c 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -718,13 +718,13 @@ do -- FSM if DelaySeconds < 0 then -- Only call the event ONCE! DelaySeconds = math.abs( DelaySeconds ) if not self._EventSchedules[EventName] then - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) + CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4 ) self._EventSchedules[EventName] = CallID else -- reschedule end else - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) + CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4 ) end else error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index a3805d9b6..3dce6e9a9 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -2065,7 +2065,7 @@ do -- COORDINATE -- @return #string The coordinate Text in the configured coordinate system. function COORDINATE:ToString( Controllable, Settings, Task ) - self:E( { Controllable = Controllable and Controllable:GetName() } ) +-- self:E( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 8173c85c8..0a70ca72d 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -52,8 +52,8 @@ end -- Nothing of this code should be modified without testing it thoroughly. -- @param #SCHEDULEDISPATCHER self -- @param Core.Scheduler#SCHEDULER Scheduler -function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop ) - self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop } ) +function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel ) + self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel } ) self.CallID = self.CallID + 1 local CallID = self.CallID .. "#" .. ( Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID() or "" ) or "" @@ -84,11 +84,23 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr self.Schedule[Scheduler][CallID].Repeat = Repeat or 0 self.Schedule[Scheduler][CallID].Randomize = Randomize or 0 self.Schedule[Scheduler][CallID].Stop = Stop + + local Source = "" + local Line = "" + + if debug then + TraceLevel = TraceLevel or 2 + Source = debug.getinfo( TraceLevel, "S" ).source + Line = debug.getinfo( TraceLevel, "nl" ).currentline + end self:T3( self.Schedule[Scheduler][CallID] ) - self.Schedule[Scheduler][CallID].CallHandler = function( CallID ) - --self:E( CallID ) + self.Schedule[Scheduler][CallID].CallHandler = function( Params ) + + local CallID = Params.CallID + local Source = Params.Source + local Line = Params.Line local ErrorHandler = function( errmsg ) env.info( "Error in timer function: " .. errmsg ) @@ -122,15 +134,19 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr local Stop = Schedule.Stop or 0 local ScheduleID = Schedule.ScheduleID + local Prefix = ( Repeat == 0 ) and " ---> " or " +++> " + local Status, Result --self:E( { SchedulerObject = SchedulerObject } ) if SchedulerObject then local function Timer() + SchedulerObject:T( Prefix .. ( Source or "-" ) .. ": " .. ( Line or "-" ) ) return ScheduleFunction( SchedulerObject, unpack( ScheduleArguments ) ) end Status, Result = xpcall( Timer, ErrorHandler ) else local function Timer() + self:T( Prefix .. ( Source or "-" ) .. ": " .. ( Line or "-" ) ) return ScheduleFunction( unpack( ScheduleArguments ) ) end Status, Result = xpcall( Timer, ErrorHandler ) @@ -161,13 +177,13 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr self:Stop( Scheduler, CallID ) end else - self:E( "Scheduled obsolete call for CallID: " .. CallID ) + self:I( " <<<> " .. ( Source or "-" ) .. ": " .. ( Line or "-" ) ) end return nil end - self:Start( Scheduler, CallID ) + self:Start( Scheduler, CallID, Source, Line ) return CallID end @@ -181,7 +197,7 @@ function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID ) end end -function SCHEDULEDISPATCHER:Start( Scheduler, CallID ) +function SCHEDULEDISPATCHER:Start( Scheduler, CallID, Source, Line ) self:F2( { Start = CallID, Scheduler = Scheduler } ) if CallID then @@ -192,13 +208,13 @@ function SCHEDULEDISPATCHER:Start( Scheduler, CallID ) Schedule[CallID].StartTime = timer.getTime() -- Set the StartTime field to indicate when the scheduler started. Schedule[CallID].ScheduleID = timer.scheduleFunction( Schedule[CallID].CallHandler, - CallID, + { CallID = CallID, Source = Source, Line = Line }, timer.getTime() + Schedule[CallID].Start ) end else for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do - self:Start( Scheduler, CallID ) -- Recursive + self:Start( Scheduler, CallID, Source, Line ) -- Recursive end end end diff --git a/Moose Development/Moose/Core/Scheduler.lua b/Moose Development/Moose/Core/Scheduler.lua index 2b5e7e03b..198329c88 100644 --- a/Moose Development/Moose/Core/Scheduler.lua +++ b/Moose Development/Moose/Core/Scheduler.lua @@ -216,7 +216,7 @@ function SCHEDULER:New( SchedulerObject, SchedulerFunction, SchedulerArguments, self.MasterObject = SchedulerObject if SchedulerFunction then - ScheduleID = self:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) + ScheduleID = self:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, 4 ) end return self, ScheduleID @@ -238,7 +238,7 @@ end -- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. -- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. -- @return #number The ScheduleID of the planned schedule. -function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) +function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, TraceLevel ) self:F2( { Start, Repeat, RandomizeFactor, Stop } ) self:T3( { SchedulerArguments } ) @@ -256,7 +256,8 @@ function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArgume Start, Repeat, RandomizeFactor, - Stop + Stop, + TraceLevel or 3 ) self.Schedules[#self.Schedules+1] = ScheduleID diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 4084e6deb..149d22d23 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -384,7 +384,6 @@ do -- SET_BASE for ObjectName, Object in pairs( self.Database ) do if self:IsIncludeObject( Object ) then - self:E( { "Adding Object:", ObjectName } ) self:Add( ObjectName, Object ) end end diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 37608d7f9..9dedcb35a 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -1393,7 +1393,7 @@ end function ZONE_POLYGON_BASE:Flush() self:F2() - self:E( { Polygon = self.ZoneName, Coordinates = self._.Polygon } ) + self:F( { Polygon = self.ZoneName, Coordinates = self._.Polygon } ) return self end diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index ac3f2923e..2c9c5d792 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1682,9 +1682,7 @@ do -- DETECTION_BASE -- @return #DETECTION_BASE.DetectedItem function DETECTION_BASE:GetDetectedItemByIndex( Index ) - self:I( { DetectedItemsByIndex = self.DetectedItemsByIndex } ) - - self:I( { self.DetectedItemsByIndex } ) + self:F( { self.DetectedItemsByIndex } ) local DetectedItem = self.DetectedItemsByIndex[Index] if DetectedItem then diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 2ec90ab27..97a6aabc0 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -208,7 +208,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) function( self, EventData ) if EventData.IniObjectCategory == 1 then local EventGroup = GROUP:Find( EventData.IniDCSGroup ) - self:E( { CommandCenter = self:GetName(), EventGroup = EventGroup:GetName(), HasGroup = self:HasGroup( EventGroup ), EventData = EventData } ) + --self:E( { CommandCenter = self:GetName(), EventGroup = EventGroup:GetName(), HasGroup = self:HasGroup( EventGroup ), EventData = EventData } ) if EventGroup and self:HasGroup( EventGroup ) then local CommandCenterMenu = MENU_GROUP:New( EventGroup, self:GetText() ) local MenuReporting = MENU_GROUP:New( EventGroup, "Missions Reports", CommandCenterMenu ) diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index 9f827e3d4..9d26e4d08 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -279,7 +279,6 @@ do -- DETECTION MANAGER -- @param Functional.Detection#DETECTION_BASE Detection -- @return #DETECTION_MANAGER self function DETECTION_MANAGER:ProcessDetected( Detection ) - self:E() end diff --git a/Moose Development/Moose/Tasking/Task_Manager.lua b/Moose Development/Moose/Tasking/Task_Manager.lua index 8c2d76406..e4f4ee9a7 100644 --- a/Moose Development/Moose/Tasking/Task_Manager.lua +++ b/Moose Development/Moose/Tasking/Task_Manager.lua @@ -185,7 +185,6 @@ do -- TASK_MANAGER -- @param #TASK_MANAGER self -- @return #TASK_MANAGER self function TASK_MANAGER:ManageTasks() - self:E() end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index d23d00070..bce6e4feb 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -307,7 +307,6 @@ end -- @param #CONTROLLABLE self -- @return #CONTROLLABLE function CONTROLLABLE:ClearTasks() - self:E( "ClearTasks" ) local DCSControllable = self:GetDCSObject() @@ -937,7 +936,7 @@ end -- @param #boolean Divebomb (optional) Perform dive bombing. Default false. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb ) - self:E( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb } ) + self:F( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb } ) local _groupattack=false if GroupAttack then @@ -983,7 +982,7 @@ function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, D }, } - self:E( { TaskBombing=DCSTask } ) + self:F( { TaskBombing=DCSTask } ) return DCSTask end From 14b35cb0692c41f092dc475c4800a3697d431e99 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 31 Aug 2019 07:24:31 +0200 Subject: [PATCH 359/485] Improvements in trace output: - Scheduled calls are traced with ---> or +++> prefixes... This is handy to find where scheduled calls were actually called in the code. Line number and function is shown correctly now. ---> is for one schedule, and +++> for repeated schedules. - Fsm output is now also organized better. All Fsm calls are shown with :::> prefix. The onafter, onbefore, onenter or onleave method names are also shown, and also the state transition including the function call place is shown now. -- Now for each trace line, the class name has 30 characters space. --- Moose Development/Moose/Core/Base.lua | 40 +++++++++---- Moose Development/Moose/Core/Fsm.lua | 16 ++++- .../Moose/Core/ScheduleDispatcher.lua | 59 ++++++++++++++----- Moose Development/Moose/Core/Scheduler.lua | 5 +- 4 files changed, 90 insertions(+), 30 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index 5b766d10f..c042904a1 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -891,6 +891,26 @@ end -- Log a trace (only shown when trace is on) -- TODO: Make trace function using variable parameters. +--- Set trace on. +-- @param #BASE self +-- @usage +-- -- Switch the tracing On +-- BASE:TraceOn() +function BASE:TraceOn() + self:TraceOnOff( true ) +end + +--- Set trace off. +-- @param #BASE self +-- @usage +-- -- Switch the tracing Off +-- BASE:TraceOff() +function BASE:TraceOff() + self:TraceOnOff( false ) +end + + + --- Set trace on or off -- Note that when trace is off, no BASE.Debug statement is performed, increasing performance! -- When Moose is loaded statically, (as one file), tracing is switched off by default. @@ -905,7 +925,7 @@ end -- -- Switch the tracing Off -- BASE:TraceOnOff( false ) function BASE:TraceOnOff( TraceOnOff ) - _TraceOnOff = TraceOnOff + _TraceOnOff = TraceOnOff or true end @@ -925,8 +945,8 @@ end -- @param #BASE self -- @param #number Level function BASE:TraceLevel( Level ) - _TraceLevel = Level - self:I( "Tracing level " .. Level ) + _TraceLevel = Level or 1 + self:I( "Tracing level " .. _TraceLevel ) end --- Trace all methods in MOOSE @@ -934,7 +954,7 @@ end -- @param #boolean TraceAll true = trace all methods in MOOSE. function BASE:TraceAll( TraceAll ) - _TraceAll = TraceAll + _TraceAll = TraceAll or true if _TraceAll then self:I( "Tracing all methods in MOOSE " ) @@ -989,7 +1009,7 @@ function BASE:_F( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) if DebugInfoFrom then LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%25s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) end end end @@ -1064,7 +1084,7 @@ function BASE:_T( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) if DebugInfoFrom then LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%25s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end end end @@ -1135,9 +1155,9 @@ function BASE:E( Arguments ) LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%25s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) else - env.info( string.format( "%1s:%25s%05d(%s)" , "E", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%1s:%30s%05d(%s)" , "E", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end end @@ -1163,9 +1183,9 @@ function BASE:I( Arguments ) LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%25s%05d.%s(%s)" , LineCurrent, LineFrom, "I", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)" , LineCurrent, LineFrom, "I", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) else - env.info( string.format( "%1s:%25s%05d(%s)" , "I", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%1s:%30s%05d(%s)" , "I", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end end diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index 22489bc7c..5149b4aa6 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -594,7 +594,17 @@ do -- FSM return errmsg end if self[handler] then - self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] ) + if step == "onafter" or step == "OnAfter" then + self:T( ":::>" .. step .. params[2] .. " : " .. params[1] .. " >> " .. params[2] .. ">" .. step .. params[2] .. "()" .. " >> " .. params[3] ) + elseif step == "onbefore" or step == "OnBefore" then + self:T( ":::>" .. step .. params[2] .. " : " .. params[1] .. " >> " .. step .. params[2] .. "()" .. ">" .. params[2] .. " >> " .. params[3] ) + elseif step == "onenter" or step == "OnEnter" then + self:T( ":::>" .. step .. params[3] .. " : " .. params[1] .. " >> " .. params[2] .. " >> " .. step .. params[3] .. "()" .. ">" .. params[3] ) + elseif step == "onleave" or step == "OnLeave" then + self:T( ":::>" .. step .. params[1] .. " : " .. params[1] .. ">" .. step .. params[1] .. "()" .. " >> " .. params[2] .. " >> " .. params[3] ) + else + self:T( ":::>" .. step .. " : " .. params[1] .. " >> " .. params[2] .. " >> " .. params[3] ) + end self._EventSchedules[EventName] = nil local Result, Value = xpcall( function() return self[handler]( self, unpack( params ) ) end, ErrorHandler ) return Value @@ -718,13 +728,13 @@ do -- FSM if DelaySeconds < 0 then -- Only call the event ONCE! DelaySeconds = math.abs( DelaySeconds ) if not self._EventSchedules[EventName] then - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4 ) + CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true ) self._EventSchedules[EventName] = CallID else -- reschedule end else - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4 ) + CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true ) end else error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 0a70ca72d..ec03bc93b 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -52,7 +52,7 @@ end -- Nothing of this code should be modified without testing it thoroughly. -- @param #SCHEDULEDISPATCHER self -- @param Core.Scheduler#SCHEDULER Scheduler -function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel ) +function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm ) self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel } ) self.CallID = self.CallID + 1 @@ -85,13 +85,40 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr self.Schedule[Scheduler][CallID].Randomize = Randomize or 0 self.Schedule[Scheduler][CallID].Stop = Stop - local Source = "" - local Line = "" + + -- This section handles the tracing of the scheduled calls. + -- Because these calls will be executed with a delay, we inspect the place where these scheduled calls are initiated. + -- The Info structure contains the output of the debug.getinfo() calls, which inspects the call stack for the function name, line number and source name. + -- The call stack has many levels, and the correct semantical function call depends on where in the code AddSchedule was "used". + -- - Using SCHEDULER:New() + -- - Using Schedule:AddSchedule() + -- - Using Fsm:__Func() + -- - Using Class:ScheduleOnce() + -- - Using Class:ScheduleRepeat() + -- - ... + -- So for each of these scheduled call variations, AddSchedule is the workhorse which will schedule the call. + -- But the correct level with the correct semantical function location will differ depending on the above scheduled call invocation forms. + -- That's where the field TraceLevel contains optionally the level in the call stack where the call information is obtained. + -- The TraceLevel field indicates the correct level where the semantical scheduled call was invoked within the source, ensuring that function name, line number and source name are correct. + -- There is one quick ... + -- The FSM class models scheduled calls using the __Func syntax. However, these functions are "tailed". + -- There aren't defined anywhere within the source code, but rather implemented as triggers within the FSM logic, + -- and using the onbefore, onafter, onenter, onleave prefixes. (See the FSM for details). + -- Therefore, in the call stack, at the TraceLevel these functions are mentioned as "tail calls", and the Info.name field will be nil as a result. + -- To obtain the correct function name for FSM object calls, the function is mentioned in the call stack at a higher stack level. + -- So when function name stored in Info.name is nil, then I inspect the function name within the call stack one level higher. + -- So this little piece of code does its magic wonderfully, preformance overhead is neglectible, as scheduled calls don't happen that often. + + local Info = {} if debug then TraceLevel = TraceLevel or 2 - Source = debug.getinfo( TraceLevel, "S" ).source - Line = debug.getinfo( TraceLevel, "nl" ).currentline + Info = debug.getinfo( TraceLevel, "nlS" ) + local name_fsm = debug.getinfo( TraceLevel - 1, "n" ).name -- #string + if name_fsm then + Info.name = name_fsm + end + --env.info( debug.traceback() ) end self:T3( self.Schedule[Scheduler][CallID] ) @@ -99,8 +126,10 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr self.Schedule[Scheduler][CallID].CallHandler = function( Params ) local CallID = Params.CallID - local Source = Params.Source - local Line = Params.Line + local Info = Params.Info + local Source = Info.source + local Line = Info.currentline + local Name = Info.name or "?" local ErrorHandler = function( errmsg ) env.info( "Error in timer function: " .. errmsg ) @@ -134,19 +163,19 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr local Stop = Schedule.Stop or 0 local ScheduleID = Schedule.ScheduleID - local Prefix = ( Repeat == 0 ) and " ---> " or " +++> " + local Prefix = ( Repeat == 0 ) and "--->" or "+++>" local Status, Result --self:E( { SchedulerObject = SchedulerObject } ) if SchedulerObject then local function Timer() - SchedulerObject:T( Prefix .. ( Source or "-" ) .. ": " .. ( Line or "-" ) ) + SchedulerObject:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) return ScheduleFunction( SchedulerObject, unpack( ScheduleArguments ) ) end Status, Result = xpcall( Timer, ErrorHandler ) else local function Timer() - self:T( Prefix .. ( Source or "-" ) .. ": " .. ( Line or "-" ) ) + self:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) return ScheduleFunction( unpack( ScheduleArguments ) ) end Status, Result = xpcall( Timer, ErrorHandler ) @@ -177,13 +206,13 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr self:Stop( Scheduler, CallID ) end else - self:I( " <<<> " .. ( Source or "-" ) .. ": " .. ( Line or "-" ) ) + self:I( "<<<>" .. Name .. ":" .. Line .. " (" .. Source .. ")" ) end return nil end - self:Start( Scheduler, CallID, Source, Line ) + self:Start( Scheduler, CallID, Info ) return CallID end @@ -197,7 +226,7 @@ function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID ) end end -function SCHEDULEDISPATCHER:Start( Scheduler, CallID, Source, Line ) +function SCHEDULEDISPATCHER:Start( Scheduler, CallID, Info ) self:F2( { Start = CallID, Scheduler = Scheduler } ) if CallID then @@ -208,13 +237,13 @@ function SCHEDULEDISPATCHER:Start( Scheduler, CallID, Source, Line ) Schedule[CallID].StartTime = timer.getTime() -- Set the StartTime field to indicate when the scheduler started. Schedule[CallID].ScheduleID = timer.scheduleFunction( Schedule[CallID].CallHandler, - { CallID = CallID, Source = Source, Line = Line }, + { CallID = CallID, Info = Info }, timer.getTime() + Schedule[CallID].Start ) end else for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do - self:Start( Scheduler, CallID, Source, Line ) -- Recursive + self:Start( Scheduler, CallID, Info ) -- Recursive end end end diff --git a/Moose Development/Moose/Core/Scheduler.lua b/Moose Development/Moose/Core/Scheduler.lua index 198329c88..df54ced90 100644 --- a/Moose Development/Moose/Core/Scheduler.lua +++ b/Moose Development/Moose/Core/Scheduler.lua @@ -238,7 +238,7 @@ end -- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. -- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. -- @return #number The ScheduleID of the planned schedule. -function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, TraceLevel ) +function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, TraceLevel, Fsm ) self:F2( { Start, Repeat, RandomizeFactor, Stop } ) self:T3( { SchedulerArguments } ) @@ -257,7 +257,8 @@ function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArgume Repeat, RandomizeFactor, Stop, - TraceLevel or 3 + TraceLevel or 3, + Fsm ) self.Schedules[#self.Schedules+1] = ScheduleID From 372dd704d22e68c1127ea299b202b5e8b419fb35 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Tue, 3 Sep 2019 17:25:45 +0200 Subject: [PATCH 360/485] - Added a randomization to some of the internal schedulers to ensure that the simulation does not get unnecessary hickups and guarantees more fluent play. - Added the option SetFlashStatus() to the CommandCenter to ensure flashing of subscribed tasks automatically to inform the player while in-flight. Also extended TASK to ensure the flasing gets initialized once the player joins the task. --- .../Moose/Functional/ZoneCaptureCoalition.lua | 8 ++++---- Moose Development/Moose/Tasking/CommandCenter.lua | 10 ++++++++++ Moose Development/Moose/Tasking/Task.lua | 2 ++ Moose Development/Moose/Wrapper/Client.lua | 5 +++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index ce32338cc..4ee91c93f 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -676,7 +676,7 @@ do -- ZONE_CAPTURE_COALITION --BASE:GetParent( self ).onafterGuard( self ) if not self.SmokeScheduler then - self.SmokeScheduler = self:ScheduleRepeat( 1, 1, 0.1, nil, self.StatusSmoke, self ) + self.SmokeScheduler = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 0.1, nil, self.StatusSmoke, self ) end end @@ -747,13 +747,13 @@ do -- ZONE_CAPTURE_COALITION -- function ZONE_CAPTURE_COALITION:Start( StartInterval, RepeatInterval ) - StartInterval = StartInterval or 15 - RepeatInterval = RepeatInterval or 15 + self.StartInterval = StartInterval or 15 + self.RepeatInterval = RepeatInterval or 15 if self.ScheduleStatusZone then self:ScheduleStop( self.ScheduleStatusZone ) end - self.ScheduleStatusZone = self:ScheduleRepeat( StartInterval, RepeatInterval, 0.1, nil, self.StatusZone, self ) + self.ScheduleStatusZone = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 1.5, nil, self.StatusZone, self ) end diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 97a6aabc0..b42744659 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -201,6 +201,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) self:SetAutoAssignTasks( false ) self:SetAutoAcceptTasks( true ) self:SetAutoAssignMethod( COMMANDCENTER.AutoAssignMethods.Random ) + self:SetFlashStatus( false ) self:HandleEvent( EVENTS.Birth, --- @param #COMMANDCENTER self @@ -791,3 +792,12 @@ function COMMANDCENTER:ReportDetails( ReportGroup, Task ) self:MessageToGroup( Report:Text(), ReportGroup ) end + +--- Let the command center flash a report of the status of the subscribed task to a group. +-- @param #COMMANDCENTER self +function COMMANDCENTER:SetFlashStatus( Flash ) + self:F() + + self.FlashStatus = Flash or true + +end diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index 1a4f8b876..b0df5edfb 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -856,6 +856,8 @@ do -- Group Assignment CommandCenter:SetMenu() + self:MenuFlashTaskStatus( TaskGroup, self:GetMission():GetCommandCenter().FlashStatus ) + return self end diff --git a/Moose Development/Moose/Wrapper/Client.lua b/Moose Development/Moose/Wrapper/Client.lua index ada653ba3..cef00651d 100644 --- a/Moose Development/Moose/Wrapper/Client.lua +++ b/Moose Development/Moose/Wrapper/Client.lua @@ -133,7 +133,8 @@ function CLIENT:FindByName( ClientName, ClientBriefing, Error ) end function CLIENT:Register( ClientName ) - local self = BASE:Inherit( self, UNIT:Register( ClientName ) ) + + local self = BASE:Inherit( self, UNIT:Register( ClientName ) ) -- #CLIENT self:F( ClientName ) self.ClientName = ClientName @@ -141,7 +142,7 @@ function CLIENT:Register( ClientName ) self.ClientAlive2 = false --self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 ) - self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. ClientName }, 1, 5 ) + self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. ClientName }, 1, 5, 0.5 ) self:F( self ) return self From e0075cc9ac52bc0a9831af1190972f47f9473f85 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 8 Sep 2019 08:38:33 +0200 Subject: [PATCH 361/485] - Added a new UTILS.kpairs iterator, which will base the iteration on a different key, which can be determined before the iteration starts. eg. the key should be a sort in ascending order on the distance of the information in the set, and it should return the distance + item in the set for each element in the set. - Resolved for TASK_CAPTURE_ZONES the whole logic of determining correctly the player and AI defense zones, as to be displayed in the information panel, this was a very difficult exercise for me. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 78 ++++++++++++------- .../Moose/Tasking/CommandCenter.lua | 4 +- Moose Development/Moose/Tasking/Task.lua | 12 ++- .../Moose/Tasking/Task_Capture_Zone.lua | 9 ++- Moose Development/Moose/Utilities/Utils.lua | 26 +++++++ 5 files changed, 92 insertions(+), 37 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index c8effaeee..32a2a5a9e 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -4024,6 +4024,29 @@ do -- AI_A2G_DISPATCHER end + --- Determine the distance as the keys of reference of the detected items. + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:Keys( DetectedItem ) + + self:F( { DetectedItem = DetectedItem } ) + + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ShortestDistance = 999999999 + + for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do + local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE + + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) + + if EvaluateDistance <= ShortestDistance then + ShortestDistance = EvaluateDistance + end + end + + return ShortestDistance + end + --- Assigns A2G AI Tasks in relation to the detected items. -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:Order( DetectedItem ) @@ -4082,7 +4105,7 @@ do -- AI_A2G_DISPATCHER -- Show tactical situation local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() - Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "â– ", ThreatLevel ) ) ) + Report:Add( string.format( " - %1s%s ( %04s ): ( #%02d - %-4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "â– ", ThreatLevel ) ) ) for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do local Defender = Defender -- Wrapper.Group#GROUP if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then @@ -4203,7 +4226,7 @@ do -- AI_A2G_DISPATCHER -- Now that all obsolete tasks are removed, loop through the detected targets. --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + for DetectedDistance, DetectedItem in UTILS.kpairs( Detection:GetDetectedItems(), function( t ) return self:Keys( t ) end, function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do if not self.Detection:IsDetectedItemLocked( DetectedItem ) == true then local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem @@ -4225,44 +4248,43 @@ do -- AI_A2G_DISPATCHER -- 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 EngageCoordinate = nil - - for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do - local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE - - local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) + + local EngageDefenses = nil - if EvaluateDistance <= self.DefenseRadius then - - local DistanceProbability = ( self.DefenseRadius / EvaluateDistance * self.DefenseReactivity ) + self:F( { DetectedDistance = DetectedDistance, DefenseRadius = self.DefenseRadius } ) + if DetectedDistance <= self.DefenseRadius then + + self:F( { DetectedApproach = self._DefenseApproach } ) + if self._DefenseApproach == AI_A2G_DISPATCHER.DefenseApproach.Distance then + EngageDefenses = true + self:F( { EngageDefenses = EngageDefenses } ) + end + + if self._DefenseApproach == AI_A2G_DISPATCHER.DefenseApproach.Random then + local DistanceProbability = ( self.DefenseRadius / DetectedDistance * self.DefenseReactivity ) local DefenseProbability = math.random() - + self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) - - if self._DefenseApproach == AI_A2G_DISPATCHER.DefenseApproach.Random then - - if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then - EngageCoordinate = DefenseCoordinate - break - end - end - if self._DefenseApproach == AI_A2G_DISPATCHER.DefenseApproach.Distance then - EngageCoordinate = DefenseCoordinate - break + + if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then + EngageDefenses = true end end + + end + self:F( { EngageDefenses = EngageDefenses, DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } ) + -- There needs to be an EngageCoordinate. -- If self.DefenseLimit is set (thus limit the amount of defenses to one zone), then only start a new defense if the maximum has not been reached. -- If self.DefenseLimit has not been set, there is an unlimited amount of zones to be defended. - if ( EngageCoordinate and ( self.DefenseLimit and DefenseTotal < self.DefenseLimit ) or not self.DefenseLimit ) then + if ( EngageDefenses and ( self.DefenseLimit and DefenseTotal < self.DefenseLimit ) or not self.DefenseLimit ) then do local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged... if DefendersMissing > 0 then self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) - self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD", EngageCoordinate ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD" ) end end @@ -4270,7 +4292,7 @@ do -- AI_A2G_DISPATCHER local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... if DefendersMissing > 0 then self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) - self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS", EngageCoordinate ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS" ) end end @@ -4278,7 +4300,7 @@ do -- AI_A2G_DISPATCHER local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_BAI( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... if DefendersMissing > 0 then self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) - self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI", EngageCoordinate ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI" ) end end end diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index b42744659..0673facb1 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -200,7 +200,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) self:SetAutoAssignTasks( false ) self:SetAutoAcceptTasks( true ) - self:SetAutoAssignMethod( COMMANDCENTER.AutoAssignMethods.Random ) + self:SetAutoAssignMethod( COMMANDCENTER.AutoAssignMethods.Distance ) self:SetFlashStatus( false ) self:HandleEvent( EVENTS.Birth, @@ -210,7 +210,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) if EventData.IniObjectCategory == 1 then local EventGroup = GROUP:Find( EventData.IniDCSGroup ) --self:E( { CommandCenter = self:GetName(), EventGroup = EventGroup:GetName(), HasGroup = self:HasGroup( EventGroup ), EventData = EventData } ) - if EventGroup and self:HasGroup( EventGroup ) then + if EventGroup and EventGroup:IsAlive() and self:HasGroup( EventGroup ) then local CommandCenterMenu = MENU_GROUP:New( EventGroup, self:GetText() ) local MenuReporting = MENU_GROUP:New( EventGroup, "Missions Reports", CommandCenterMenu ) local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Status Report", MenuReporting, self.ReportSummary, self, EventGroup ) diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index b0df5edfb..7efa0962d 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -1232,12 +1232,16 @@ end --- Report the task status. -- @param #TASK self +-- @param Wrapper.Group#GROUP TaskGroup function TASK:MenuTaskStatus( TaskGroup ) - local ReportText = self:ReportDetails( TaskGroup ) - - self:T( ReportText ) - self:GetMission():GetCommandCenter():MessageTypeToGroup( ReportText, TaskGroup, MESSAGE.Type.Detailed ) + if TaskGroup:IsAlive() then + + local ReportText = self:ReportDetails( TaskGroup ) + + self:T( ReportText ) + self:GetMission():GetCommandCenter():MessageTypeToGroup( ReportText, TaskGroup, MESSAGE.Type.Detailed ) + end end diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index 59e7d988f..3ca128b98 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -240,10 +240,13 @@ do -- TASK_CAPTURE_ZONE local DefenseTaskCaptureDispatcher = self.Dispatcher:GetDefenseTaskCaptureDispatcher() -- Tasking.Task_Capture_Dispatcher#TASK_CAPTURE_DISPATCHER if DefenseTaskCaptureDispatcher then - -- Loop through all zones of the Defenses, and check which zone has an assigned task! + -- Loop through all zones of the player Defenses, and check which zone has an assigned task! + -- The Zones collection contains a Task. This Task is checked if it is assigned. + -- If Assigned, then this task will be the task that is the closest to the defense zone. for TaskName, CaptureZone in pairs( DefenseTaskCaptureDispatcher.Zones or {} ) do local Task = CaptureZone.Task -- Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE - if Task then + if Task and Task:IsStateAssigned() then -- We also check assigned. + -- Now we register the defense player zone information to the task report. self.TaskInfo:AddInfo( "Defense Player Zone", Task.ZoneGoal:GetName(), 30, "MOD", Persist ) self.TaskInfo:AddCoordinate( Task.ZoneGoal:GetZone():GetCoordinate(), 31, "MOD", Persist, false, "Defense Player Coordinate" ) end @@ -252,7 +255,7 @@ do -- TASK_CAPTURE_ZONE local DefenseAIA2GDispatcher = self.Dispatcher:GetDefenseAIA2GDispatcher() -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER if DefenseAIA2GDispatcher then - -- Loop through all zones of the Defenses, and check which zone has an assigned task! + -- Loop through all the tasks of the AI Defenses, and check which zone is involved in the defenses and is active! for Defender, Task in pairs( DefenseAIA2GDispatcher:GetDefenderTasks() or {} ) do local DetectedItem = DefenseAIA2GDispatcher:GetDefenderTaskTarget( Defender ) if DetectedItem then diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index f32caa744..62f81945a 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -519,6 +519,32 @@ function UTILS.spairs( t, order ) end end + +-- Here is a customized version of pairs, which I called kpairs because it iterates over the table in a sorted order, based on a function that will determine the keys as reference first. +function UTILS.kpairs( t, getkey, order ) + -- collect the keys + local keys = {} + local keyso = {} + for k, o in pairs(t) do keys[#keys+1] = k keyso[#keyso+1] = getkey( o ) end + + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + else + table.sort(keys) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keyso[i], t[keys[i]] + end + end +end + -- Here is a customized version of pairs, which I called rpairs because it iterates over the table in a random order. function UTILS.rpairs( t ) -- collect the keys From 1aedcf1ae466cd09a4a433173028e9a327c5785d Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 9 Sep 2019 08:19:48 +0200 Subject: [PATCH 362/485] Radio Queue --- .../Moose/AI/AI_A2G_Dispatcher.lua | 33 +- .../Moose/AI/AI_Air_Dispatcher.lua | 3 + .../Moose/AI/AI_Escort_Dispatcher.lua | 1 + Moose Development/Moose/Core/Radio.lua | 466 ----------------- Moose Development/Moose/Core/RadioQueue.lua | 483 ++++++++++++++++++ Moose Development/Moose/Core/Settings.lua | 70 ++- Moose Development/Moose/Modules.lua | 1 + .../Moose/Tasking/DetectionManager.lua | 50 +- Moose Development/Moose/Wrapper/Unit.lua | 42 +- 9 files changed, 667 insertions(+), 482 deletions(-) create mode 100644 Moose Development/Moose/Core/RadioQueue.lua diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 32a2a5a9e..e03bb740a 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -1133,7 +1133,7 @@ do -- AI_A2G_DISPATCHER self.TakeoffScheduleID = self:ScheduleRepeat( 10, 10, 0, nil, self.ResourceTakeoff, self ) - self:__Start( 1 ) + self:__Start( 1 ) return self end @@ -3508,15 +3508,16 @@ do -- AI_A2G_DISPATCHER self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, Fsm, nil, DefenderGrouping ) function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"Defender Birth", Defender:GetName()}) + self:F({"Defender Takeoff", Defender:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) local DefenderName = Defender:GetCallsign() + local DefenderUnitName = Defender:GetName() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) if Squadron then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne." ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne.", "Wheels_up.wav", 3, "A2G/", Squadron, Defender ) Fsm:Patrol() -- Engage on the TargetSetUnit end end @@ -3526,10 +3527,11 @@ do -- AI_A2G_DISPATCHER self:GetParent(self).onafterPatrolRoute( self, Defender, From, Event, To ) local DefenderName = Defender:GetCallsign() + local DefenderUnitName = Defender:GetName() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) if Squadron then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " patrolling." ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " patrolling.", "Patrolling.wav", 3, "A2G/", Squadron, Defender ) end Dispatcher:ClearDefenderTaskTarget( Defender ) @@ -3540,9 +3542,10 @@ do -- AI_A2G_DISPATCHER self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) local DefenderName = Defender:GetCallsign() + local DefenderUnitName = Defender:GetName() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning.", "Returning_to_base.wav", 3, "A2G/", Squadron, Defender ) Dispatcher:ClearDefenderTaskTarget( Defender ) end @@ -3568,9 +3571,10 @@ do -- AI_A2G_DISPATCHER self:GetParent(self).onafterHome( self, Defender, From, Event, To ) local DefenderName = Defender:GetCallsign() + local DefenderUnitName = Defender:GetName() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing.", "Landing_at_base.wav", 3, "A2G/", Squadron, Defender ) if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) @@ -3620,6 +3624,7 @@ do -- AI_A2G_DISPATCHER --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) local DefenderName = Defender:GetCallsign() + local DefenderUnitName = Defender:GetName() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) @@ -3627,16 +3632,16 @@ do -- AI_A2G_DISPATCHER self:F( { DefenderTarget = DefenderTarget } ) if DefenderTarget then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne. Engaging!" ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne. Engaging!", "Wheels_up.wav", 3, "A2G/", Squadron, Defender ) Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end function Fsm:onafterEngageRoute( Defender, From, Event, To, AttackSetUnit ) self:F({"Engage Route", Defender:GetName()}) - self:GetParent(self).onafterEngageRoute( self, Defender, From, Event, To, AttackSetUnit ) local DefenderName = Defender:GetCallsign() + local DefenderUnitName = Defender:GetName() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) @@ -3644,8 +3649,9 @@ do -- AI_A2G_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route to ground target at " .. Coordinate:ToStringA2G( Defender ) ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route to ground target at " .. Coordinate:ToStringA2G( Defender ), "Moving_on_to_ground_target.wav", 3, "A2G/", Squadron, Defender ) end + self:GetParent(self).onafterEngageRoute( self, Defender, From, Event, To, AttackSetUnit ) end function Fsm:OnAfterEngage( Defender, From, Event, To, AttackSetUnit ) @@ -3653,13 +3659,14 @@ do -- AI_A2G_DISPATCHER --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) local DefenderName = Defender:GetCallsign() + local DefenderUnitName = Defender:GetName() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) local FirstUnit = AttackSetUnit:GetFirst() if FirstUnit then local Coordinate = FirstUnit:GetCoordinate() - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging ground target at " .. Coordinate:ToStringA2G( Defender ) ) + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging ground target at " .. Coordinate:ToStringA2G( Defender ), "Engaging_ground_target.wav", 3, "A2G/", DefenderUnitName ) end end @@ -3667,9 +3674,10 @@ do -- AI_A2G_DISPATCHER self:F({"Defender RTB", Defender:GetName()}) local DefenderName = Defender:GetCallsign() + local DefenderUnitName = Defender:GetName() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning.", "Returning_to_base.wav", 3, "A2G/", Squadron, Defender ) self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) @@ -3698,9 +3706,10 @@ do -- AI_A2G_DISPATCHER self:GetParent(self).onafterHome( self, Defender, From, Event, To ) local DefenderName = Defender:GetCallsign() + local DefenderUnitName = Defender:GetName() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing.", "Landing_at_base.wav", 3, "A2G/", Squadron, Defender ) if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) diff --git a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua index c802521dd..ac912f819 100644 --- a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua @@ -3289,6 +3289,9 @@ do self:Patrol( SquadronName, PatrolTaskType ) end + + + end diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua index 8615573fb..7b85afe07 100644 --- a/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua @@ -3,6 +3,7 @@ -- ## Features: -- -- -- * Provides the facilities to trigger escorts when players join flight slots. +-- * -- -- === -- diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index b6c13185c..bd3b5e669 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -797,470 +797,4 @@ function BEACON:_TACANToFrequency(TACANChannel, TACANMode) return (A + TACANChannel - B) * 1000000 end ---- Manages radio transmissions. --- --- @type RADIOQUEUE --- @field #string ClassName Name of the class "RADIOQUEUE". --- @field #string lid ID for dcs.log. --- @field #number frequency The radio frequency in Hz. --- @field #number modulation The radio modulation. Either radio.modulation.AM or radio.modulation.FM. --- @field Core.Scheduler#SCHEDULER scheduler The scheduler. --- @field #string RQid The radio queue scheduler ID. --- @field #table queue The queue of transmissions. --- @field #number Tlast Time (abs) when the last transmission finished. --- @field Core.Point#COORDINATE sendercoord Coordinate from where transmissions are broadcasted. --- @field #number sendername Name of the sending unit or static. --- @field Wrapper.Positionable#POSITIONABLE positionable The positionable to broadcast the message. --- @field #number power Power of radio station in Watts. Default 100 W. --- @field #table numbers Table of number transmission parameters. --- @extends Core.Base#BASE -RADIOQUEUE = { - ClassName = "RADIOQUEUE", - lid=nil, - frequency=nil, - modulation=nil, - scheduler=nil, - RQid=nil, - queue={}, - Tlast=nil, - sendercoord=nil, - sendername=nil, - positionable=nil, - power=100, - numbers={}, -} - ---- Radio queue transmission data. --- @type RADIOQUEUE.Transmission --- @field #string filename Name of the file to be transmitted. --- @field #string path Path in miz file where the file is located. --- @field #number duration Duration in seconds. --- @field #number Tstarted Mission time (abs) in seconds when the transmission started. --- @field #boolean isplaying If true, transmission is currently playing. --- @field #number Tplay Mission time (abs) in seconds when the transmission should be played. - - ---- Create a new RADIOQUEUE object for a given radio frequency/modulation. --- @param #RADIOQUEUE self --- @param #number frequency The radio frequency in MHz. --- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM. --- @return #RADIOQUEUE self The RADIOQUEUE object. -function RADIOQUEUE:New(frequency, modulation) - - -- Inherit base - local self=BASE:Inherit(self, BASE:New()) -- Core.Radio#RADIOQUEUE - - self.lid="RADIOQUEUE | " - - if frequency==nil then - self:E(self.lid.."ERROR: No frequency specified as first parameter!") - return nil - end - - -- Frequency in Hz. - self.frequency=frequency*1000000 - - -- Modulation. - self.modulation=modulation or radio.modulation.AM - - -- Scheduler - self.scheduler=SCHEDULER:New() - - return self -end - ---- Start the radio queue. --- @param #RADIOQUEUE self --- @param #number delay (Optional) Delay in seconds, before the radio queue is started. Default 1 sec. --- @param #number dt (Optional) Time step in seconds for checking the queue. Default 0.01 sec. --- @return #RADIOQUEUE self The RADIOQUEUE object. -function RADIOQUEUE:Start(delay, dt) - - delay=delay or 1 - - dt=dt or 0.01 - - self:I(self.lid..string.format("Starting RADIOQUEUE in %.1f seconds with interval dt=%.3f seconds.", delay, dt)) - - self.RQid=self.scheduler:Schedule(self, self._CheckRadioQueue, {}, delay, dt) - - return self -end - ---- Stop the radio queue. Stop scheduler and delete queue. --- @param #RADIOQUEUE self --- @return #RADIOQUEUE self The RADIOQUEUE object. -function RADIOQUEUE:Stop() - self:I(self.lid.."Stopping RADIOQUEUE.") - self.scheduler:Stop(self.RQid) - self.queue={} - return self -end - ---- Set coordinate from where the transmission is broadcasted. --- @param #RADIOQUEUE self --- @param Core.Point#COORDINATE coordinate Coordinate of the sender. --- @return #RADIOQUEUE self The RADIOQUEUE object. -function RADIOQUEUE:SetSenderCoordinate(coordinate) - self.sendercoord=coordinate - return self -end - ---- Set name of unit or static from which transmissions are made. --- @param #RADIOQUEUE self --- @param #string name Name of the unit or static used for transmissions. --- @return #RADIOQUEUE self The RADIOQUEUE object. -function RADIOQUEUE:SetSenderUnitName(name) - self.sendername=name - return self -end - ---- Set parameters of a digit. --- @param #RADIOQUEUE self --- @param #number digit The digit 0-9. --- @param #string filename The name of the sound file. --- @param #number duration The duration of the sound file in seconds. --- @param #string path The directory within the miz file where the sound is located. Default "l10n/DEFAULT/". --- @return #RADIOQUEUE self The RADIOQUEUE object. -function RADIOQUEUE:SetDigit(digit, filename, duration, path) - - local transmission={} --#RADIOQUEUE.Transmission - transmission.filename=filename - transmission.duration=duration - transmission.path=path or "l10n/DEFAULT/" - - -- Convert digit to string in case it is given as a number. - if type(digit)=="number" then - digit=tostring(digit) - end - - -- Set transmission. - self.numbers[digit]=transmission - - return self -end - ---- Add a transmission to the radio queue. --- @param #RADIOQUEUE self --- @param #RADIOQUEUE.Transmission transmission The transmission data table. --- @return #RADIOQUEUE self The RADIOQUEUE object. -function RADIOQUEUE:AddTransmission(transmission) - self:F({transmission=transmission}) - - -- Init. - transmission.isplaying=false - transmission.Tstarted=nil - - -- Add to queue. - table.insert(self.queue, transmission) - - return self -end - ---- Add a transmission to the radio queue. --- @param #RADIOQUEUE self --- @param #string filename Name of the sound file. Usually an ogg or wav file type. --- @param #number duration Duration in seconds the file lasts. --- @param #number path Directory path inside the miz file where the sound file is located. Default "l10n/DEFAULT/". --- @param #number tstart Start time (abs) seconds. Default now. --- @param #number interval Interval in seconds after the last transmission finished. --- @return #RADIOQUEUE self The RADIOQUEUE object. -function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval) - - -- Sanity checks. - if not filename then - self:E(self.lid.."ERROR: No filename specified.") - return nil - end - if type(filename)~="string" then - self:E(self.lid.."ERROR: Filename specified is NOT a string.") - return nil - end - - if not duration then - self:E(self.lid.."ERROR: No duration of transmission specified.") - return nil - end - if type(duration)~="number" then - self:E(self.lid.."ERROR: Duration specified is NOT a number.") - return nil - end - - - local transmission={} --#RADIOQUEUE.Transmission - transmission.filename=filename - transmission.duration=duration - transmission.path=path or "l10n/DEFAULT/" - transmission.Tplay=tstart or timer.getAbsTime() - - -- Add transmission to queue. - self:AddTransmission(transmission) - - return self -end - ---- Convert a number (as string) into a radio transmission. --- E.g. for board number or headings. --- @param #RADIOQUEUE self --- @param #string number Number string, e.g. "032" or "183". --- @param #number delay Delay before transmission in seconds. --- @param #number interval Interval between the next call. --- @return #number Duration of the call in seconds. -function RADIOQUEUE:Number2Transmission(number, delay, interval) - - --- 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 - - -- Split string into characters. - local numbers=_split(number) - - local wait=0 - for i=1,#numbers do - - -- Current number - local n=numbers[i] - - -- Radio call. - local transmission=UTILS.DeepCopy(self.numbers[n]) --#RADIOQUEUE.Transmission - - transmission.Tplay=timer.getAbsTime()+(delay or 0) - - if interval and i==1 then - transmission.interval=interval - end - - self:AddTransmission(transmission) - - -- Add up duration of the number. - wait=wait+transmission.duration - end - - -- Return the total duration of the call. - return wait -end - - ---- Broadcast radio message. --- @param #RADIOQUEUE self --- @param #RADIOQUEUE.Transmission transmission The transmission. -function RADIOQUEUE:Broadcast(transmission) - - -- Get unit sending the transmission. - local sender=self:_GetRadioSender() - - -- Construct file name. - local filename=string.format("%s%s", transmission.path, transmission.filename) - - -- Create subtitle for transmission. - --local subtitle=self:_RadioSubtitle(radio, call, loud) - - 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=self.frequency, -- Frequency in Hz. - modulation=self.modulation, - }} - - -- Command to tranmit the call. - local commandTransmit={ - id = "TransmitMessage", - params = { - file=filename, - duration=transmission.subduration or 5, - subtitle=transmission.subtitle or "", - 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().")) - - -- Position from where to transmit. - local vec3=nil - - -- Try to get positon from sender unit/static. - if self.sendername then - local coord=self:_GetRadioSenderCoord() - if coord then - vec3=coord:GetVec3() - end - end - - -- Try to get fixed positon. - if self.sendercoord and not vec3 then - vec3=self.sendercoord:GetVec3() - end - - -- Transmit via trigger. - if vec3 then - trigger.action.radioTransmission(filename, vec3, self.modulation, false, self.frequency, self.power) - end - - end -end - ---- Check radio queue for transmissions to be broadcasted. --- @param #RADIOQUEUE self -function RADIOQUEUE:_CheckRadioQueue() - - -- Check if queue is empty. - if #self.queue==0 then - return - end - - -- Get current abs time. - local time=timer.getAbsTime() - - local playing=false - local next=nil --#RADIOQUEUE.Transmission - local remove=nil - for i,_transmission in ipairs(self.queue) do - local transmission=_transmission --#RADIOQUEUE.Transmission - - -- 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 ith element in queue. - remove=i - - -- Store time last transmission finished. - self.Tlast=time - - else -- still playing - - -- Transmission is still playing. - playing=true - - end - - else -- not playing yet - - local Tlast=self.Tlast - - if transmission.interval==nil then - - -- Not playing ==> this will be next. - if next==nil then - next=transmission - end - - else - - if Tlast==nil or time-Tlast>=transmission.interval then - next=transmission - else - - end - end - - -- We got a transmission or one with an interval that is not due yet. No need for anything else. - if next or Tlast then - break - end - - end - - else - - -- Transmission not due yet. - - end - end - - -- Found a new transmission. - if next~=nil and not playing then - self:Broadcast(next) - next.isplaying=true - next.Tstarted=time - end - - -- Remove completed calls from queue. - if remove then - table.remove(self.queue, remove) - end - -end - ---- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. --- @param #RADIOQUEUE self --- @return Wrapper.Unit#UNIT Sending aircraft unit or nil if was not setup, is not an aircraft or is not alive. -function RADIOQUEUE:_GetRadioSender() - - -- Check if we have a sending aircraft. - local sender=nil --Wrapper.Unit#UNIT - - -- Try the general default. - if self.sendername then - -- First try to find a unit - sender=UNIT:FindByName(self.sendername) - - -- Check that sender is alive and an aircraft. - if sender and sender:IsAlive() and sender:IsAir() then - return sender - end - - end - - return nil -end - ---- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. --- @param #RADIOQUEUE self --- @return Core.Point#COORDINATE Coordinate of the sender unit. -function RADIOQUEUE:_GetRadioSenderCoord() - - local vec3=nil - - -- Try the general default. - if self.sendername then - - -- First try to find a unit - local sender=UNIT:FindByName(self.sendername) - - -- Check that sender is alive and an aircraft. - if sender and sender:IsAlive() then - return sender:GetCoodinate() - end - - -- Now try a static. - local sender=STATIC:FindByName(self.sendername) - - -- Check that sender is alive and an aircraft. - if sender then - return sender:GetCoodinate() - end - - end - - return nil -end diff --git a/Moose Development/Moose/Core/RadioQueue.lua b/Moose Development/Moose/Core/RadioQueue.lua new file mode 100644 index 000000000..8ee0d7749 --- /dev/null +++ b/Moose Development/Moose/Core/RadioQueue.lua @@ -0,0 +1,483 @@ +--- **Core** - Queues Radio Transmissions. +-- +-- === +-- +-- ## Features: +-- +-- * Managed Radio Transmissions. +-- +-- === +-- +-- ### Authors: funkyfranky +-- +-- @module Core.RadioQueue +-- @image Core_Radio.JPG + +--- Manages radio transmissions. +-- +-- @type RADIOQUEUE +-- @field #string ClassName Name of the class "RADIOQUEUE". +-- @field #string lid ID for dcs.log. +-- @field #number frequency The radio frequency in Hz. +-- @field #number modulation The radio modulation. Either radio.modulation.AM or radio.modulation.FM. +-- @field Core.Scheduler#SCHEDULER scheduler The scheduler. +-- @field #string RQid The radio queue scheduler ID. +-- @field #table queue The queue of transmissions. +-- @field #number Tlast Time (abs) when the last transmission finished. +-- @field Core.Point#COORDINATE sendercoord Coordinate from where transmissions are broadcasted. +-- @field #number sendername Name of the sending unit or static. +-- @field Wrapper.Positionable#POSITIONABLE positionable The positionable to broadcast the message. +-- @field #number power Power of radio station in Watts. Default 100 W. +-- @field #table numbers Table of number transmission parameters. +-- @extends Core.Base#BASE +RADIOQUEUE = { + ClassName = "RADIOQUEUE", + lid=nil, + frequency=nil, + modulation=nil, + scheduler=nil, + RQid=nil, + queue={}, + Tlast=nil, + sendercoord=nil, + sendername=nil, + positionable=nil, + power=100, + numbers={}, +} + +--- Radio queue transmission data. +-- @type RADIOQUEUE.Transmission +-- @field #string filename Name of the file to be transmitted. +-- @field #string path Path in miz file where the file is located. +-- @field #number duration Duration in seconds. +-- @field #number Tstarted Mission time (abs) in seconds when the transmission started. +-- @field #boolean isplaying If true, transmission is currently playing. +-- @field #number Tplay Mission time (abs) in seconds when the transmission should be played. + + +--- Create a new RADIOQUEUE object for a given radio frequency/modulation. +-- @param #RADIOQUEUE self +-- @param #number frequency The radio frequency in MHz. +-- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:New(frequency, modulation) + + -- Inherit base + local self=BASE:Inherit(self, BASE:New()) -- #RADIOQUEUE + + self.lid="RADIOQUEUE | " + + if frequency==nil then + self:E(self.lid.."ERROR: No frequency specified as first parameter!") + return nil + end + + -- Frequency in Hz. + self.frequency=frequency*1000000 + + -- Modulation. + self.modulation=modulation or radio.modulation.AM + + -- Scheduler + self.scheduler=SCHEDULER:New() + + return self +end + +--- Start the radio queue. +-- @param #RADIOQUEUE self +-- @param #number delay (Optional) Delay in seconds, before the radio queue is started. Default 1 sec. +-- @param #number dt (Optional) Time step in seconds for checking the queue. Default 0.01 sec. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:Start(delay, dt) + + delay=delay or 1 + + dt=dt or 0.01 + + self:I(self.lid..string.format("Starting RADIOQUEUE in %.1f seconds with interval dt=%.3f seconds.", delay, dt)) + + self.RQid=self.scheduler:Schedule(self, self._CheckRadioQueue, {}, delay, dt) + + return self +end + +--- Stop the radio queue. Stop scheduler and delete queue. +-- @param #RADIOQUEUE self +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:Stop() + self:I(self.lid.."Stopping RADIOQUEUE.") + self.scheduler:Stop(self.RQid) + self.queue={} + return self +end + +--- Set coordinate from where the transmission is broadcasted. +-- @param #RADIOQUEUE self +-- @param Core.Point#COORDINATE coordinate Coordinate of the sender. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSenderCoordinate(coordinate) + self.sendercoord=coordinate + return self +end + +--- Set name of unit or static from which transmissions are made. +-- @param #RADIOQUEUE self +-- @param #string name Name of the unit or static used for transmissions. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSenderUnitName(name) + self.sendername=name + return self +end + +--- Set parameters of a digit. +-- @param #RADIOQUEUE self +-- @param #number digit The digit 0-9. +-- @param #string filename The name of the sound file. +-- @param #number duration The duration of the sound file in seconds. +-- @param #string path The directory within the miz file where the sound is located. Default "l10n/DEFAULT/". +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetDigit(digit, filename, duration, path) + + local transmission={} --#RADIOQUEUE.Transmission + transmission.filename=filename + transmission.duration=duration + transmission.path=path or "l10n/DEFAULT/" + + -- Convert digit to string in case it is given as a number. + if type(digit)=="number" then + digit=tostring(digit) + end + + -- Set transmission. + self.numbers[digit]=transmission + + return self +end + +--- Add a transmission to the radio queue. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission data table. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:AddTransmission(transmission) + self:F({transmission=transmission}) + + -- Init. + transmission.isplaying=false + transmission.Tstarted=nil + + -- Add to queue. + table.insert(self.queue, transmission) + + return self +end + +--- Add a transmission to the radio queue. +-- @param #RADIOQUEUE self +-- @param #string filename Name of the sound file. Usually an ogg or wav file type. +-- @param #number duration Duration in seconds the file lasts. +-- @param #number path Directory path inside the miz file where the sound file is located. Default "l10n/DEFAULT/". +-- @param #number tstart Start time (abs) seconds. Default now. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval) + + -- Sanity checks. + if not filename then + self:E(self.lid.."ERROR: No filename specified.") + return nil + end + if type(filename)~="string" then + self:E(self.lid.."ERROR: Filename specified is NOT a string.") + return nil + end + + if not duration then + self:E(self.lid.."ERROR: No duration of transmission specified.") + return nil + end + if type(duration)~="number" then + self:E(self.lid.."ERROR: Duration specified is NOT a number.") + return nil + end + + + local transmission={} --#RADIOQUEUE.Transmission + transmission.filename=filename + transmission.duration=duration + transmission.path=path or "l10n/DEFAULT/" + transmission.Tplay=tstart or timer.getAbsTime() + + -- Add transmission to queue. + self:AddTransmission(transmission) + + return self +end + +--- Convert a number (as string) into a radio transmission. +-- E.g. for board number or headings. +-- @param #RADIOQUEUE self +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +-- @param #number interval Interval between the next call. +-- @return #number Duration of the call in seconds. +function RADIOQUEUE:Number2Transmission(number, delay, interval) + + --- 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 + + -- Split string into characters. + local numbers=_split(number) + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Radio call. + local transmission=UTILS.DeepCopy(self.numbers[n]) --#RADIOQUEUE.Transmission + + transmission.Tplay=timer.getAbsTime()+(delay or 0) + + if interval and i==1 then + transmission.interval=interval + end + + self:AddTransmission(transmission) + + -- Add up duration of the number. + wait=wait+transmission.duration + end + + -- Return the total duration of the call. + return wait +end + + +--- Broadcast radio message. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission. +function RADIOQUEUE:Broadcast(transmission) + + -- Get unit sending the transmission. + local sender=self:_GetRadioSender() + + -- Construct file name. + local filename=string.format("%s%s", transmission.path, transmission.filename) + + -- Create subtitle for transmission. + --local subtitle=self:_RadioSubtitle(radio, call, loud) + + 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=self.frequency, -- Frequency in Hz. + modulation=self.modulation, + }} + + -- Command to tranmit the call. + local commandTransmit={ + id = "TransmitMessage", + params = { + file=filename, + duration=transmission.subduration or 5, + subtitle=transmission.subtitle or "", + 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().")) + + -- Position from where to transmit. + local vec3=nil + + -- Try to get positon from sender unit/static. + if self.sendername then + local coord=self:_GetRadioSenderCoord() + if coord then + vec3=coord:GetVec3() + end + end + + -- Try to get fixed positon. + if self.sendercoord and not vec3 then + vec3=self.sendercoord:GetVec3() + end + + -- Transmit via trigger. + if vec3 then + self:E("Sending") + self:E( { filename = filename, vec3 = vec3, modulation = self.modulation, frequency = self.frequency, power = self.power } ) + trigger.action.radioTransmission(filename, vec3, self.modulation, false, self.frequency, self.power) + end + + end +end + +--- Check radio queue for transmissions to be broadcasted. +-- @param #RADIOQUEUE self +function RADIOQUEUE:_CheckRadioQueue() + + -- Check if queue is empty. + if #self.queue==0 then + return + end + + -- Get current abs time. + local time=timer.getAbsTime() + + local playing=false + local next=nil --#RADIOQUEUE.Transmission + local remove=nil + for i,_transmission in ipairs(self.queue) do + local transmission=_transmission --#RADIOQUEUE.Transmission + + -- 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 ith element in queue. + remove=i + + -- Store time last transmission finished. + self.Tlast=time + + else -- still playing + + -- Transmission is still playing. + playing=true + + end + + else -- not playing yet + + local Tlast=self.Tlast + + if transmission.interval==nil then + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + else + + if Tlast==nil or time-Tlast>=transmission.interval then + next=transmission + else + + end + end + + -- We got a transmission or one with an interval that is not due yet. No need for anything else. + if next or Tlast then + break + end + + end + + else + + -- Transmission not due yet. + + end + end + + -- Found a new transmission. + if next~=nil and not playing then + self:Broadcast(next) + next.isplaying=true + next.Tstarted=time + end + + -- Remove completed calls from queue. + if remove then + table.remove(self.queue, remove) + end + +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #RADIOQUEUE self +-- @return Wrapper.Unit#UNIT Sending aircraft unit or nil if was not setup, is not an aircraft or is not alive. +function RADIOQUEUE:_GetRadioSender() + + -- Check if we have a sending aircraft. + local sender=nil --Wrapper.Unit#UNIT + + -- Try the general default. + if self.sendername then + -- First try to find a unit + sender=UNIT:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() and sender:IsAir() then + return sender + end + + end + + return nil +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #RADIOQUEUE self +-- @return Core.Point#COORDINATE Coordinate of the sender unit. +function RADIOQUEUE:_GetRadioSenderCoord() + + local vec3=nil + + -- Try the general default. + if self.sendername then + + -- First try to find a unit + local sender=UNIT:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() then + return sender:GetCoordinate() + end + + -- Now try a static. + local sender=STATIC:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender then + return sender:GetCoordinate() + end + + end + + return nil +end diff --git a/Moose Development/Moose/Core/Settings.lua b/Moose Development/Moose/Core/Settings.lua index 1f504271d..fd1aca413 100644 --- a/Moose Development/Moose/Core/Settings.lua +++ b/Moose Development/Moose/Core/Settings.lua @@ -193,7 +193,20 @@ -- -- - @{#SETTINGS.SetMessageTime}(): Define for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. -- - @{#SETTINGS.GetMessageTime}(): Retrieves for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. --- +-- +-- ## 3.5) **Era** of the battle +-- +-- The threat level metric is scaled according the era of the battle. A target that is AAA, will pose a much greather threat in WWII than on modern warfare. +-- Therefore, there are 4 era that are defined within the settings: +-- +-- - **WWII** era: Use for warfare with equipment during the world war II time. +-- - **Korea** era: Use for warfare with equipment during the Korea war time. +-- - **Cold War** era: Use for warfare with equipment during the cold war time. +-- - **Modern** era: Use for warfare with modern equipment in the 2000s. +-- +-- There are different API defined that you can use with the _SETTINGS object to configure your mission script to work in one of the 4 era: +-- @{#SETTINGS.SetEraWWII}(), @{#SETTINGS.SetEraKorea}(), @{#SETTINGS.SetEraCold}(), @{#SETTINGS.SetEraModern}() +-- -- === -- -- @field #SETTINGS @@ -202,6 +215,19 @@ SETTINGS = { ShowPlayerMenu = true, } +SETTINGS.__Enum = {} + +--- @type SETTINGS.__Enum.Era +-- @field #number WWII +-- @field #number Korea +-- @field #number Cold +-- @field #number Modern +SETTINGS.__Enum.Era = { + WWII = 1, + Korea = 2, + Cold = 3, + Modern = 4, +} do -- SETTINGS @@ -223,6 +249,7 @@ do -- SETTINGS self:SetMessageTime( MESSAGE.Type.Information, 30 ) self:SetMessageTime( MESSAGE.Type.Overview, 60 ) self:SetMessageTime( MESSAGE.Type.Update, 15 ) + self:SetEraModern() return self else local Settings = _DATABASE:GetPlayerSettings( PlayerName ) @@ -838,6 +865,47 @@ do -- SETTINGS end end + + --- Configures the era of the mission to be WWII. + -- @param #SETTINGS self + -- @return #SETTINGS self + function SETTINGS:SetEraWWII() + + self.Era = SETTINGS.__Enum.Era.WWII + + end + + --- Configures the era of the mission to be Korea. + -- @param #SETTINGS self + -- @return #SETTINGS self + function SETTINGS:SetEraKorea() + + self.Era = SETTINGS.__Enum.Era.Korea + + end + + + --- Configures the era of the mission to be Cold war. + -- @param #SETTINGS self + -- @return #SETTINGS self + function SETTINGS:SetEraCold() + + self.Era = SETTINGS.__Enum.Era.Cold + + end + + + --- Configures the era of the mission to be Modern war. + -- @param #SETTINGS self + -- @return #SETTINGS self + function SETTINGS:SetEraModern() + + self.Era = SETTINGS.__Enum.Era.Modern + + end + + + end diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index a5e12f901..eb0d8f61e 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -20,6 +20,7 @@ __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/RadioQueue.lua' ) __Moose.Include( 'Scripts/Moose/Core/Spawn.lua' ) __Moose.Include( 'Scripts/Moose/Core/SpawnStatic.lua' ) __Moose.Include( 'Scripts/Moose/Core/Goal.lua' ) diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index 9d26e4d08..47824aa77 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -254,11 +254,46 @@ do -- DETECTION MANAGER end + --- Get the command center to communicate actions to the players. + -- @param #DETECTION_MANAGER self + -- @return Tasking.CommandCenter#COMMANDCENTER The command center. + function DETECTION_MANAGER:GetCommandCenter() + + return self.CC + end + + --- Set the frequency of communication and the mode of communication for voice overs. + -- @param #DETECTION_MANAGER self + -- @param #number RadioFrequency The frequency of communication. + -- @param #number RadioModulation The modulation of communication. + -- @param #number RadioPower The power in Watts of communication. + function DETECTION_MANAGER:SetRadioFrequency( RadioFrequency, RadioModulation, RadioPower ) + + self.RadioFrequency = RadioFrequency + self.RadioModulation = RadioModulation or radio.modulation.AM + self.RadioPower = RadioPower or 100 + + if self.RadioQueue then + self.RadioQueue:Stop() + end + + self.RadioQueue = nil + + self.RadioQueue = RADIOQUEUE:New( self.RadioFrequency, self.RadioModulation ) + self.RadioQueue.power = self.RadioPower + self.RadioQueue:Start( 0.5 ) + 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. + -- @param #string SoundFile The name of the sound file .wav or .ogg. + -- @param #number SoundDuration The duration of the sound. + -- @param #string SoundPath The path pointing to the folder in the mission file. + -- @param #table Squadron The squadron. + -- @param Wrapper.Unit#UNIT Defender The defender sending the message. -- @return #DETECTION_MANGER self - function DETECTION_MANAGER:MessageToPlayers( Message ) + function DETECTION_MANAGER:MessageToPlayers( Message, SoundFile, SoundDuration, SoundPath, Squadron, Defender ) self:F( { Message = Message } ) @@ -269,6 +304,19 @@ do -- DETECTION MANAGER end end + -- Here we handle the transmission of the voice over. + -- If for a certain reason the Defender does not exist, we use the coordinate of the airbase to send the message from. + if SoundFile then + local RadioQueue = self.RadioQueue -- Core.RadioQueue#RADIOQUEUE + if Defender and Defender:IsAlive() then + RadioQueue:SetSenderUnitName( Defender:GetName() ) + else + -- Use the airbase of the squadron as the coordinate of send. + RadioQueue:SetSenderCoordinate( Squadron.Airbase:GetCoordinate() ) + end + RadioQueue:NewTransmission( SoundFile, SoundDuration, SoundPath ) + end + return self end diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 6e28f3323..6987e5ad5 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -696,7 +696,9 @@ end --- Returns the Unit's A2G threat level on a scale from 1 to 10 ... --- The following threat levels are foreseen: +-- Depending on the era and the type of unit, the following threat levels are foreseen: +-- +-- **Modern**: -- -- * Threat level 0: Unit is unarmed. -- * Threat level 1: Unit is infantry. @@ -709,13 +711,49 @@ end -- * Threat level 8: Unit is a Short Range SAM, radar guided. -- * Threat level 9: Unit is a Medium Range SAM, radar guided. -- * Threat level 10: Unit is a Long Range SAM, radar guided. +-- +-- **Cold**: +-- +-- * Threat level 0: Unit is unarmed. +-- * Threat level 1: Unit is infantry. +-- * Threat level 2: Unit is an infantry vehicle. +-- * Threat level 3: Unit is ground artillery. +-- * Threat level 4: Unit is a tank. +-- * Threat level 5: Unit is a modern tank or ifv with ATGM. +-- * Threat level 6: Unit is a AAA. +-- * Threat level 7: Unit is a SAM or manpad, IR guided. +-- * Threat level 8: Unit is a Short Range SAM, radar guided. +-- * Threat level 10: Unit is a Medium Range SAM, radar guided. +-- +-- **Korea**: +-- +-- * Threat level 0: Unit is unarmed. +-- * Threat level 1: Unit is infantry. +-- * Threat level 2: Unit is an infantry vehicle. +-- * Threat level 3: Unit is ground artillery. +-- * Threat level 5: Unit is a tank. +-- * Threat level 6: Unit is a AAA. +-- * Threat level 7: Unit is a SAM or manpad, IR guided. +-- * Threat level 10: Unit is a Short Range SAM, radar guided. +-- +-- **WWII**: +-- +-- * Threat level 0: Unit is unarmed. +-- * Threat level 1: Unit is infantry. +-- * Threat level 2: Unit is an infantry vehicle. +-- * Threat level 3: Unit is ground artillery. +-- * Threat level 5: Unit is a tank. +-- * Threat level 7: Unit is FLAK. +-- * Threat level 10: Unit is AAA. +-- +-- -- @param #UNIT self function UNIT:GetThreatLevel() local ThreatLevel = 0 local ThreatText = "" - + local Descriptor = self:GetDesc() if Descriptor then From 1fd9cbec1f452b03941211af33f54391946cbf11 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 9 Sep 2019 11:17:45 +0200 Subject: [PATCH 363/485] - Added A2G voice overs to some of the basic events during defender flight. More to come, like multiple languages, and also more voice overs concerning some of the more detailed events, like: - Damage - Firing - Enemy location - Callsigns - Numbers for distance and degrees. --- .../Moose/AI/AI_A2G_Dispatcher.lua | 255 ++++++++++-------- Moose Development/Moose/Core/Database.lua | 1 + .../Moose/Tasking/DetectionManager.lua | 13 +- 3 files changed, 145 insertions(+), 124 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index e03bb740a..0d74a86d2 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3496,95 +3496,123 @@ 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( 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() + local AI_A2G_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 ) + AI_A2G_Fsm:SetDispatcher( self ) + AI_A2G_Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + AI_A2G_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + AI_A2G_Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + AI_A2G_Fsm:SetDisengageRadius( self.DisengageRadius ) + AI_A2G_Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) + AI_A2G_Fsm:Start() - self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, Fsm, nil, DefenderGrouping ) + self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, AI_A2G_Fsm, nil, DefenderGrouping ) - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"Defender Takeoff", Defender:GetName()}) + function AI_A2G_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + self:F({"Defender Takeoff", DefenderGroup:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - local DefenderName = Defender:GetCallsign() - local DefenderUnitName = Defender:GetName() - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne.", "Wheels_up.wav", 3, "A2G/", Squadron, Defender ) - Fsm:Patrol() -- Engage on the TargetSetUnit + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne.", "Wheels_up.wav", 3, "A2G/", DefenderGroup ) + AI_A2G_Fsm:Patrol() -- Engage on the TargetSetUnit end end - function Fsm:onafterPatrolRoute( Defender, From, Event, To ) - self:F({"Defender PatrolRoute", Defender:GetName()}) - self:GetParent(self).onafterPatrolRoute( self, Defender, From, Event, To ) + function AI_A2G_Fsm:onafterPatrolRoute( DefenderGroup, From, Event, To ) + self:F({"Defender PatrolRoute", DefenderGroup:GetName()}) + self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To ) - local DefenderName = Defender:GetCallsign() - local DefenderUnitName = Defender:GetName() + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " patrolling.", "Patrolling.wav", 3, "A2G/", Squadron, Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " patrolling.", "Patrolling.wav", 3, "A2G/", DefenderGroup ) end - Dispatcher:ClearDefenderTaskTarget( Defender ) + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"Defender RTB", Defender:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + function AI_A2G_Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", DefenderGroup:GetName()}) - local DefenderName = Defender:GetCallsign() - local DefenderUnitName = Defender:GetName() - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning.", "Returning_to_base.wav", 3, "A2G/", Squadron, Defender ) + self:GetParent(self).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron then + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), "Moving_on_to_ground_target.wav", 3, "A2G/", DefenderGroup ) + end + end - Dispatcher:ClearDefenderTaskTarget( Defender ) + function AI_A2G_Fsm:OnAfterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + local FirstUnit = AttackSetUnit:GetFirst() + if FirstUnit then + local Coordinate = FirstUnit:GetCoordinate() + + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), "Engaging_ground_target.wav", 3, "A2G/", DefenderGroup ) + end + end + + function AI_A2G_Fsm:onafterRTB( DefenderGroup, From, Event, To ) + self:F({"Defender RTB", DefenderGroup:GetName()}) + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning.", "Returning_to_base.wav", 3, "A2G/", DefenderGroup ) + + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) 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 ) + function AI_A2G_Fsm:onafterLostControl( DefenderGroup, From, Event, To ) + self:F({"Defender LostControl", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) - local DefenderName = Defender:GetCallsign() - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) - if Defender:IsAboveRunway() then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() + if DefenderGroup:IsAboveRunway() then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup: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 ) + function AI_A2G_Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) + self:F({"Defender Home", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) - local DefenderName = Defender:GetCallsign() - local DefenderUnitName = Defender:GetName() + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing.", "Landing_at_base.wav", 3, "A2G/", Squadron, Defender ) + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing.", "Landing_at_base.wav", 3, "A2G/", DefenderGroup ) if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() end if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - Dispatcher:ResourcePark( Squadron, Defender ) + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + Dispatcher:ResourcePark( Squadron, DefenderGroup ) end end end @@ -3609,117 +3637,112 @@ do -- AI_A2G_DISPATCHER 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() + local AI_A2G_Fsm = AI_A2G[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude ) -- AI.AI_A2G_ENGAGE + AI_A2G_Fsm:SetDispatcher( self ) + AI_A2G_Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + AI_A2G_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + AI_A2G_Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + AI_A2G_Fsm:SetDisengageRadius( self.DisengageRadius ) + AI_A2G_Fsm:Start() - self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, Fsm, AttackerDetection, DefenderGrouping ) + self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, AI_A2G_Fsm, AttackerDetection, DefenderGrouping ) - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"Defender Birth", Defender:GetName()}) + function AI_A2G_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + self:F({"Defender Birth", DefenderGroup:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - local DefenderName = Defender:GetCallsign() - local DefenderUnitName = Defender:GetName() - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup ) self:F( { DefenderTarget = DefenderTarget } ) if DefenderTarget then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne. Engaging!", "Wheels_up.wav", 3, "A2G/", Squadron, Defender ) - Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne. Engaging!", "Wheels_up.wav", 3, "A2G/", DefenderGroup ) + AI_A2G_Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end - function Fsm:onafterEngageRoute( Defender, From, Event, To, AttackSetUnit ) - self:F({"Engage Route", Defender:GetName()}) + function AI_A2G_Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", DefenderGroup:GetName()}) - local DefenderName = Defender:GetCallsign() - local DefenderUnitName = Defender:GetName() - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route to ground target at " .. Coordinate:ToStringA2G( Defender ), "Moving_on_to_ground_target.wav", 3, "A2G/", Squadron, Defender ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), "Moving_on_to_ground_target.wav", 3, "A2G/", DefenderGroup ) end - self:GetParent(self).onafterEngageRoute( self, Defender, From, Event, To, AttackSetUnit ) + self:GetParent(self).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) end - function Fsm:OnAfterEngage( Defender, From, Event, To, AttackSetUnit ) - self:F({"Engage Route", Defender:GetName()}) + function AI_A2G_Fsm:OnAfterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", DefenderGroup:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - local DefenderName = Defender:GetCallsign() - local DefenderUnitName = Defender:GetName() - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) local FirstUnit = AttackSetUnit:GetFirst() if FirstUnit then local Coordinate = FirstUnit:GetCoordinate() - Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging ground target at " .. Coordinate:ToStringA2G( Defender ), "Engaging_ground_target.wav", 3, "A2G/", DefenderUnitName ) + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), "Engaging_ground_target.wav", 3, "A2G/", DefenderGroup ) end end - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"Defender RTB", Defender:GetName()}) + function AI_A2G_Fsm:onafterRTB( DefenderGroup, From, Event, To ) + self:F({"Defender RTB", DefenderGroup:GetName()}) - local DefenderName = Defender:GetCallsign() - local DefenderUnitName = Defender:GetName() + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning.", "Returning_to_base.wav", 3, "A2G/", Squadron, Defender ) + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning.", "Returning_to_base.wav", 3, "A2G/", DefenderGroup ) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) - Dispatcher:ClearDefenderTaskTarget( Defender ) + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) 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 ) + function AI_A2G_Fsm:onafterLostControl( DefenderGroup, From, Event, To ) + self:F({"Defender LostControl", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) - local DefenderName = Defender:GetCallsign() - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) --Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) - if Defender:IsAboveRunway() then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() + if DefenderGroup:IsAboveRunway() then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup: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 ) + function AI_A2G_Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) + self:F({"Defender Home", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) - local DefenderName = Defender:GetCallsign() - local DefenderUnitName = Defender:GetName() + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing.", "Landing_at_base.wav", 3, "A2G/", Squadron, Defender ) + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing.", "Landing_at_base.wav", 3, "A2G/", DefenderGroup ) if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() end if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - Dispatcher:ResourcePark( Squadron, Defender ) + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + Dispatcher:ResourcePark( Squadron, DefenderGroup ) end end end diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 3a4dccefd..4463defbc 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -187,6 +187,7 @@ end function DATABASE:AddUnit( DCSUnitName ) if not self.UNITS[DCSUnitName] then + self:I( { "Add UNIT:", DCSUnitName } ) local UnitRegister = UNIT:Register( DCSUnitName ) self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index 47824aa77..8bec92065 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -290,10 +290,9 @@ do -- DETECTION MANAGER -- @param #string SoundFile The name of the sound file .wav or .ogg. -- @param #number SoundDuration The duration of the sound. -- @param #string SoundPath The path pointing to the folder in the mission file. - -- @param #table Squadron The squadron. - -- @param Wrapper.Unit#UNIT Defender The defender sending the message. + -- @param Wrapper.Group#GROUP DefenderGroup The defender group sending the message. -- @return #DETECTION_MANGER self - function DETECTION_MANAGER:MessageToPlayers( Message, SoundFile, SoundDuration, SoundPath, Squadron, Defender ) + function DETECTION_MANAGER:MessageToPlayers( Message, SoundFile, SoundDuration, SoundPath, DefenderGroup ) self:F( { Message = Message } ) @@ -308,11 +307,9 @@ do -- DETECTION MANAGER -- If for a certain reason the Defender does not exist, we use the coordinate of the airbase to send the message from. if SoundFile then local RadioQueue = self.RadioQueue -- Core.RadioQueue#RADIOQUEUE - if Defender and Defender:IsAlive() then - RadioQueue:SetSenderUnitName( Defender:GetName() ) - else - -- Use the airbase of the squadron as the coordinate of send. - RadioQueue:SetSenderCoordinate( Squadron.Airbase:GetCoordinate() ) + local DefenderUnit = DefenderGroup:GetUnit(1) + if DefenderUnit and DefenderUnit:IsAlive() then + RadioQueue:SetSenderUnitName( DefenderUnit:GetName() ) end RadioQueue:NewTransmission( SoundFile, SoundDuration, SoundPath ) end From e23ebd622a472022432492f62cc625b7e85fed48 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 9 Sep 2019 20:24:51 +0300 Subject: [PATCH 364/485] - Fixing repetition of Patrolling in A2G dispatcher. --- Moose Development/Moose/AI/AI_A2G_Patrol.lua | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua index 8c7ae67a9..68e1954ca 100644 --- a/Moose Development/Moose/AI/AI_A2G_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -286,9 +286,18 @@ function AI_A2G_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) self:SetTargetDistance( ToTargetCoord ) -- For RTB status check local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + + local FromWP = CurrentCoord:WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + PatrolRoute[#PatrolRoute+1] = FromWP --- Create a route point of type air. - local ToPatrolRoutePoint = ToTargetCoord:WaypointAir( + local ToWP = ToTargetCoord:WaypointAir( self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, @@ -296,8 +305,7 @@ function AI_A2G_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) true ) - PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint - PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint + PatrolRoute[#PatrolRoute+1] = ToWP local Tasks = {} Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2G_PATROL.___PatrolRoute", self ) From 2d4a4fb2ca7f060eeffb02a04bee8d36ed820bd3 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 13 Sep 2019 18:57:27 +0200 Subject: [PATCH 365/485] Update ScheduleDispatcher.lua Fix for Source (=Infor.source) nil problem. --- Moose Development/Moose/Core/ScheduleDispatcher.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index ec03bc93b..7cb22b364 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -127,8 +127,8 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr local CallID = Params.CallID local Info = Params.Info - local Source = Info.source - local Line = Info.currentline + local Source = Info.source or "unknown source" + local Line = Info.currentline or -1 local Name = Info.name or "?" local ErrorHandler = function( errmsg ) From d51fc5de8a398784a0c2bfba03fcfdfbb7608785 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 13 Sep 2019 19:37:13 +0200 Subject: [PATCH 366/485] Update ScheduleDispatcher.lua better fix --- Moose Development/Moose/Core/ScheduleDispatcher.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 7cb22b364..b87b0685c 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -127,8 +127,8 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr local CallID = Params.CallID local Info = Params.Info - local Source = Info.source or "unknown source" - local Line = Info.currentline or -1 + local Source = Info.source or "?" + local Line = Info.currentline or "?" local Name = Info.name or "?" local ErrorHandler = function( errmsg ) From b309c25619cf7427faa81ce59e1ba2931e7bfa45 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 15 Sep 2019 18:02:45 +0200 Subject: [PATCH 367/485] Speech Engine Prototype --- .../Moose/AI/AI_A2G_Dispatcher.lua | 26 +- Moose Development/Moose/Core/RadioSpeech.lua | 310 ++++++++++++++++++ Moose Development/Moose/Modules.lua | 1 + .../Moose/Tasking/DetectionManager.lua | 38 ++- 4 files changed, 345 insertions(+), 30 deletions(-) create mode 100644 Moose Development/Moose/Core/RadioSpeech.lua diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 0d74a86d2..0c6450c2c 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3511,12 +3511,12 @@ do -- AI_A2G_DISPATCHER self:F({"Defender Takeoff", DefenderGroup:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - local DefenderName = DefenderGroup:GetCallsign() + local DefenderName = DefenderGroup:GetCallsign() -- #string local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne.", "Wheels_up.wav", 3, "A2G/", DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. ", wheels up.", DefenderGroup ) AI_A2G_Fsm:Patrol() -- Engage on the TargetSetUnit end end @@ -3529,7 +3529,7 @@ do -- AI_A2G_DISPATCHER local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " patrolling.", "Patrolling.wav", 3, "A2G/", DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. ", patrolling.", DefenderGroup ) end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) @@ -3548,7 +3548,7 @@ do -- AI_A2G_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), "Moving_on_to_ground_target.wav", 3, "A2G/", DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. ", moving on to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) end end @@ -3563,7 +3563,7 @@ do -- AI_A2G_DISPATCHER if FirstUnit then local Coordinate = FirstUnit:GetCoordinate() - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), "Engaging_ground_target.wav", 3, "A2G/", DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) end end @@ -3574,7 +3574,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning.", "Returning_to_base.wav", 3, "A2G/", DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. ", returning to base.", DefenderGroup ) Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end @@ -3587,7 +3587,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) + Dispatcher:MessageToPlayers( DefenderName .. ", lost control." ) if DefenderGroup:IsAboveRunway() then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) DefenderGroup:Destroy() @@ -3602,7 +3602,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing.", "Landing_at_base.wav", 3, "A2G/", DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. ", landing at base.", DefenderGroup ) if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) @@ -3659,7 +3659,7 @@ do -- AI_A2G_DISPATCHER self:F( { DefenderTarget = DefenderTarget } ) if DefenderTarget then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne. Engaging!", "Wheels_up.wav", 3, "A2G/", DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. ", wheels up.", DefenderGroup ) AI_A2G_Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end @@ -3675,7 +3675,7 @@ do -- AI_A2G_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), "Moving_on_to_ground_target.wav", 3, "A2G/", DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. ", on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) end self:GetParent(self).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) end @@ -3691,7 +3691,7 @@ do -- AI_A2G_DISPATCHER if FirstUnit then local Coordinate = FirstUnit:GetCoordinate() - Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), "Engaging_ground_target.wav", 3, "A2G/", DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) end end @@ -3701,7 +3701,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning.", "Returning_to_base.wav", 3, "A2G/", DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. ", returning to base.", DefenderGroup ) self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) @@ -3732,7 +3732,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing.", "Landing_at_base.wav", 3, "A2G/", DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. ", landing at base.", DefenderGroup ) if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) diff --git a/Moose Development/Moose/Core/RadioSpeech.lua b/Moose Development/Moose/Core/RadioSpeech.lua new file mode 100644 index 000000000..618cb5161 --- /dev/null +++ b/Moose Development/Moose/Core/RadioSpeech.lua @@ -0,0 +1,310 @@ +--- **Core** - Makes the radio talk. +-- +-- === +-- +-- ## Features: +-- +-- * Send text strings using a vocabulary that is converted in spoken language. +-- * Possiblity to implement multiple language. +-- +-- === +-- +-- ### Authors: FlightControl +-- +-- @module Core.Speech +-- @image Core_Radio.JPG + +--- Makes the radio speak. +-- +-- # RADIOSPEECH usage +-- +-- +-- @type RADIOSPEECH +-- @extends Core.RadioQueue#RADIOQUEUE +RADIOSPEECH = { + ClassName = "RADIOSPEECH", + Vocabulary = { + EN = {}, + } +} + + +RADIOSPEECH.Vocabulary.EN = { + ["1"] = { "1", 0.25 }, + ["2"] = { "2", 0.25 }, + ["3"] = { "3", 0.30 }, + ["4"] = { "4", 0.35 }, + ["5"] = { "5", 0.35 }, + ["6"] = { "6", 0.42 }, + ["7"] = { "7", 0.38 }, + ["8"] = { "8", 0.20 }, + ["9"] = { "9", 0.32 }, + ["10"] = { "10", 0.35 }, + ["11"] = { "11", 0.40 }, + ["12"] = { "12", 0.42 }, + ["13"] = { "13", 0.38 }, + ["14"] = { "14", 0.42 }, + ["15"] = { "15", 0.42 }, + ["16"] = { "16", 0.52 }, + ["17"] = { "17", 0.59 }, + ["18"] = { "18", 0.40 }, + ["19"] = { "19", 0.47 }, + ["20"] = { "20", 0.38 }, + ["30"] = { "30", 0.29 }, + ["40"] = { "40", 0.35 }, + ["50"] = { "50", 0.32 }, + ["60"] = { "60", 0.44 }, + ["70"] = { "70", 0.48 }, + ["80"] = { "80", 0.26 }, + ["90"] = { "90", 0.36 }, + ["100"] = { "100", 0.55 }, + ["1000"] = { "1000", 1 }, + + ["chevy"] = { "chevy", 0.35 }, + ["colt"] = { "colt", 0.35 }, + ["springfield"] = { "springfield", 0.65 }, + ["dodge"] = { "dodge", 0.35 }, + ["enfield"] = { "enfield", 0.5 }, + ["ford"] = { "ford", 0.32 }, + ["pontiac"] = { "pontiac", 0.55 }, + ["uzi"] = { "uzi", 0.28 }, + + ["degrees"] = { "degrees", 0.5 }, + ["�"] = { "degrees", 0.5 }, + ["kilometers"] = { "kilometers", 0.65 }, + ["km"] = { "kilometers", 0.65 }, + ["miles"] = { "miles", 0.45 }, + ["mi"] = { "miles", 0.45 }, + + ["br"] = { "br", 1.1 }, + ["bra"] = { "bra", 0.3 }, + + + ["returning to base"] = { "returning_to_base", 0.85 }, + ["moving on to ground target"] = { "moving_on_to_ground_target", 1.20 }, + ["engaging ground target"] = { "engaging_ground_target", 1.20 }, + ["wheels up"] = { "wheels_up", 0.42 }, + ["landing at base"] = { "landing at base", 0.8 }, + ["patrolling"] = { "patrolling", 0.55 }, + + ["for"] = { "for", 0.31 }, + ["and"] = { "and", 0.31 }, + ["at"] = { "at", 0.3 }, + ["dot"] = { "dot", 0.26 }, + ["defender"] = { "defender", 0.45 }, +} + + + +--- Create a new RADIOSPEECH object for a given radio frequency/modulation. +-- @param #RADIOSPEECH self +-- @param #number frequency The radio frequency in MHz. +-- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM. +-- @return #RADIOSPEECH self The RADIOSPEECH object. +function RADIOSPEECH:New(frequency, modulation) + + -- Inherit base + local self = BASE:Inherit( self, RADIOQUEUE:New( frequency, modulation ) ) -- #RADIOSPEECH + + self.Language = "EN" + + self:BuildTree() + + return self +end + + +--- Add Sentence to the Speech collection. +-- @param #RADIOSPEECH self +-- @param #string RemainingSentence The remaining sentence during recursion. +-- @param #table Speech The speech node. +-- @param #string Sentence The full sentence. +-- @param #string Data The speech data. +-- @return #RADIOSPEECH self The RADIOSPEECH object. +function RADIOSPEECH:AddSentenceToSpeech( RemainingSentence, Speech, Sentence, Data ) + + self:I( { RemainingSentence, Speech, Sentence, Data } ) + + local Token, RemainingSentence = RemainingSentence:match( "^ *(%w+)(.*)" ) + self:I( { Token = Token, RemainingSentence = RemainingSentence } ) + + -- Is there a Token? + if Token then + + -- We check if the Token is already in the Speech collection. + if not Speech[Token] then + + -- There is not yet a vocabulary registered for this. + Speech[Token] = {} + + if RemainingSentence and RemainingSentence ~= "" then + -- We use recursion to iterate through the complete Sentence, and make a chain of Tokens. + -- The last Speech node in the collection contains the Sentence and the Data to be spoken. + -- This to ensure that during the actual speech: + -- - Complete sentences are being understood. + -- - Words without speech are ignored. + -- - Incorrect sequence of words are ignored. + Speech[Token].Next = {} + self:AddSentenceToSpeech( RemainingSentence, Speech[Token].Next, Sentence, Data ) + else + -- There is no remaining sentence, so we add speech to the Sentence. + -- The recursion stops here. + Speech[Token].Sentence = Sentence + Speech[Token].Data = Data + end + end + end +end + +--- Build the tree structure based on the language words, in order to find the correct sentences and to ignore incomprehensible words. +-- @param #RADIOSPEECH self +-- @return #RADIOSPEECH self The RADIOSPEECH object. +function RADIOSPEECH:BuildTree() + + self.Speech = {} + + for Language, Sentences in pairs( self.Vocabulary ) do + self:I( { Language = Language, Sentences = Sentences }) + self.Speech[Language] = {} + for Sentence, Data in pairs( Sentences ) do + self:I( { Sentence = Sentence, Data = Data } ) + self:AddSentenceToSpeech( Sentence, self.Speech[Language], Sentence, Data ) + end + end + + self:I( { Speech = self.Speech } ) + + return self +end + +--- Speak a sentence. +-- @param #RADIOSPEECH self +-- @param #string Sentence The sentence to be spoken. +function RADIOSPEECH:SpeakWords( Sentence, Speech ) + + local Word, RemainderSentence = Sentence:match( "^[^%d%a]*(%a*)(.*)" ) + + self:I( { Word = Word, Speech = Speech[Word], RemainderSentence = RemainderSentence } ) + + if Word and Word ~= "" then + + -- Construct of words + Word = Word:lower() + if Speech[Word] then + -- The end of the sentence has been reached. Now Speech.Next should be nil, otherwise there is an error. + if Speech[Word].Next == nil then + self:I( { Sentence = Speech[Word].Sentence, Data = Speech[Word].Data } ) + self:NewTransmission( Speech[Word].Data[1] .. ".wav", Speech[Word].Data[2], "EN/" ) + else + if RemainderSentence and RemainderSentence ~= "" then + RemainderSentence = self:SpeakWords( RemainderSentence, Speech[Word].Next ) + end + end + end + end + + return RemainderSentence + +end + +--- Speak a sentence. +-- @param #RADIOSPEECH self +-- @param #string Sentence The sentence to be spoken. +function RADIOSPEECH:SpeakDigits( Sentence, Speech ) + + local Digits, RemainderSentence = Sentence:match( "^[^%a%d]*(%d*)(.*)" ) + + self:I( { Digits = Digits, Speech = Speech[Digits], RemainderSentence = RemainderSentence } ) + + if Digits and Digits ~= "" then + + -- Construct numbers + local Number = tonumber( Digits ) + local Multiple = nil + while Number >= 0 do + if Number > 1000 then + Multiple = math.floor( Number / 1000 ) * 1000 + elseif Number > 100 then + Multiple = math.floor( Number / 100 ) * 100 + elseif Number > 20 then + Multiple = math.floor( Number / 10 ) * 10 + elseif Number >= 0 then + Multiple = Number + end + Sentence = tostring( Multiple ) + if Speech[Sentence] then + self:I( { Speech = Speech[Sentence].Sentence, Data = Speech[Sentence].Data } ) + self:NewTransmission( Speech[Sentence].Data[1] .. ".wav", Speech[Sentence].Data[2], "EN/" ) + end + Number = Number - Multiple + Number = ( Number == 0 ) and -1 or Number + end + end + + return RemainderSentence +end + + +--- Speak a sentence. +-- @param #RADIOSPEECH self +-- @param #string Sentence The sentence to be spoken. +function RADIOSPEECH:SpeakSymbols( Sentence, Speech ) + + local Symbol, RemainderSentence = Sentence:match( "^[^%a%d]*(°*)(.*)" ) + + self:I( { Sentence = Sentence, Symbol = Symbol, Speech = Speech[Symbol], RemainderSentence = RemainderSentence } ) + + if Symbol and Symbol ~= "" then + local Word = nil + if Symbol == "°" then + Word = "degrees" + end + if Word then + if Speech[Word] then + self:I( { Speech = Speech[Word].Sentence, Data = Speech[Word].Data } ) + self:NewTransmission( Speech[Word].Data[1] .. ".wav", Speech[Word].Data[2], "EN/" ) + end + end + end + + return RemainderSentence +end + + +--- Speak a sentence. +-- @param #RADIOSPEECH self +-- @param #string Sentence The sentence to be spoken. +function RADIOSPEECH:Speak( Sentence, Speech ) + + self:I( { Sentence, Speech } ) + + local Language = self.Language + + -- If there is no node for Speech, then we start at the first nodes of the language. + if not Speech then + Speech = self.Speech[Language] + end + + self:I( { Speech = Speech, Language = Language } ) + + self:NewTransmission( "_In.wav", 0.52, "EN/" ) + + repeat + + Sentence = self:SpeakWords( Sentence, Speech ) + + self:I( { Sentence = Sentence } ) + + Sentence = self:SpeakDigits( Sentence, Speech ) + + self:I( { Sentence = Sentence } ) + +-- Sentence = self:SpeakSymbols( Sentence, Speech ) +-- +-- self:I( { Sentence = Sentence } ) + + until not Sentence or Sentence == "" + + self:NewTransmission( "_Out.wav", 0.28, "EN/" ) + +end diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index eb0d8f61e..0a499fe25 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -21,6 +21,7 @@ __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/RadioQueue.lua' ) +__Moose.Include( 'Scripts/Moose/Core/RadioSpeech.lua' ) __Moose.Include( 'Scripts/Moose/Core/Spawn.lua' ) __Moose.Include( 'Scripts/Moose/Core/SpawnStatic.lua' ) __Moose.Include( 'Scripts/Moose/Core/Goal.lua' ) diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index 8bec92065..de21616bd 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -225,7 +225,6 @@ do -- DETECTION MANAGER --- 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:SetTacticalMenu( DispatcherMainMenuText, DispatcherMenuText ) @@ -279,7 +278,7 @@ do -- DETECTION MANAGER self.RadioQueue = nil - self.RadioQueue = RADIOQUEUE:New( self.RadioFrequency, self.RadioModulation ) + self.RadioQueue = RADIOSPEECH:New( self.RadioFrequency, self.RadioModulation ) self.RadioQueue.power = self.RadioPower self.RadioQueue:Start( 0.5 ) end @@ -292,27 +291,32 @@ do -- DETECTION MANAGER -- @param #string SoundPath The path pointing to the folder in the mission file. -- @param Wrapper.Group#GROUP DefenderGroup The defender group sending the message. -- @return #DETECTION_MANGER self - function DETECTION_MANAGER:MessageToPlayers( Message, SoundFile, SoundDuration, SoundPath, DefenderGroup ) + function DETECTION_MANAGER:MessageToPlayers( Message, DefenderGroup ) self:F( { Message = Message } ) - if not self.PreviousMessage or self.PreviousMessage ~= Message then - self.PreviousMessage = Message - if self.CC then - self.CC:MessageToCoalition( Message ) - end +-- if not self.PreviousMessage or self.PreviousMessage ~= Message then +-- self.PreviousMessage = Message +-- if self.CC then +-- self.CC:MessageToCoalition( Message ) +-- end +-- end + + if self.CC then + self.CC:MessageToCoalition( Message ) end - -- Here we handle the transmission of the voice over. - -- If for a certain reason the Defender does not exist, we use the coordinate of the airbase to send the message from. - if SoundFile then - local RadioQueue = self.RadioQueue -- Core.RadioQueue#RADIOQUEUE - local DefenderUnit = DefenderGroup:GetUnit(1) - if DefenderUnit and DefenderUnit:IsAlive() then - RadioQueue:SetSenderUnitName( DefenderUnit:GetName() ) - end - RadioQueue:NewTransmission( SoundFile, SoundDuration, SoundPath ) + Message = Message:gsub( "°", "degrees" ) + Message = Message:gsub( "(%d)%.(%d)", "%1dot%2" ) + + -- Here we handle the transmission of the voice over. + -- If for a certain reason the Defender does not exist, we use the coordinate of the airbase to send the message from. + local RadioQueue = self.RadioQueue -- Core.RadioQueue#RADIOQUEUE + local DefenderUnit = DefenderGroup:GetUnit(1) + if DefenderUnit and DefenderUnit:IsAlive() then + RadioQueue:SetSenderUnitName( DefenderUnit:GetName() ) end + RadioQueue:Speak( Message ) return self end From 8c54b7298d28b55a5586286f223cfc27c7a1c828 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 15 Sep 2019 18:36:27 +0200 Subject: [PATCH 368/485] Tuning A2A AI dispatcher to work with new sound speech system. --- .../Moose/AI/AI_A2A_Dispatcher.lua | 70 +++++++++---------- .../Moose/Tasking/DetectionManager.lua | 10 +-- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index eb7c8d004..cf59ef0fa 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -3100,31 +3100,31 @@ do -- AI_A2A_DISPATCHER self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", Fsm ) - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"CAP Birth", Defender:GetName()}) + function Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + self:F({"CAP Birth", DefenderGroup:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - local DefenderName = Defender:GetCallsign() + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne. Starting Patrol." ) + Dispatcher:MessageToPlayers( DefenderName .. " Wheels up.", DefenderGroup ) Fsm:__Patrol( 2 ) -- Start Patrolling end end - function Fsm:onafterRTB( Defender, From, Event, To ) + function Fsm:onafterRTB( DefenderGroup, From, Event, To ) - self:F({"CAP RTB", Defender:GetName()}) + self:F({"CAP RTB", DefenderGroup:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) - local DefenderName = Defender:GetCallsign() + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) - Dispatcher:ClearDefenderTaskTarget( Defender ) + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. " returning to base.", DefenderGroup ) + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end --- @param #AI_A2A_DISPATCHER self @@ -3295,30 +3295,30 @@ do -- AI_A2A_DISPATCHER self:SetDefenderTask( ClosestDefenderSquadronName, DefenderGCI, "GCI", Fsm, AttackerDetection ) - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"GCI Birth", Defender:GetName()}) + function Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + self:F({"GCI Birth", DefenderGroup:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - local DefenderName = Defender:GetCallsign() + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup ) if DefenderTarget then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne. Engaging target!" ) + Dispatcher:MessageToPlayers( DefenderName .. " wheels up.", DefenderGroup ) Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit end end - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"GCI RTB", Defender:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + function Fsm:onafterRTB( DefenderGroup, From, Event, To ) + self:F({"GCI RTB", DefenderGroup:GetName()}) + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) - local DefenderName = Defender:GetCallsign() + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) - Dispatcher:ClearDefenderTaskTarget( Defender ) + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. " returning to base.", DefenderGroup ) + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end --- @param #AI_A2A_DISPATCHER self @@ -3335,24 +3335,24 @@ do -- AI_A2A_DISPATCHER end --- @param #AI_A2A_DISPATCHER self - function Fsm:onafterHome( Defender, From, Event, To, Action ) - self:F({"GCI Home", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + function Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) + self:F({"GCI Home", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) - local DefenderName = Defender:GetCallsign() + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) + Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing at base.", DefenderGroup ) if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() end if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() Dispatcher:ParkDefender( Squadron ) end end diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index de21616bd..e40121654 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -312,11 +312,13 @@ do -- DETECTION MANAGER -- Here we handle the transmission of the voice over. -- If for a certain reason the Defender does not exist, we use the coordinate of the airbase to send the message from. local RadioQueue = self.RadioQueue -- Core.RadioQueue#RADIOQUEUE - local DefenderUnit = DefenderGroup:GetUnit(1) - if DefenderUnit and DefenderUnit:IsAlive() then - RadioQueue:SetSenderUnitName( DefenderUnit:GetName() ) + if RadioQueue then + local DefenderUnit = DefenderGroup:GetUnit(1) + if DefenderUnit and DefenderUnit:IsAlive() then + RadioQueue:SetSenderUnitName( DefenderUnit:GetName() ) + end + RadioQueue:Speak( Message ) end - RadioQueue:Speak( Message ) return self end From c579aad6060130e8d9b79b1785fbfc6dc6db452f Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 22 Sep 2019 08:23:28 +0200 Subject: [PATCH 369/485] API V2 of the A2A and A2G dispatchers. Now a more fine grained attack is possible. You can now tweak the altitude and speed when units engage the target. Especially useful for ground attacks when SEAD, CAS or BAI.You can model low altitude approaches. Added 2 new test missions under the IAD - A2G API v2. Please report any bugs. --- Moose Development/Moose/AI/AI_A2A.lua | 737 ------------------ Moose Development/Moose/AI/AI_A2A_Cap.lua | 379 ++------- .../Moose/AI/AI_A2A_Dispatcher.lua | 178 ++++- Moose Development/Moose/AI/AI_A2A_Gci.lua | 372 +-------- Moose Development/Moose/AI/AI_A2A_Patrol.lua | 49 +- Moose Development/Moose/AI/AI_A2G.lua | 69 -- Moose Development/Moose/AI/AI_A2G_BAI.lua | 149 +--- Moose Development/Moose/AI/AI_A2G_CAS.lua | 148 ++-- .../Moose/AI/AI_A2G_Dispatcher.lua | 342 +++++--- Moose Development/Moose/AI/AI_A2G_SEAD.lua | 158 ++-- .../Moose/AI/AI_Air_Dispatcher.lua | 2 +- .../{AI_A2G_Engage.lua => AI_Air_Engage.lua} | 346 +++++--- .../{AI_A2G_Patrol.lua => AI_Air_Patrol.lua} | 234 ++++-- Moose Development/Moose/Core/RadioSpeech.lua | 11 +- Moose Development/Moose/Modules.lua | 6 +- 15 files changed, 1012 insertions(+), 2168 deletions(-) delete mode 100644 Moose Development/Moose/AI/AI_A2A.lua delete mode 100644 Moose Development/Moose/AI/AI_A2G.lua rename Moose Development/Moose/AI/{AI_A2G_Engage.lua => AI_Air_Engage.lua} (64%) rename Moose Development/Moose/AI/{AI_A2G_Patrol.lua => AI_Air_Patrol.lua} (56%) diff --git a/Moose Development/Moose/AI/AI_A2A.lua b/Moose Development/Moose/AI/AI_A2A.lua deleted file mode 100644 index f23fc006e..000000000 --- a/Moose Development/Moose/AI/AI_A2A.lua +++ /dev/null @@ -1,737 +0,0 @@ ---- **AI** -- (R2.2) - Models the process of air operations for airplanes. --- --- === --- --- ### Author: **FlightControl** --- --- === --- --- @module AI.AI_A2A --- @image MOOSE.JPG - ---BASE:TraceClass("AI_A2A") - - ---- @type AI_A2A --- @extends Core.Fsm#FSM_CONTROLLABLE - ---- The AI_A2A class implements the core functions to operate an AI @{Wrapper.Group} A2A tasking. --- --- --- ## AI_A2A constructor --- --- * @{#AI_A2A.New}(): Creates a new AI_A2A object. --- --- ## 2. AI_A2A is a FSM --- --- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) --- --- ### 2.1. AI_A2A States --- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Returning** ( Group ): The AI is returning to Base. --- * **Stopped** ( Group ): The process is stopped. --- * **Crashed** ( Group ): The AI has crashed or is dead. --- --- ### 2.2. AI_A2A Events --- --- * **Start** ( Group ): Start the process. --- * **Stop** ( Group ): Stop the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 3. Set or Get the AI controllable --- --- * @{#AI_A2A.SetControllable}(): Set the AIControllable. --- * @{#AI_A2A.GetControllable}(): Get the AIControllable. --- --- @field #AI_A2A -AI_A2A = { - ClassName = "AI_A2A", -} - ---- Creates a new AI_A2A object --- @param #AI_A2A self --- @param Wrapper.Group#GROUP AIGroup The GROUP object to receive the A2A Process. --- @return #AI_A2A -function AI_A2A:New( AIGroup ) - - -- Inherits from BASE - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_A2A - - self:SetControllable( AIGroup ) - - self:SetFuelThreshold( .2, 60 ) - self:SetDamageThreshold( 0.4 ) - self:SetDisengageRadius( 70000 ) - - self:SetStartState( "Stopped" ) - - self:AddTransition( "*", "Start", "Started" ) - - --- Start Handler OnBefore for AI_A2A - -- @function [parent=#AI_A2A] OnBeforeStart - -- @param #AI_A2A self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @return #boolean - - --- Start Handler OnAfter for AI_A2A - -- @function [parent=#AI_A2A] OnAfterStart - -- @param #AI_A2A self - -- @param #string From - -- @param #string Event - -- @param #string To - - --- Start Trigger for AI_A2A - -- @function [parent=#AI_A2A] Start - -- @param #AI_A2A self - - --- Start Asynchronous Trigger for AI_A2A - -- @function [parent=#AI_A2A] __Start - -- @param #AI_A2A self - -- @param #number Delay - - self:AddTransition( "*", "Stop", "Stopped" ) - ---- OnLeave Transition Handler for State Stopped. --- @function [parent=#AI_A2A] OnLeaveStopped --- @param #AI_A2A 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_A2A] OnEnterStopped --- @param #AI_A2A 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_A2A] OnBeforeStop --- @param #AI_A2A 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_A2A] OnAfterStop --- @param #AI_A2A 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_A2A] Stop --- @param #AI_A2A self - ---- Asynchronous Event Trigger for Event Stop. --- @function [parent=#AI_A2A] __Stop --- @param #AI_A2A self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A. - ---- OnBefore Transition Handler for Event Status. --- @function [parent=#AI_A2A] OnBeforeStatus --- @param #AI_A2A 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_A2A] OnAfterStatus --- @param #AI_A2A 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_A2A] Status --- @param #AI_A2A self - ---- Asynchronous Event Trigger for Event Status. --- @function [parent=#AI_A2A] __Status --- @param #AI_A2A self --- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "RTB", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A. - ---- OnBefore Transition Handler for Event RTB. --- @function [parent=#AI_A2A] OnBeforeRTB --- @param #AI_A2A 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_A2A] OnAfterRTB --- @param #AI_A2A 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_A2A] RTB --- @param #AI_A2A self - ---- Asynchronous Event Trigger for Event RTB. --- @function [parent=#AI_A2A] __RTB --- @param #AI_A2A self --- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Returning. --- @function [parent=#AI_A2A] OnLeaveReturning --- @param #AI_A2A 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_A2A] OnEnterReturning --- @param #AI_A2A 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_A2A - -- @function [parent=#AI_A2A] OnBeforeRefuel - -- @param #AI_A2A 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_A2A - -- @function [parent=#AI_A2A] OnAfterRefuel - -- @param #AI_A2A 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_A2A - -- @function [parent=#AI_A2A] Refuel - -- @param #AI_A2A self - - --- Refuel Asynchronous Trigger for AI_A2A - -- @function [parent=#AI_A2A] __Refuel - -- @param #AI_A2A 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_A2A:SetDispatcher( Dispatcher ) - self.Dispatcher = Dispatcher -end - -function AI_A2A:GetDispatcher() - return self.Dispatcher -end - -function AI_A2A: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_A2A:ClearTargetDistance() - - self.TargetDistance = nil - self.ClosestTargetDistance = nil -end - - ---- Sets (modifies) the minimum and maximum speed of the patrol. --- @param #AI_A2A 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_A2A self -function AI_A2A: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_A2A 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_A2A self -function AI_A2A:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) - self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) - - self.PatrolFloorAltitude = PatrolFloorAltitude - self.PatrolCeilingAltitude = PatrolCeilingAltitude -end - - ---- Sets the home airbase. --- @param #AI_A2A self --- @param Wrapper.Airbase#AIRBASE HomeAirbase --- @return #AI_A2A self -function AI_A2A:SetHomeAirbase( HomeAirbase ) - self:F2( { HomeAirbase } ) - - self.HomeAirbase = HomeAirbase -end - ---- Sets to refuel at the given tanker. --- @param #AI_A2A self --- @param Wrapper.Group#GROUP TankerName The group name of the tanker as defined within the Mission Editor or spawned. --- @return #AI_A2A self -function AI_A2A: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_A2A self --- @param #number DisengageRadius The disengage range. --- @return #AI_A2A self -function AI_A2A:SetDisengageRadius( DisengageRadius ) - self:F2( { DisengageRadius } ) - - self.DisengageRadius = DisengageRadius -end - ---- Set the status checking off. --- @param #AI_A2A self --- @return #AI_A2A self -function AI_A2A: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_A2A. --- Once the time is finished, the old AI will return to the base. --- @param #AI_A2A self --- @param #number PatrolFuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. --- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. --- @return #AI_A2A self -function AI_A2A:SetFuelThreshold( PatrolFuelThresholdPercentage, PatrolOutOfFuelOrbitTime ) - - self.PatrolFuelThresholdPercentage = PatrolFuelThresholdPercentage - self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime - - 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_A2A self --- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. --- @return #AI_A2A self -function AI_A2A: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_A2A self --- @return #AI_A2A 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_A2A: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_A2A self -function AI_A2A:onbeforeStatus() - - return self.CheckStatus -end - ---- @param #AI_A2A self -function AI_A2A: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:I( 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, PatrolFuelThresholdPercentage=self.PatrolFuelThresholdPercentage}) - if Fuel < self.PatrolFuelThresholdPercentage then - if self.TankerName then - self:I( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) - self:Refuel() - else - self:I( 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.PatrolOutOfFuelOrbitTime,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:I( 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 >= 3 then - if Damage ~= InitialLife then - self:Damaged() - else - self:I( 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_A2A.RTBRoute( AIGroup, Fsm ) - - AIGroup:F( { "AI_A2A.RTBRoute:", AIGroup:GetName() } ) - - if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) - end - -end - ---- @param Wrapper.Group#GROUP AIGroup -function AI_A2A.RTBHold( AIGroup, Fsm ) - - AIGroup:F( { "AI_A2A.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_A2A self --- @param Wrapper.Group#GROUP AIGroup -function AI_A2A:onafterRTB( AIGroup, From, Event, To ) - self:F( { AIGroup, From, Event, To } ) - - - if AIGroup and AIGroup:IsAlive() then - - self:I( "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:I( "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_A2A.RTBRoute", self ) - EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) - - --- NOW ROUTE THE GROUP! - AIGroup:Route( EngageRoute, 0.5 ) - - end - -end - ---- @param #AI_A2A self --- @param Wrapper.Group#GROUP AIGroup -function AI_A2A:onafterHome( AIGroup, From, Event, To ) - self:F( { AIGroup, From, Event, To } ) - - self:I( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) - - if AIGroup and AIGroup:IsAlive() then - end - -end - - - ---- @param #AI_A2A self --- @param Wrapper.Group#GROUP AIGroup -function AI_A2A:onafterHold( AIGroup, From, Event, To, HoldTime ) - self:F( { AIGroup, From, Event, To } ) - - self:I( "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_A2A.RTBHold", self ) - - local OrbitHoldTask = AIGroup:TaskOrbitCircle( 4000, self.PatrolMinSpeed ) - - --AIGroup:SetState( AIGroup, "AI_A2A", self ) - - AIGroup:SetTask( AIGroup:TaskCombo( { TimedOrbitTask, RTBTask, OrbitHoldTask } ), 1 ) - end - -end - ---- @param Wrapper.Group#GROUP AIGroup -function AI_A2A.Resume( AIGroup, Fsm ) - - AIGroup:I( { "AI_A2A.Resume:", AIGroup:GetName() } ) - if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) - end - -end - ---- @param #AI_A2A self --- @param Wrapper.Group#GROUP AIGroup -function AI_A2A:onafterRefuel( AIGroup, From, Event, To ) - self:F( { AIGroup, From, Event, To } ) - - self:I( "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 = CurrentCoord: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_A2A self -function AI_A2A:onafterDead() - self:SetStatusOff() -end - - ---- @param #AI_A2A self --- @param Core.Event#EVENTDATA EventData -function AI_A2A:OnCrash( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:I( self.Controllable:GetUnits() ) - if #self.Controllable:GetUnits() == 1 then - self:__Crash( 1, EventData ) - end - end -end - ---- @param #AI_A2A self --- @param Core.Event#EVENTDATA EventData -function AI_A2A:OnEjection( EventData ) - - if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__Eject( 1, EventData ) - end -end - ---- @param #AI_A2A self --- @param Core.Event#EVENTDATA EventData -function AI_A2A: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_A2A_Cap.lua b/Moose Development/Moose/AI/AI_A2A_Cap.lua index ba41a3bab..460d5d614 100644 --- a/Moose Development/Moose/AI/AI_A2A_Cap.lua +++ b/Moose Development/Moose/AI/AI_A2A_Cap.lua @@ -10,7 +10,8 @@ -- @image AI_Combat_Air_Patrol.JPG --- @type AI_A2A_CAP --- @extends AI.AI_A2A_Patrol#AI_A2A_PATROL +-- @extends AI.AI_Air_Patrol#AI_AIR_PATROL +-- @extends AI.AI_Air_Engage#AI_AIR_ENGAGE --- The AI_A2A_CAP class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} @@ -97,6 +98,36 @@ AI_A2A_CAP = { ClassName = "AI_A2A_CAP", } +--- Creates a new AI_A2A_CAP object +-- @param #AI_A2A_CAP self +-- @param Wrapper.Group#GROUP AICap +-- @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 DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @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#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#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @return #AI_A2A_CAP +function AI_A2A_CAP:New2( AICap, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType ) + + -- Multiple inheritance ... :-) + local AI_Air = AI_AIR:New( AICap ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AICap, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) + + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.4 ) + self:SetDisengageRadius( 70000 ) + + return self +end + --- Creates a new AI_A2A_CAP object -- @param #AI_A2A_CAP self -- @param Wrapper.Group#GROUP AICap @@ -111,176 +142,8 @@ AI_A2A_CAP = { -- @return #AI_A2A_CAP function AI_A2A_CAP:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType ) - -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2A_PATROL:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2A_CAP + return self:New2( AICap, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolZone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, PatrolAltType ) - 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_A2A_CAP. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_A2A_CAP] OnBeforeEngage - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] OnAfterEngage - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] Engage - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2A_CAP] __Engage - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_A2A_CAP] OnLeaveEngaging --- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] OnEnterEngaging --- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap 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_A2A_CAP. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_A2A_CAP] OnBeforeFired - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] OnAfterFired - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] Fired - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2A_CAP] __Fired - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_A2A_CAP] OnBeforeDestroy - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] OnAfterDestroy - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] Destroy - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2A_CAP] __Destroy - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_A2A_CAP] OnBeforeAbort - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] OnAfterAbort - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] Abort - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2A_CAP] __Abort - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2A_CAP] OnBeforeAccomplish - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] OnAfterAccomplish - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] Accomplish - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2A_CAP] __Accomplish - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - - self:AddTransition( { "Patrolling", "Engaging" }, "Refuel", "Refuelling" ) - - return self end --- onafter State Transition for Event Patrol. @@ -291,7 +154,7 @@ end -- @param #string To The To State string. function AI_A2A_CAP:onafterStart( AICap, From, Event, To ) - self:GetParent( self ).onafterStart( self, AICap, From, Event, To ) + self:GetParent( self, AI_A2A_CAP ).onafterStart( self, AICap, From, Event, To ) AICap:HandleEvent( EVENTS.Takeoff, nil, self ) end @@ -324,168 +187,26 @@ function AI_A2A_CAP:SetEngageRange( EngageRange ) end end ---- onafter State Transition for Event Patrol. +--- Evaluate the attack and create an AttackUnitTask list. -- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap 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_A2A_CAP:onafterPatrol( AICap, From, Event, To ) +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2A_CAP self +function AI_A2A_CAP:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) - -- Call the parent Start event handler - self:GetParent(self).onafterPatrol( self, AICap, From, Event, To ) - self:HandleEvent( EVENTS.Dead ) + local AttackUnitTasks = {} -end - --- todo: need to fix this global function - ---- @param Wrapper.Group#GROUP AICap -function AI_A2A_CAP.AttackRoute( AICap, Fsm ) - - AICap:F( { "AI_A2A_CAP.AttackRoute:", AICap:GetName() } ) - - if AICap:IsAlive() then - Fsm:__Engage( 0.5 ) - end -end - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap 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_A2A_CAP:onbeforeEngage( AICap, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap 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_A2A_CAP:onafterAbort( AICap, From, Event, To ) - AICap:ClearTasks() - self:__Route( 0.5 ) -end - - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The AICap 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_A2A_CAP:onafterEngage( AICap, From, Event, To, AttackSetUnit ) - - self:F( { AICap, 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 AICap and AICap:IsAlive() then - - local EngageRoute = {} - - --- Calculate the target route point. - local CurrentCoord = AICap: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 - -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition() - -- Maybe the detected set also contains - AttackTasks[#AttackTasks+1] = AICap:TaskAttackUnit( AttackUnit ) - end - end - - if #AttackTasks == 0 then - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 0.5 ) - else - AICap:OptionROEOpenFire() - AICap:OptionROTEvadeFire() - - AttackTasks[#AttackTasks+1] = AICap:TaskFunction( "AI_A2A_CAP.AttackRoute", self ) - EngageRoute[#EngageRoute].task = AICap:TaskCombo( AttackTasks ) - end - - AICap:Route( EngageRoute, 0.5 ) + 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 + -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition() + -- Maybe the detected set also contains + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) end - else - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 0.5 ) - end -end - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap 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_A2A_CAP:onafterAccomplish( AICap, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -end - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap 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_A2A_CAP:onafterDestroy( AICap, From, Event, To, EventData ) - - if EventData.IniUnit then - self.AttackUnits[EventData.IniUnit] = nil - end -end - ---- @param #AI_A2A_CAP self --- @param Core.Event#EVENTDATA EventData -function AI_A2A_CAP: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 AICap -function AI_A2A_CAP.Resume( AICap, Fsm ) - - AICap:I( { "AI_A2A_CAP.Resume:", AICap:GetName() } ) - if AICap:IsAlive() then - Fsm:__Reset( 1 ) - Fsm:__Route( 5 ) end + return AttackUnitTasks end + diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index cf59ef0fa..43d74920d 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -1770,13 +1770,87 @@ do -- AI_A2A_DISPATCHER --- Set a CAP for a Squadron. -- @param #AI_A2A_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 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 #number EngageAltType The altitude type to engage, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @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 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 PatrolFloorAltitude The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude the maximum altitude at which the cap can be executed. + -- @param #number PatrolAltType The altitude type to patrol, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2A_DISPATCHER + -- @usage + -- + -- -- CAP Squadron execution. + -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) + -- -- Setup a CAP, engaging between 800 and 900 km/h, altitude 30 (above the sea), radio altitude measurement, + -- -- patrolling speed between 500 and 600 km/h, altitude between 4000 and 10000 meters, barometric altitude measurement. + -- A2ADispatcher:SetSquadronCapV2( "Mineralnye", 800, 900, 30, 30, "RADIO", CAPZoneEast, 500, 600, 4000, 10000, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) + -- -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 4000 and 10000 meters, radio altitude measurement, + -- -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, barometric altitude measurement. + -- A2ADispatcher:SetSquadronCapV2( "Sochi", 800, 1200, 2000, 3000, "RADIO", CAPZoneWest, 600, 800, 4000, 8000, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- + -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") + -- -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 5000 and 8000 meters, barometric altitude measurement, + -- -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, radio altitude. + -- A2ADispatcher:SetSquadronCapV2( "Maykop", 800, 1200, 5000, 8000, "BARO", CAPZoneMiddle, 600, 800, 4000, 8000, "RADIO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Maykop", 2, 30, 120, 1 ) + -- + function AI_A2A_DISPATCHER:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType ) + + 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.EngageMinSpeed = EngageMinSpeed + Cap.EngageMaxSpeed = EngageMaxSpeed + Cap.EngageFloorAltitude = EngageFloorAltitude + Cap.EngageCeilingAltitude = EngageCeilingAltitude + Cap.Zone = Zone + Cap.PatrolMinSpeed = PatrolMinSpeed + Cap.PatrolMaxSpeed = PatrolMaxSpeed + Cap.PatrolFloorAltitude = PatrolFloorAltitude + Cap.PatrolCeilingAltitude = PatrolCeilingAltitude + Cap.PatrolAltType = PatrolAltType + Cap.EngageAltType = EngageAltType + + self:SetSquadronCapInterval( SquadronName, self.DefenderDefault.CapLimit, self.DefenderDefault.CapMinSeconds, self.DefenderDefault.CapMaxSeconds, 1 ) + + self:I( { CAP = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageAltType } } ) + + -- Add the CAP to the EWR network. + + local RecceSet = self.Detection:GetDetectionSet() + RecceSet:FilterPrefixes( DefenderSquadron.TemplatePrefixes ) + RecceSet:FilterStart() + + self.Detection:SetFriendlyPrefixes( DefenderSquadron.TemplatePrefixes ) + + return self + end + + --- Set a CAP for a Squadron. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @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 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 Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. + -- @param #number PatrolFloorAltitude The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude 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 AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. -- @return #AI_A2A_DISPATCHER -- @usage @@ -1794,37 +1868,9 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) -- - function AI_A2A_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 ) + function AI_A2A_DISPATCHER:SetSquadronCap( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, EngageMinSpeed, EngageMaxSpeed, AltType ) - 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:GetDetectionSet() - RecceSet:FilterPrefixes( DefenderSquadron.TemplatePrefixes ) - RecceSet:FilterStart() - - self.Detection:SetFriendlyPrefixes( DefenderSquadron.TemplatePrefixes ) - - return self + return self:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType ) end --- Set the squadron CAP parameters. @@ -2012,6 +2058,37 @@ do -- AI_A2A_DISPATCHER return nil end + --- Set squadron GCI. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed The minimum speed [km/h] at which the GCI can be executed. + -- @param #number EngageMaxSpeed The maximum speed [km/h] at which the GCI can be executed. + -- @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 DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". + -- @usage + -- + -- -- GCI Squadron execution. + -- A2ADispatcher:SetSquadronGci2( "Mozdok", 900, 1200, 5000, 5000, "BARO" ) + -- A2ADispatcher:SetSquadronGci2( "Novo", 900, 2100, 30, 30, "RADIO" ) + -- A2ADispatcher:SetSquadronGci2( "Maykop", 900, 1200, 100, 300, "RADIO" ) + -- + -- @return #AI_A2A_DISPATCHER + function AI_A2A_DISPATCHER:SetSquadronGci2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {} + + local Intercept = self.DefenderSquadrons[SquadronName].Gci + Intercept.Name = SquadronName + Intercept.EngageMinSpeed = EngageMinSpeed + Intercept.EngageMaxSpeed = EngageMaxSpeed + Intercept.EngageFloorAltitude = EngageFloorAltitude + Intercept.EngageCeilingAltitude = EngageCeilingAltitude + Intercept.EngageAltType = EngageAltType + + self:I( { GCI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + end --- Set squadron GCI. -- @param #AI_A2A_DISPATCHER self @@ -3080,7 +3157,7 @@ do -- AI_A2A_DISPATCHER 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 ) + local Fsm = AI_A2A_CAP:New2( DefenderCAP, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.EngageFloorAltitude, Cap.EngageCeilingAltitude, Cap.EngageAltType, Cap.Zone, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.PatrolFloorAltitude, Cap.PatrolCeilingAltitude, Cap.PatrolAltType ) Fsm:SetDispatcher( self ) Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) @@ -3101,7 +3178,7 @@ do -- AI_A2A_DISPATCHER self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", Fsm ) function Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) - self:F({"CAP Birth", DefenderGroup:GetName()}) + self:F({"CAP Takeoff", DefenderGroup:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() @@ -3113,7 +3190,7 @@ do -- AI_A2A_DISPATCHER Fsm:__Patrol( 2 ) -- Start Patrolling end end - + function Fsm:onafterRTB( DefenderGroup, From, Event, To ) self:F({"CAP RTB", DefenderGroup:GetName()}) @@ -3169,7 +3246,7 @@ do -- AI_A2A_DISPATCHER for DefenderID, Defender in pairs( Defenders ) do local Fsm = self:GetDefenderTaskFsm( Defender ) - Fsm:__Engage( 1, AttackerDetection.Set ) -- Engage on the TargetSetUnit + Fsm:__EngageRoute( 1, AttackerDetection.Set ) -- Engage on the TargetSetUnit self:SetDefenderTaskTarget( Defender, AttackerDetection ) @@ -3201,7 +3278,7 @@ do -- AI_A2A_DISPATCHER for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) - Fsm:__Engage( 1, AttackerSet ) -- Engage on the TargetSetUnit + Fsm:__EngageRoute( 1, AttackerSet ) -- Engage on the TargetSetUnit self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) @@ -3283,7 +3360,7 @@ do -- AI_A2A_DISPATCHER DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead - local Fsm = AI_A2A_GCI:New( DefenderGCI, Gci.EngageMinSpeed, Gci.EngageMaxSpeed ) + local Fsm = AI_A2A_GCI:New2( DefenderGCI, Gci.EngageMinSpeed, Gci.EngageMaxSpeed, Gci.EngageFloorAltitude, Gci.EngageCeilingAltitude, Gci.EngageAltType ) Fsm:SetDispatcher( self ) Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) @@ -3306,10 +3383,27 @@ do -- AI_A2A_DISPATCHER if DefenderTarget then Dispatcher:MessageToPlayers( DefenderName .. " wheels up.", DefenderGroup ) - Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit + --Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit + Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end + function Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"GCI Route", DefenderGroup:GetName()}) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron then + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + + Dispatcher:MessageToPlayers( DefenderName .. ", intercepting bogeys at " .. Coordinate:ToStringA2A( DefenderGroup ), DefenderGroup ) + end + self:GetParent( Fsm ).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) + end + function Fsm:onafterRTB( DefenderGroup, From, Event, To ) self:F({"GCI RTB", DefenderGroup:GetName()}) self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) diff --git a/Moose Development/Moose/AI/AI_A2A_Gci.lua b/Moose Development/Moose/AI/AI_A2A_Gci.lua index a6642a9ce..f0ddb1d13 100644 --- a/Moose Development/Moose/AI/AI_A2A_Gci.lua +++ b/Moose Development/Moose/AI/AI_A2A_Gci.lua @@ -105,185 +105,39 @@ AI_A2A_GCI = { --- Creates a new AI_A2A_GCI object -- @param #AI_A2A_GCI self -- @param Wrapper.Group#GROUP AIIntercept +-- @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 DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". -- @return #AI_A2A_GCI -function AI_A2A_GCI:New( AIIntercept, EngageMinSpeed, EngageMaxSpeed ) +function AI_A2A_GCI:New2( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) - -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2A:New( AIIntercept ) ) -- #AI_A2A_GCI - - self.Accomplished = false - self.Engaging = false + local AI_Air = AI_AIR:New( AIIntercept ) + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air, AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) -- #AI_A2A_GCI - 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_A2A_GCI. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_A2A_GCI] OnBeforeEngage - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] OnAfterEngage - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] Engage - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2A_GCI] __Engage - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_A2A_GCI] OnLeaveEngaging --- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] OnEnterEngaging --- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_A2A_GCI] OnBeforeFired - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] OnAfterFired - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] Fired - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2A_GCI] __Fired - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_A2A_GCI] OnBeforeDestroy - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] OnAfterDestroy - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] Destroy - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2A_GCI] __Destroy - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_A2A_GCI] OnBeforeAbort - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] OnAfterAbort - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] Abort - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2A_GCI] __Abort - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2A_GCI] OnBeforeAccomplish - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] OnAfterAccomplish - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] Accomplish - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2A_GCI] __Accomplish - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - - self:AddTransition( { "Patrolling", "Engaging" }, "Refuel", "Refuelling" ) + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.4 ) + self:SetDisengageRadius( 70000 ) return self end +--- Creates a new AI_A2A_GCI object +-- @param #AI_A2A_GCI self +-- @param Wrapper.Group#GROUP AIIntercept +-- @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 DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @return #AI_A2A_GCI +function AI_A2A_GCI:New( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + return self:New2( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) +end + --- onafter State Transition for Event Patrol. -- @param #AI_A2A_GCI self -- @param Wrapper.Group#GROUP AIIntercept The AI Group managed by the FSM. @@ -292,173 +146,33 @@ end -- @param #string To The To State string. function AI_A2A_GCI:onafterStart( AIIntercept, From, Event, To ) - self:GetParent( self ).onafterStart( self, AIIntercept, From, Event, To ) - AIIntercept:HandleEvent( EVENTS.Takeoff, nil, self ) - + self:GetParent( self, AI_A2A_GCI ).onafterStart( self, AIIntercept, From, Event, To ) end - ---- onafter State Transition for Event Patrol. +--- Evaluate the attack and create an AttackUnitTask list. -- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI:onafterEngage( AIIntercept, From, Event, To ) +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2A_GCI self +function AI_A2A_GCI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) - self:HandleEvent( EVENTS.Dead ) + local AttackUnitTasks = {} -end - --- todo: need to fix this global function - ---- @param Wrapper.Group#GROUP AIControllable -function AI_A2A_GCI.InterceptRoute( AIIntercept, Fsm ) - - AIIntercept:F( { "AI_A2A_GCI.InterceptRoute:", AIIntercept:GetName() } ) - - if AIIntercept:IsAlive() then - Fsm:__Engage( 0.5 ) - - --local Task = AIIntercept:TaskOrbitCircle( 4000, 400 ) - --AIIntercept:SetTask( Task ) - end -end - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI:onbeforeEngage( AIIntercept, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI:onafterAbort( AIIntercept, From, Event, To ) - AIIntercept:ClearTasks() - self:Return() - self:__RTB( 0.5 ) -end - - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI:onafterEngage( AIIntercept, From, Event, To, AttackSetUnit ) - - self:F( { AIIntercept, 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 AIIntercept:IsAlive() then - - local EngageRoute = {} - - local CurrentCoord = AIIntercept:GetCoordinate() - - --- Calculate the target route point. - - local CurrentCoord = AIIntercept:GetCoordinate() - - local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - self:SetTargetDistance( ToTargetCoord ) -- For RTB status check - - 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( 15000, ToInterceptAngle ):WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = ToInterceptAngle, 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:IsAir() then - self:T( { "Intercepting Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) - AttackTasks[#AttackTasks+1] = AIIntercept:TaskAttackUnit( AttackUnit ) - end - end - - if #AttackTasks == 0 then - self:E("No targets found -> Going RTB") - self:Return() - self:__RTB( 0.5 ) - else - AIIntercept:OptionROEOpenFire() - AIIntercept:OptionROTEvadeFire() - - AttackTasks[#AttackTasks+1] = AIIntercept:TaskFunction( "AI_A2A_GCI.InterceptRoute", self ) - EngageRoute[#EngageRoute].task = AIIntercept:TaskCombo( AttackTasks ) - end - - AIIntercept:Route( EngageRoute, 0.5 ) - + 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 + -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition() + -- Maybe the detected set also contains + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) end - else - self:E("No targets found -> Going RTB") - self:Return() - self:__RTB( 0.5 ) end + + return AttackUnitTasks end ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI:onafterAccomplish( AIIntercept, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -end ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI:onafterDestroy( AIIntercept, From, Event, To, EventData ) - if EventData.IniUnit then - self.AttackUnits[EventData.IniUnit] = nil - end -end ---- @param #AI_A2A_GCI self --- @param Core.Event#EVENTDATA EventData -function AI_A2A_GCI: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_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index 6b1315440..9eedd29cc 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -136,8 +136,14 @@ AI_A2A_PATROL = { -- PatrolArea = AI_A2A_PATROL:New( PatrolZone, 3000, 6000, 600, 900 ) function AI_A2A_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2A:New( AIPatrol ) ) -- #AI_A2A_PATROL + local AI_Air = AI_AIR:New( AIPatrol ) + local AI_Air_Patrol = AI_A2A_PATROL:New( AI_Air, AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + local self = BASE:Inherit( self, AI_Air_Patrol ) -- #AI_A2A_PATROL + + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.4 ) + self:SetDisengageRadius( 70000 ) + self.PatrolZone = PatrolZone self.PatrolFloorAltitude = PatrolFloorAltitude @@ -257,35 +263,6 @@ function AI_A2A_PATROL:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) self.PatrolCeilingAltitude = PatrolCeilingAltitude end ---- Set race track parameters. CAP flights will perform race track patterns rather than randomly patrolling the zone. --- @param #AI_A2A_PATROL self --- @param #number LegMin Min Length of the race track leg in meters. Default 10,000 m. --- @param #number LegMax Max length of the race track leg in meters. Default 15,000 m. --- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North. --- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from South to North. --- @param #number DurationMin (Optional) Min duration before switching the orbit position. Default is keep same orbit until RTB or engage. --- @param #number DurationMax (Optional) Max duration before switching the orbit position. Default is keep same orbit until RTB or engage. --- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. --- @return #AI_A2A_PATROL self -function AI_A2A_PATROL:SetRaceTrackPattern(LegMin, LegMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) - self:F2({leglength, duration}) - - self.racetrack=true - self.racetracklegmin=LegMin or 10000 - self.racetracklegmax=LegMax or 15000 - self.racetrackheadingmin=HeadingMin or 0 - self.racetrackheadingmax=HeadingMax or 180 - self.racetrackdurationmin=DurationMin - self.racetrackdurationmax=DurationMax - - if self.racetrackdurationmax and not self.racetrackdurationmin then - self.racetrackdurationmin=self.racetrackdurationmax - end - - self.racetrackcapcoordinates=CapCoordinates - -end - --- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. -- @param #AI_A2A_PATROL self @@ -426,13 +403,3 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) end ---- @param Wrapper.Group#GROUP AIPatrol -function AI_A2A_PATROL.Resume( AIPatrol, Fsm ) - - AIPatrol:I( { "AI_A2A_PATROL.Resume:", AIPatrol:GetName() } ) - if AIPatrol:IsAlive() then - Fsm:__Reset( 1 ) - Fsm:__Route( 5 ) - end - -end diff --git a/Moose Development/Moose/AI/AI_A2G.lua b/Moose Development/Moose/AI/AI_A2G.lua deleted file mode 100644 index b80eb8796..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.95 ) - self:SetDisengageRadius( 70000 ) - - return self -end - diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index ac8efbb4f..8b8b8e880 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -35,6 +35,7 @@ AI_A2G_BAI = { -- @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 DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". -- @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. @@ -42,10 +43,12 @@ 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, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) +function AI_A2G_BAI:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - -- 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 AI_Air = AI_AIR:New( AIGroup ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) local RTBSpeedMax = AIGroup:GetSpeedMax() or 9999 @@ -55,112 +58,46 @@ function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAlt 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 ) +--- 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 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. +-- @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, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - self:I( { DefenderGroup, From, Event, To, AttackSetUnit} ) - - local DefenderGroupName = DefenderGroup:GetName() - - local AttackCount = AttackSetUnit:Count() - - self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air! - - if AttackCount > 0 then - - 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( EngageAltitude ) -- Ground targets don't have an altitude. - - local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() - TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. - - local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) - local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) - - local EngageRoute = {} - local AttackTasks = {} - - --- 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 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 AttackUnitTasks = {} - - local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) - for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do - if AttackUnit then - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "BAI Unit:", AttackUnit:GetName() } ) - AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) - end - end - end - - if #AttackUnitTasks == 0 then - self:I( DefenderGroupName .. ": No targets found -> Going RTB") - self:Return() - self:__RTB( self.TaskDelay ) - else - DefenderGroup:OptionROEOpenFire() - DefenderGroup:OptionROTEvadeFire() - DefenderGroup:OptionKeepWeaponsOnThreat() - - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) - end - - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.___Engage", self, AttackSetUnit ) - EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) - - DefenderGroup:Route( EngageRoute, self.TaskDelay ) - end - else - self:I( DefenderGroupName .. ": No targets found -> Going RTB") - self:Return() - self:__RTB( self.TaskDelay ) - end + return self:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType) end ---- @param Wrapper.Group#GROUP AIEngage -function AI_A2G_BAI.Resume( AIEngage, Fsm ) +--- Evaluate the attack and create an AttackUnitTask list. +-- @param #AI_A2G_BAI self +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2G_BAI self +function AI_A2G_BAI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) - AIEngage:F( { "AI_A2G_BAI.Resume:", AIEngage:GetName() } ) - if AIEngage:IsAlive() then - Fsm:__Reset( Fsm.TaskDelay ) - Fsm:__EngageRoute( Fsm.TaskDelay, Fsm.AttackSetUnit ) + local AttackUnitTasks = {} + + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "BAI Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + end + end end -end \ No newline at end of file + return AttackUnitTasks +end + + diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 8ed9ee29d..00cd73383 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_Patrol#AI_A2G_PATROL +-- @extends AI.AI_A2G_Patrol#AI_AIR_PATROL --- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. @@ -35,6 +35,7 @@ AI_A2G_CAS = { -- @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 DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". -- @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. @@ -42,10 +43,12 @@ 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, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) +function AI_A2G_CAS:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - -- 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 AI_Air = AI_AIR:New( AIGroup ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) local RTBSpeedMax = AIGroup:GetSpeedMax() or 9999 @@ -55,108 +58,47 @@ function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAlt 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 ) +--- 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 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. +-- @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, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) - - local DefenderGroupName = DefenderGroup:GetName() - - self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air! - - local AttackCount = AttackSetUnit:Count() - - if AttackCount > 0 then - - if DefenderGroup:IsAlive() then - - local EngageAltitude = math.random( self.EngageFloorAltitude or 500, self.EngageCeilingAltitude or 1000 ) - local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - - local DefenderCoord = DefenderGroup:GetPointVec3() - DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. - - local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() - TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. - - local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) - local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) - - local EngageRoute = {} - local AttackTasks = {} - - 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 = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) - local ToWP = DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( - self.PatrolAltType or "RADIO", - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - EngageSpeed, - true - ) - EngageRoute[#EngageRoute+1] = ToWP - - if TargetDistance <= EngageDistance * 3 then - - local AttackUnitTasks = {} - - local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) - for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do - if AttackUnit then - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - self:T( { "CAS Unit:", AttackUnit:GetName() } ) - AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) - end - end - end - - - if #AttackUnitTasks == 0 then - self:I( DefenderGroupName .. ": No targets found -> Going RTB") - self:Return() - self:__RTB( self.TaskDelay ) - else - DefenderGroup:OptionROEOpenFire() - DefenderGroup:OptionROTEvadeFire() - DefenderGroup:OptionKeepWeaponsOnThreat() - - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) - end - end - - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.___Engage", self, AttackSetUnit ) - EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) - - DefenderGroup:Route( EngageRoute, self.TaskDelay ) - end - else - self:I( DefenderGroupName .. ": No targets found -> Going RTB") - self:Return() - self:__RTB( self.TaskDelay ) - end + return self:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType) end ---- @param Wrapper.Group#GROUP AIEngage -function AI_A2G_CAS.Resume( AIEngage, Fsm ) +--- Evaluate the attack and create an AttackUnitTask list. +-- @param #AI_A2G_CAS self +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2G_CAS self +function AI_A2G_CAS:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) - AIEngage:F( { "AI_A2G_CAS.Resume:", AIEngage:GetName() } ) - if AIEngage:IsAlive() then - Fsm:__Reset( Fsm.TaskDelay ) - Fsm:__EngageRoute( Fsm.TaskDelay, Fsm.AttackSetUnit ) + local AttackUnitTasks = {} + + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "CAS Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + end + end end -end \ No newline at end of file + return AttackUnitTasks +end + + + diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 0c6450c2c..3628fca69 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -2088,22 +2088,23 @@ do -- AI_A2G_DISPATCHER - --- + --- Set a squadron to engage for suppression of air defenses, when a defense point is under attack. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. -- @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. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. -- @usage -- -- -- SEAD Squadron execution. - -- A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200 ) - -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100 ) - -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200 ) + -- A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200, 4000, 5000, "BARO" ) + -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100, 6000, 9000, "BARO" ) + -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200, 30, 100, "RADIO" ) -- -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + function AI_A2G_DISPATCHER:SetSquadronSead2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -2115,9 +2116,32 @@ do -- AI_A2G_DISPATCHER Sead.EngageMaxSpeed = EngageMaxSpeed Sead.EngageFloorAltitude = EngageFloorAltitude or 500 Sead.EngageCeilingAltitude = EngageCeilingAltitude or 1000 + Sead.EngageAltType = EngageAltType Sead.Defend = true - self:F( { Sead = Sead } ) + self:I( { SEAD = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + + return self + end + + --- Set a squadron to engage for suppression of air defenses, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @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. + -- A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200, 4000, 5000 ) + -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100, 6000, 8000 ) + -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200, 6000, 10000 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + return self:SetSquadronSead2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" ) end --- Set the squadron SEAD engage limit. @@ -2140,6 +2164,55 @@ do -- AI_A2G_DISPATCHER + --- 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 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 PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolAltType The altitude type when patrolling, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @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 EngageFloorAltitude (optional, default = 1000m ) The minimum altitude at which the engage can be executed. + -- @param #number EngageCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the engage can be executed. + -- @param #number EngageAltType The altitude type when engaging, 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:SetSquadronSeadPatrol2( "Mineralnye", PatrolZoneEast, 500, 600, 4000, 10000, "BARO", 800, 900, 2000, 3000, "RADIO", ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} + + local SeadPatrol = DefenderSquadron.SEAD + SeadPatrol.Name = SquadronName + SeadPatrol.Zone = Zone + SeadPatrol.PatrolFloorAltitude = PatrolFloorAltitude + SeadPatrol.PatrolCeilingAltitude = PatrolCeilingAltitude + SeadPatrol.EngageFloorAltitude = EngageFloorAltitude + SeadPatrol.EngageCeilingAltitude = EngageCeilingAltitude + SeadPatrol.PatrolMinSpeed = PatrolMinSpeed + SeadPatrol.PatrolMaxSpeed = PatrolMaxSpeed + SeadPatrol.EngageMinSpeed = EngageMinSpeed + SeadPatrol.EngageMaxSpeed = EngageMaxSpeed + SeadPatrol.PatrolAltType = PatrolAltType + SeadPatrol.EngageAltType = EngageAltType + SeadPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "SEAD" ) + + self:I( { SEAD = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + 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 @@ -2160,47 +2233,29 @@ do -- AI_A2G_DISPATCHER -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) -- function AI_A2G_DISPATCHER:SetSquadronSeadPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + self:SetSquadronSeadPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, FloorAltitude, CeilingAltitude, AltType, EngageMinSpeed, EngageMaxSpeed, FloorAltitude, CeilingAltitude, AltType ) - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} - - local SeadPatrol = DefenderSquadron.SEAD - SeadPatrol.Name = SquadronName - SeadPatrol.Zone = Zone - SeadPatrol.PatrolFloorAltitude = FloorAltitude - SeadPatrol.PatrolCeilingAltitude = CeilingAltitude - SeadPatrol.EngageFloorAltitude = FloorAltitude - SeadPatrol.EngageCeilingAltitude = 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 - + - --- + --- Set a squadron to engage for close air support, when a defense point is under attack. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. -- @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. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. -- @usage -- -- -- CAS Squadron execution. - -- A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200 ) - -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100 ) - -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200 ) + -- A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200, 4000, 5000, "BARO" ) + -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100, 6000, 9000, "BARO" ) + -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200, 30, 100, "RADIO" ) -- -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + function AI_A2G_DISPATCHER:SetSquadronCas2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -2212,9 +2267,32 @@ do -- AI_A2G_DISPATCHER Cas.EngageMaxSpeed = EngageMaxSpeed Cas.EngageFloorAltitude = EngageFloorAltitude or 500 Cas.EngageCeilingAltitude = EngageCeilingAltitude or 1000 + Cas.EngageAltType = EngageAltType Cas.Defend = true - self:F( { Cas = Cas } ) + self:I( { CAS = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + + return self + end + + --- Set a squadron to engage for close air support, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @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. + -- A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200, 4000, 5000 ) + -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100, 6000, 8000 ) + -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200, 6000, 10000 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + return self:SetSquadronCas2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" ) end @@ -2236,6 +2314,53 @@ do -- AI_A2G_DISPATCHER 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 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 PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolAltType The altitude type when patrolling, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @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 EngageFloorAltitude (optional, default = 1000m ) The minimum altitude at which the engage can be executed. + -- @param #number EngageCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the engage can be executed. + -- @param #number EngageAltType The altitude type when engaging, 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:SetSquadronCasPatrol2( "Mineralnye", PatrolZoneEast, 500, 600, 4000, 10000, "BARO", 800, 900, 2000, 3000, "RADIO", ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.CAS = DefenderSquadron.CAS or {} + + local CasPatrol = DefenderSquadron.CAS + CasPatrol.Name = SquadronName + CasPatrol.Zone = Zone + CasPatrol.PatrolFloorAltitude = PatrolFloorAltitude + CasPatrol.PatrolCeilingAltitude = PatrolCeilingAltitude + CasPatrol.EngageFloorAltitude = EngageFloorAltitude + CasPatrol.EngageCeilingAltitude = EngageCeilingAltitude + CasPatrol.PatrolMinSpeed = PatrolMinSpeed + CasPatrol.PatrolMaxSpeed = PatrolMaxSpeed + CasPatrol.EngageMinSpeed = EngageMinSpeed + CasPatrol.EngageMaxSpeed = EngageMaxSpeed + CasPatrol.PatrolAltType = PatrolAltType + CasPatrol.EngageAltType = EngageAltType + CasPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "CAS" ) + + self:I( { CAS = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + end --- Set a Cas patrol for a Squadron. @@ -2258,47 +2383,28 @@ do -- AI_A2G_DISPATCHER -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) -- function AI_A2G_DISPATCHER:SetSquadronCasPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + self:SetSquadronCasPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, FloorAltitude, CeilingAltitude, AltType, EngageMinSpeed, EngageMaxSpeed, FloorAltitude, CeilingAltitude, AltType ) - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.CAS = DefenderSquadron.CAS or {} - - local CasPatrol = DefenderSquadron.CAS - CasPatrol.Name = SquadronName - CasPatrol.Zone = Zone - CasPatrol.PatrolFloorAltitude = FloorAltitude - CasPatrol.PatrolCeilingAltitude = CeilingAltitude - CasPatrol.EngageFloorAltitude = FloorAltitude - CasPatrol.EngageCeilingAltitude = 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 - - --- + --- Set a squadron to engage for a battlefield area interdiction, when a defense point is under attack. -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. -- @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. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. -- @usage -- -- -- BAI Squadron execution. - -- A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200 ) - -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100 ) - -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200 ) + -- A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200, 4000, 5000, "BARO" ) + -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100, 6000, 9000, "BARO" ) + -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200, 30, 100, "RADIO" ) -- -- @return #AI_A2G_DISPATCHER - function AI_A2G_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + function AI_A2G_DISPATCHER:SetSquadronBai2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -2310,9 +2416,32 @@ do -- AI_A2G_DISPATCHER Bai.EngageMaxSpeed = EngageMaxSpeed Bai.EngageFloorAltitude = EngageFloorAltitude or 500 Bai.EngageCeilingAltitude = EngageCeilingAltitude or 1000 + Bai.EngageAltType = EngageAltType Bai.Defend = true - self:F( { Bai = Bai } ) + self:I( { BAI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + + return self + end + + --- Set a squadron to engage for a battlefield area interdiction, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @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. + -- A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200, 4000, 5000 ) + -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100, 6000, 8000 ) + -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200, 6000, 10000 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + return self:SetSquadronBai2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" ) end @@ -2333,6 +2462,54 @@ do -- AI_A2G_DISPATCHER 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 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 PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolAltType The altitude type when patrolling, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @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 EngageFloorAltitude (optional, default = 1000m ) The minimum altitude at which the engage can be executed. + -- @param #number EngageCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the engage can be executed. + -- @param #number EngageAltType The altitude type when engaging, 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:SetSquadronBaiPatrol2( "Mineralnye", PatrolZoneEast, 500, 600, 4000, 10000, "BARO", 800, 900, 2000, 3000, "RADIO", ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.BAI = DefenderSquadron.BAI or {} + + local BaiPatrol = DefenderSquadron.BAI + BaiPatrol.Name = SquadronName + BaiPatrol.Zone = Zone + BaiPatrol.PatrolFloorAltitude = PatrolFloorAltitude + BaiPatrol.PatrolCeilingAltitude = PatrolCeilingAltitude + BaiPatrol.EngageFloorAltitude = EngageFloorAltitude + BaiPatrol.EngageCeilingAltitude = EngageCeilingAltitude + BaiPatrol.PatrolMinSpeed = PatrolMinSpeed + BaiPatrol.PatrolMaxSpeed = PatrolMaxSpeed + BaiPatrol.EngageMinSpeed = EngageMinSpeed + BaiPatrol.EngageMaxSpeed = EngageMaxSpeed + BaiPatrol.PatrolAltType = PatrolAltType + BaiPatrol.EngageAltType = EngageAltType + BaiPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "BAI" ) + + self:I( { BAI = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + 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. @@ -2354,28 +2531,9 @@ do -- AI_A2G_DISPATCHER -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) -- function AI_A2G_DISPATCHER:SetSquadronBaiPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + self:SetSquadronBaiPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, FloorAltitude, CeilingAltitude, AltType, EngageMinSpeed, EngageMaxSpeed, FloorAltitude, CeilingAltitude, AltType ) - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.BAI = DefenderSquadron.BAI or {} - - local BaiPatrol = DefenderSquadron.BAI - BaiPatrol.Name = SquadronName - BaiPatrol.Zone = Zone - BaiPatrol.PatrolFloorAltitude = FloorAltitude - BaiPatrol.PatrolCeilingAltitude = CeilingAltitude - BaiPatrol.EngageFloorAltitude = FloorAltitude - BaiPatrol.EngageCeilingAltitude = 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 @@ -3496,7 +3654,7 @@ do -- AI_A2G_DISPATCHER local AI_A2G_PATROL = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } - local AI_A2G_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 ) + local AI_A2G_Fsm = AI_A2G_PATROL[DefenseTaskType]:New2( DefenderGroup, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.EngageFloorAltitude, Patrol.EngageCeilingAltitude, Patrol.EngageAltType, Patrol.Zone, Patrol.PatrolFloorAltitude, Patrol.PatrolCeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.PatrolAltType ) AI_A2G_Fsm:SetDispatcher( self ) AI_A2G_Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) AI_A2G_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) @@ -3508,7 +3666,7 @@ do -- AI_A2G_DISPATCHER self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, AI_A2G_Fsm, nil, DefenderGrouping ) function AI_A2G_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) - self:F({"Defender Takeoff", DefenderGroup:GetName()}) + self:F({"Takeoff", DefenderGroup:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() -- #string @@ -3522,7 +3680,7 @@ do -- AI_A2G_DISPATCHER end function AI_A2G_Fsm:onafterPatrolRoute( DefenderGroup, From, Event, To ) - self:F({"Defender PatrolRoute", DefenderGroup:GetName()}) + self:F({"PatrolRoute", DefenderGroup:GetName()}) self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() @@ -3544,7 +3702,7 @@ do -- AI_A2G_DISPATCHER local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - if Squadron then + if Squadron and AttackSetUnit:Count() > 0 then local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE @@ -3568,7 +3726,7 @@ do -- AI_A2G_DISPATCHER end function AI_A2G_Fsm:onafterRTB( DefenderGroup, From, Event, To ) - self:F({"Defender RTB", DefenderGroup:GetName()}) + self:F({"RTB", DefenderGroup:GetName()}) self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() @@ -3581,7 +3739,7 @@ do -- AI_A2G_DISPATCHER --- @param #AI_A2G_DISPATCHER self function AI_A2G_Fsm:onafterLostControl( DefenderGroup, From, Event, To ) - self:F({"Defender LostControl", DefenderGroup:GetName()}) + self:F({"LostControl", DefenderGroup:GetName()}) self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() @@ -3596,7 +3754,7 @@ do -- AI_A2G_DISPATCHER --- @param #AI_A2G_DISPATCHER self function AI_A2G_Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) - self:F({"Defender Home", DefenderGroup:GetName()}) + self:F({"Home", DefenderGroup:GetName()}) self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() @@ -3635,9 +3793,9 @@ do -- AI_A2G_DISPATCHER if DefenderGroup then - local AI_A2G = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + local AI_A2G_ENGAGE = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } - local AI_A2G_Fsm = AI_A2G[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude ) -- AI.AI_A2G_ENGAGE + local AI_A2G_Fsm = AI_A2G_ENGAGE[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude, Defense.EngageAltType ) -- AI.AI_AIR_ENGAGE AI_A2G_Fsm:SetDispatcher( self ) AI_A2G_Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) AI_A2G_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index cd2883772..b72080558 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_Patrol#AI_A2G_PATROL +-- @extends AI.AI_A2G_Patrol#AI_AIR_PATROL --- Implements the core functions to SEAD intruders. Use the Engage trigger to intercept intruders. @@ -85,6 +85,7 @@ AI_A2G_SEAD = { -- @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 DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". -- @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. @@ -92,10 +93,12 @@ 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, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) +function AI_A2G_SEAD:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - -- 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 AI_Air = AI_AIR:New( AIGroup ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) local RTBSpeedMax = AIGroup:GetSpeedMax() or 9999 @@ -105,116 +108,49 @@ function AI_A2G_SEAD:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAl end +--- 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 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. +-- @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, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ---- @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 -- Kept in memory in case of resume from refuel in air! - - local AttackCount = 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 EngageAltitude = math.random( self.EngageFloorAltitude or 500, self.EngageCeilingAltitude or 1000 ) - local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - - local DefenderCoord = DefenderGroup:GetPointVec3() - DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. - - local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() - TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. - - local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) - local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 25000 ) - - local EngageRoute = {} - local AttackTasks = {} - - local FromWP = DefenderCoord:WaypointAir( - self.PatrolAltType or "RADIO", - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - EngageSpeed, - false - ) - EngageRoute[#EngageRoute+1] = FromWP - - self:SetTargetDistance( TargetCoord ) -- For RTB status check - - local FromEngageAngle = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) - local ToWP = DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( - self.PatrolAltType or "RADIO", - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - EngageSpeed, - true - ) - EngageRoute[#EngageRoute+1] = ToWP - - if TargetDistance <= EngageDistance * 3 then - - local AttackUnitTasks = {} - - local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) - for AttackUnitID, AttackUnit in ipairs( AttackSetUnitPerThreatLevel ) do - if AttackUnit then - if AttackUnit:IsAlive() and AttackUnit:IsGround() then - local HasRadar = AttackUnit:HasSEAD() - if HasRadar then - self:F( { "SEAD Unit:", AttackUnit:GetName() } ) - AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) - end - end - end - end - - if #AttackUnitTasks == 0 then - self:I( 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:TaskCombo( AttackUnitTasks ) - end - end - - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.___Engage", self, AttackSetUnit ) - EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) - - DefenderGroup:Route( EngageRoute, self.TaskDelay ) - end - else - self:I( DefenderGroupName .. ": No targets found -> Going RTB") - self:Return() - self:__RTB( self.TaskDelay ) - end + return self:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) end ---- @param Wrapper.Group#GROUP AIEngage -function AI_A2G_SEAD.Resume( AIEngage, Fsm ) - AIEngage:F( { "AI_A2G_SEAD.Resume:", AIEngage:GetName() } ) - if AIEngage:IsAlive() then - Fsm:__Reset( Fsm.TaskDelay ) - Fsm:__EngageRoute( Fsm.TaskDelay, Fsm.AttackSetUnit ) +--- Evaluate the attack and create an AttackUnitTask list. +-- @param #AI_A2G_SEAD self +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2G_SEAD self +function AI_A2G_SEAD:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) + + local AttackUnitTasks = {} + + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitID, AttackUnit in ipairs( AttackSetUnitPerThreatLevel ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:F( { "SEAD Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + end + end + end end -end \ No newline at end of file + return AttackUnitTasks +end + diff --git a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua index ac912f819..23531f689 100644 --- a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua @@ -2577,7 +2577,7 @@ do -- AI_AIR_DISPATCHER 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 + local Fsm = AI_A2G[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude ) -- AI.AI_AIR_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_Air_Engage.lua similarity index 64% rename from Moose Development/Moose/AI/AI_A2G_Engage.lua rename to Moose Development/Moose/AI/AI_Air_Engage.lua index f74f82f80..a14d604aa 100644 --- a/Moose Development/Moose/AI/AI_A2G_Engage.lua +++ b/Moose Development/Moose/AI/AI_Air_Engage.lua @@ -8,20 +8,20 @@ -- -- === -- --- @module AI.AI_A2G_Engage +-- @module AI.AI_Air_Engage -- @image AI_Air_To_Ground_Engage.JPG ---- @type AI_A2G_ENGAGE --- @extends AI.AI_A2G#AI_A2G +--- @type AI_AIR_ENGAGE +-- @extends AI.AI_AIR#AI_AIR --- 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. +-- The AI_AIR_ENGAGE is assigned a @{Wrapper.Group} and this must be done before the AI_AIR_ENGAGE process can be started using the **Start** event. -- -- ![Process](..\Presentations\AI_GCI\Dia4.JPG) -- @@ -47,9 +47,9 @@ -- -- ![Process](..\Presentations\AI_GCI\Dia13.JPG) -- --- ## 1. AI_A2G_ENGAGE constructor +-- ## 1. AI_AIR_ENGAGE constructor -- --- * @{#AI_A2G_ENGAGE.New}(): Creates a new AI_A2G_ENGAGE object. +-- * @{#AI_AIR_ENGAGE.New}(): Creates a new AI_AIR_ENGAGE object. -- -- ## 3. Set the Range of Engagement -- @@ -59,7 +59,7 @@ -- 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. +-- Use the method @{AI.AI_GCI#AI_AIR_ENGAGE.SetEngageRange}() to define that range. -- -- ## 4. Set the Zone of Engagement -- @@ -67,29 +67,30 @@ -- -- 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. +-- Use the method @{AI.AI_Cap#AI_AIR_ENGAGE.SetEngageZone}() to define that Zone. -- -- === -- --- @field #AI_A2G_ENGAGE -AI_A2G_ENGAGE = { - ClassName = "AI_A2G_ENGAGE", +-- @field #AI_AIR_ENGAGE +AI_AIR_ENGAGE = { + ClassName = "AI_AIR_ENGAGE", } ---- Creates a new AI_A2G_ENGAGE object --- @param #AI_A2G_ENGAGE self +--- Creates a new AI_AIR_ENGAGE object +-- @param #AI_AIR_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, EngageFloorAltitude, EngageCeilingAltitude ) +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @return #AI_AIR_ENGAGE +function AI_AIR_ENGAGE:New( AI_Air, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2G:New( AIGroup ) ) -- #AI_A2G_ENGAGE + local self = BASE:Inherit( self, AI_Air ) -- #AI_AIR_ENGAGE self.Accomplished = false self.Engaging = false @@ -100,12 +101,13 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor self.EngageMaxSpeed = EngageMaxSpeed or SpeedMax * 0.75 self.EngageFloorAltitude = EngageFloorAltitude or 1000 self.EngageCeilingAltitude = EngageCeilingAltitude or 1500 + self.EngageAltType = EngageAltType or "RADIO" - self:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "EngageRoute", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + self:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "EngageRoute", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. --- OnBefore Transition Handler for Event EngageRoute. - -- @function [parent=#AI_A2G_ENGAGE] OnBeforeEngageRoute - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeEngageRoute + -- @param #AI_AIR_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. @@ -113,25 +115,25 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @return #boolean Return false to cancel Transition. --- OnAfter Transition Handler for Event EngageRoute. - -- @function [parent=#AI_A2G_ENGAGE] OnAfterEngageRoute - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] OnAfterEngageRoute + -- @param #AI_AIR_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 EngageRoute. - -- @function [parent=#AI_A2G_ENGAGE] EngageRoute - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] EngageRoute + -- @param #AI_AIR_ENGAGE self --- Asynchronous Event Trigger for Event EngageRoute. - -- @function [parent=#AI_A2G_ENGAGE] __EngageRoute - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] __EngageRoute + -- @param #AI_AIR_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 +-- @function [parent=#AI_AIR_ENGAGE] OnLeaveEngaging +-- @param #AI_AIR_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. @@ -139,18 +141,18 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @return #boolean Return false to cancel Transition. --- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_A2G_ENGAGE] OnEnterEngaging --- @param #AI_A2G_ENGAGE self +-- @function [parent=#AI_AIR_ENGAGE] OnEnterEngaging +-- @param #AI_AIR_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( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "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_AIR_ENGAGE. --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_A2G_ENGAGE] OnBeforeEngage - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeEngage + -- @param #AI_AIR_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. @@ -158,25 +160,25 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @return #boolean Return false to cancel Transition. --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_A2G_ENGAGE] OnAfterEngage - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] OnAfterEngage + -- @param #AI_AIR_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 + -- @function [parent=#AI_AIR_ENGAGE] Engage + -- @param #AI_AIR_ENGAGE self --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2G_ENGAGE] __Engage - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] __Engage + -- @param #AI_AIR_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 +-- @function [parent=#AI_AIR_ENGAGE] OnLeaveEngaging +-- @param #AI_AIR_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. @@ -184,18 +186,18 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @return #boolean Return false to cancel Transition. --- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_A2G_ENGAGE] OnEnterEngaging --- @param #AI_A2G_ENGAGE self +-- @function [parent=#AI_AIR_ENGAGE] OnEnterEngaging +-- @param #AI_AIR_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. + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_A2G_ENGAGE] OnBeforeFired - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeFired + -- @param #AI_AIR_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. @@ -203,27 +205,27 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @return #boolean Return false to cancel Transition. --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_A2G_ENGAGE] OnAfterFired - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] OnAfterFired + -- @param #AI_AIR_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 + -- @function [parent=#AI_AIR_ENGAGE] Fired + -- @param #AI_AIR_ENGAGE self --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2G_ENGAGE] __Fired - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] __Fired + -- @param #AI_AIR_ENGAGE self -- @param #number Delay The delay in seconds. - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_A2G_ENGAGE] OnBeforeDestroy - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeDestroy + -- @param #AI_AIR_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. @@ -231,28 +233,28 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @return #boolean Return false to cancel Transition. --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_A2G_ENGAGE] OnAfterDestroy - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] OnAfterDestroy + -- @param #AI_AIR_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 + -- @function [parent=#AI_AIR_ENGAGE] Destroy + -- @param #AI_AIR_ENGAGE self --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2G_ENGAGE] __Destroy - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] __Destroy + -- @param #AI_AIR_ENGAGE self -- @param #number Delay The delay in seconds. - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_A2G_ENGAGE] OnBeforeAbort - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeAbort + -- @param #AI_AIR_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. @@ -260,27 +262,27 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @return #boolean Return false to cancel Transition. --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_A2G_ENGAGE] OnAfterAbort - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] OnAfterAbort + -- @param #AI_AIR_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 + -- @function [parent=#AI_AIR_ENGAGE] Abort + -- @param #AI_AIR_ENGAGE self --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2G_ENGAGE] __Abort - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] __Abort + -- @param #AI_AIR_ENGAGE self -- @param #number Delay The delay in seconds. - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2G_ENGAGE] OnBeforeAccomplish - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeAccomplish + -- @param #AI_AIR_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. @@ -288,20 +290,20 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @return #boolean Return false to cancel Transition. --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2G_ENGAGE] OnAfterAccomplish - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] OnAfterAccomplish + -- @param #AI_AIR_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 + -- @function [parent=#AI_AIR_ENGAGE] Accomplish + -- @param #AI_AIR_ENGAGE self --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2G_ENGAGE] __Accomplish - -- @param #AI_A2G_ENGAGE self + -- @function [parent=#AI_AIR_ENGAGE] __Accomplish + -- @param #AI_AIR_ENGAGE self -- @param #number Delay The delay in seconds. self:AddTransition( { "Patrolling", "Engaging" }, "Refuel", "Refuelling" ) @@ -310,14 +312,14 @@ function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor end --- onafter event handler for Start event. --- @param #AI_A2G_ENGAGE self +-- @param #AI_AIR_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 ) +function AI_AIR_ENGAGE:onafterStart( AIGroup, From, Event, To ) - self:GetParent( self, AI_A2G_ENGAGE ).onafterStart( self, AIGroup, From, Event, To ) + self:GetParent( self, AI_AIR_ENGAGE ).onafterStart( self, AIGroup, From, Event, To ) AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) @@ -326,12 +328,12 @@ end --- onafter event handler for Engage event. --- @param #AI_A2G_ENGAGE self +-- @param #AI_AIR_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 ) +function AI_AIR_ENGAGE:onafterEngage( AIGroup, From, Event, To ) self:HandleEvent( EVENTS.Dead ) @@ -339,39 +341,14 @@ end -- todo: need to fix this global function ---- @param Wrapper.Group#GROUP AIControllable -function AI_A2G_ENGAGE.___EngageRoute( AIGroup, Fsm, AttackSetUnit ) - - AIGroup:I( { "AI_A2G_ENGAGE.___EngageRoute:", AIGroup:GetName() } ) - - if AIGroup:IsAlive() then - Fsm:__EngageRoute( Fsm.TaskDelay, AttackSetUnit ) - - --local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) - --AIGroup:SetTask( Task ) - end -end - ---- @param Wrapper.Group#GROUP AIControllable -function AI_A2G_ENGAGE.___Engage( AIGroup, Fsm, AttackSetUnit ) - - AIGroup:I( { "AI_A2G_ENGAGE.___Engage:", AIGroup:GetName() } ) - - if AIGroup:IsAlive() then - Fsm:__Engage( Fsm.TaskDelay, AttackSetUnit ) - - --local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) - --AIGroup:SetTask( Task ) - end -end --- onbefore event handler for Engage event. --- @param #AI_A2G_ENGAGE self +-- @param #AI_AIR_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 ) +function AI_AIR_ENGAGE:onbeforeEngage( AIGroup, From, Event, To ) if self.Accomplished == true then return false @@ -379,44 +356,44 @@ function AI_A2G_ENGAGE:onbeforeEngage( AIGroup, From, Event, To ) end --- onafter event handler for Abort event. --- @param #AI_A2G_ENGAGE self +-- @param #AI_AIR_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 ) +function AI_AIR_ENGAGE:onafterAbort( AIGroup, From, Event, To ) AIGroup:ClearTasks() self:Return() self:__RTB( self.TaskDelay ) end ---- @param #AI_A2G_ENGAGE self +--- @param #AI_AIR_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 ) +function AI_AIR_ENGAGE:onafterAccomplish( AIGroup, From, Event, To ) self.Accomplished = true self:SetDetectionOff() end ---- @param #AI_A2G_ENGAGE self +--- @param #AI_AIR_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 ) +function AI_AIR_ENGAGE:onafterDestroy( AIGroup, From, Event, To, EventData ) if EventData.IniUnit then self.AttackUnits[EventData.IniUnit] = nil end end ---- @param #AI_A2G_ENGAGE self +--- @param #AI_AIR_ENGAGE self -- @param Core.Event#EVENTDATA EventData -function AI_A2G_ENGAGE:OnEventDead( EventData ) +function AI_AIR_ENGAGE:OnEventDead( EventData ) self:F( { "EventDead", EventData } ) if EventData.IniDCSUnit then @@ -426,14 +403,29 @@ function AI_A2G_ENGAGE:OnEventDead( EventData ) end end ---- @param #AI_A2G_ENGAGE self + +--- @param Wrapper.Group#GROUP AIControllable +function AI_AIR_ENGAGE.___EngageRoute( AIGroup, Fsm, AttackSetUnit ) + + Fsm:I( { "AI_AIR_ENGAGE.___EngageRoute:", AIGroup:GetName() } ) + + if AIGroup:IsAlive() then + Fsm:__EngageRoute( Fsm.TaskDelay, AttackSetUnit ) + + --local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) + --AIGroup:SetTask( Task ) + end +end + + +--- @param #AI_AIR_ENGAGE 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_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) +function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) - self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + self:I( { DefenderGroup, From, Event, To, AttackSetUnit } ) local DefenderGroupName = DefenderGroup:GetName() @@ -495,7 +487,7 @@ function AI_A2G_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, Attac EngageRoute[#EngageRoute+1] = ToWP - AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.___EngageRoute", self, AttackSetUnit ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_AIR_ENGAGE.___EngageRoute", self, AttackSetUnit ) EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) DefenderGroup:OptionROEReturnFire() @@ -512,3 +504,113 @@ function AI_A2G_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, Attac end end + +--- @param Wrapper.Group#GROUP AIControllable +function AI_AIR_ENGAGE.___Engage( AIGroup, Fsm, AttackSetUnit ) + + Fsm:I( { "AI_AIR_ENGAGE.___Engage:", AIGroup:GetName() } ) + + if AIGroup:IsAlive() then + Fsm:__Engage( Fsm.TaskDelay, AttackSetUnit ) + end +end + + +--- @param #AI_AIR_ENGAGE 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_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air! + + local AttackCount = AttackSetUnit:Count() + self:I({AttackCount = AttackCount}) + + if AttackCount > 0 then + + if DefenderGroup:IsAlive() then + + local EngageAltitude = math.random( self.EngageFloorAltitude or 500, self.EngageCeilingAltitude or 1000 ) + local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + local DefenderCoord = DefenderGroup:GetPointVec3() + DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) + + local EngageRoute = {} + local AttackTasks = {} + + local FromWP = DefenderCoord:WaypointAir( + self.EngageAltType 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 = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) + local ToWP = DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( + self.EngageAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + EngageSpeed, + true + ) + EngageRoute[#EngageRoute+1] = ToWP + + if TargetDistance <= EngageDistance * 3 then + + local AttackUnitTasks = self:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) + + if #AttackUnitTasks == 0 then + self:I( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + return + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + DefenderGroup:OptionKeepWeaponsOnThreat() + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) + end + end + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_AIR_ENGAGE.___Engage", self, AttackSetUnit ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + + DefenderGroup:Route( EngageRoute, self.TaskDelay ) + + end + else + self:I( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( self.TaskDelay ) + return + end +end + +--- @param Wrapper.Group#GROUP AIEngage +function AI_AIR_ENGAGE.Resume( AIEngage, Fsm ) + + AIEngage:F( { "Resume:", AIEngage:GetName() } ) + if AIEngage:IsAlive() then + Fsm:__Reset( Fsm.TaskDelay ) + Fsm:__EngageRoute( Fsm.TaskDelay, Fsm.AttackSetUnit ) + end + +end diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_Air_Patrol.lua similarity index 56% rename from Moose Development/Moose/AI/AI_A2G_Patrol.lua rename to Moose Development/Moose/AI/AI_Air_Patrol.lua index 68e1954ca..53e439f8a 100644 --- a/Moose Development/Moose/AI/AI_A2G_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Air_Patrol.lua @@ -6,19 +6,19 @@ -- -- === -- --- @module AI.AI_A2G_Patrol +-- @module AI.AI_Air_Patrol -- @image AI_Air_To_Ground_Patrol.JPG ---- @type AI_A2G_PATROL --- @extends AI.AI_A2G_Engage#AI_A2G_ENGAGE +--- @type AI_AIR_PATROL +-- @extends AI.AI_Air#AI_AIR ---- The AI_A2G_PATROL class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} +--- The AI_AIR_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. +-- The AI_AIR_PATROL is assigned a @{Wrapper.Group} and this must be done before the AI_AIR_PATROL process can be started using the **Start** event. -- -- ![Process](..\Presentations\AI_CAP\Dia4.JPG) -- @@ -44,32 +44,32 @@ -- -- ![Process](..\Presentations\AI_CAP\Dia13.JPG) -- --- ## 1. AI_A2G_PATROL constructor +-- ## 1. AI_AIR_PATROL constructor -- --- * @{#AI_A2G_PATROL.New}(): Creates a new AI_A2G_PATROL object. +-- * @{#AI_AIR_PATROL.New}(): Creates a new AI_AIR_PATROL object. -- --- ## 2. AI_A2G_PATROL is a FSM +-- ## 2. AI_AIR_PATROL is a FSM -- -- ![Process](..\Presentations\AI_CAP\Dia2.JPG) -- --- ### 2.1 AI_A2G_PATROL States +-- ### 2.1 AI_AIR_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 +-- ### 2.2 AI_AIR_PATROL Events -- -- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. -- * **@{AI.AI_Patrol#AI_PATROL_ZONE.PatrolRoute}**: 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_AIR_PATROL.Engage}**: Let the AI engage the bogeys. +-- * **@{#AI_AIR_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. +-- * **@{#AI_AIR_PATROL.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_AIR_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 @@ -80,7 +80,7 @@ -- 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. +-- Use the method @{AI.AI_CAP#AI_AIR_PATROL.SetEngageRange}() to define that range. -- -- ## 4. Set the Zone of Engagement -- @@ -88,33 +88,29 @@ -- -- 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. +-- Use the method @{AI.AI_Cap#AI_AIR_PATROL.SetEngageZone}() to define that Zone. -- -- === -- --- @field #AI_A2G_PATROL -AI_A2G_PATROL = { - ClassName = "AI_A2G_PATROL", +-- @field #AI_AIR_PATROL +AI_AIR_PATROL = { + ClassName = "AI_AIR_PATROL", } ---- Creates a new AI_A2G_PATROL object --- @param #AI_A2G_PATROL self +--- Creates a new AI_AIR_PATROL object +-- @param #AI_AIR_PATROL 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. -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. -- @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( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) +-- @return #AI_AIR_PATROL +function AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) ) -- #AI_A2G_PATROL + local self = BASE:Inherit( self, AI_Air ) -- #AI_AIR_PATROL local SpeedMax = AIGroup:GetSpeedMax() @@ -131,8 +127,8 @@ function AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor self:AddTransition( { "Started", "Airborne", "Refuelling" }, "Patrol", "Patrolling" ) --- OnBefore Transition Handler for Event Patrol. - -- @function [parent=#AI_A2G_PATROL] OnBeforePatrol - -- @param #AI_A2G_PATROL self + -- @function [parent=#AI_AIR_PATROL] OnBeforePatrol + -- @param #AI_AIR_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. @@ -140,25 +136,25 @@ function AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @return #boolean Return false to cancel Transition. --- OnAfter Transition Handler for Event Patrol. - -- @function [parent=#AI_A2G_PATROL] OnAfterPatrol - -- @param #AI_A2G_PATROL self + -- @function [parent=#AI_AIR_PATROL] OnAfterPatrol + -- @param #AI_AIR_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 Patrol. - -- @function [parent=#AI_A2G_PATROL] Patrol - -- @param #AI_A2G_PATROL self + -- @function [parent=#AI_AIR_PATROL] Patrol + -- @param #AI_AIR_PATROL self --- Asynchronous Event Trigger for Event Patrol. - -- @function [parent=#AI_A2G_PATROL] __Patrol - -- @param #AI_A2G_PATROL self + -- @function [parent=#AI_AIR_PATROL] __Patrol + -- @param #AI_AIR_PATROL self -- @param #number Delay The delay in seconds. --- OnLeave Transition Handler for State Patrolling. - -- @function [parent=#AI_A2G_PATROL] OnLeavePatrolling - -- @param #AI_A2G_PATROL self + -- @function [parent=#AI_AIR_PATROL] OnLeavePatrolling + -- @param #AI_AIR_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. @@ -166,18 +162,18 @@ function AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @return #boolean Return false to cancel Transition. --- OnEnter Transition Handler for State Patrolling. - -- @function [parent=#AI_A2G_PATROL] OnEnterPatrolling - -- @param #AI_A2G_PATROL self + -- @function [parent=#AI_AIR_PATROL] OnEnterPatrolling + -- @param #AI_AIR_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( "Patrolling", "PatrolRoute", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + self:AddTransition( "Patrolling", "PatrolRoute", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_PATROL. --- OnBefore Transition Handler for Event PatrolRoute. - -- @function [parent=#AI_A2G_PATROL] OnBeforePatrolRoute - -- @param #AI_A2G_PATROL self + -- @function [parent=#AI_AIR_PATROL] OnBeforePatrolRoute + -- @param #AI_AIR_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. @@ -185,34 +181,34 @@ function AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloor -- @return #boolean Return false to cancel Transition. --- OnAfter Transition Handler for Event PatrolRoute. - -- @function [parent=#AI_A2G_PATROL] OnAfterPatrolRoute - -- @param #AI_A2G_PATROL self + -- @function [parent=#AI_AIR_PATROL] OnAfterPatrolRoute + -- @param #AI_AIR_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 PatrolRoute. - -- @function [parent=#AI_A2G_PATROL] PatrolRoute - -- @param #AI_A2G_PATROL self + -- @function [parent=#AI_AIR_PATROL] PatrolRoute + -- @param #AI_AIR_PATROL self --- Asynchronous Event Trigger for Event PatrolRoute. - -- @function [parent=#AI_A2G_PATROL] __PatrolRoute - -- @param #AI_A2G_PATROL self + -- @function [parent=#AI_AIR_PATROL] __PatrolRoute + -- @param #AI_AIR_PATROL self -- @param #number Delay The delay in seconds. - self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_PATROL. return self end --- Set the Engage Range when the AI will engage with airborne enemies. --- @param #AI_A2G_PATROL self +-- @param #AI_AIR_PATROL self -- @param #number EngageRange The Engage Range. --- @return #AI_A2G_PATROL self -function AI_A2G_PATROL:SetEngageRange( EngageRange ) +-- @return #AI_AIR_PATROL self +function AI_AIR_PATROL:SetEngageRange( EngageRange ) self:F2() if EngageRange then @@ -222,14 +218,44 @@ function AI_A2G_PATROL:SetEngageRange( EngageRange ) end end +--- Set race track parameters. CAP flights will perform race track patterns rather than randomly patrolling the zone. +-- @param #AI_AIR_PATROL self +-- @param #number LegMin Min Length of the race track leg in meters. Default 10,000 m. +-- @param #number LegMax Max length of the race track leg in meters. Default 15,000 m. +-- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North. +-- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from South to North. +-- @param #number DurationMin (Optional) Min duration before switching the orbit position. Default is keep same orbit until RTB or engage. +-- @param #number DurationMax (Optional) Max duration before switching the orbit position. Default is keep same orbit until RTB or engage. +-- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. +-- @return #AI_AIR_PATROL self +function AI_AIR_PATROL:SetRaceTrackPattern(LegMin, LegMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) + + self.racetrack=true + self.racetracklegmin=LegMin or 10000 + self.racetracklegmax=LegMax or 15000 + self.racetrackheadingmin=HeadingMin or 0 + self.racetrackheadingmax=HeadingMax or 180 + self.racetrackdurationmin=DurationMin + self.racetrackdurationmax=DurationMax + + if self.racetrackdurationmax and not self.racetrackdurationmin then + self.racetrackdurationmin=self.racetrackdurationmax + end + + self.racetrackcapcoordinates=CapCoordinates + +end + + + --- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_A2G_PATROL self --- @return #AI_A2G_PATROL self +-- @param #AI_AIR_PATROL self +-- @return #AI_AIR_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 ) +function AI_AIR_PATROL:onafterPatrol( AIPatrol, From, Event, To ) self:F2() self:ClearTargetDistance() @@ -247,9 +273,9 @@ end --- @param Wrapper.Group#GROUP AIPatrol -- 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 ) +function AI_AIR_PATROL.___PatrolRoute( AIPatrol, Fsm ) - AIPatrol:F( { "AI_A2G_PATROL.___PatrolRoute:", AIPatrol:GetName() } ) + AIPatrol:F( { "AI_AIR_PATROL.___PatrolRoute:", AIPatrol:GetName() } ) if AIPatrol:IsAlive() then Fsm:PatrolRoute() @@ -258,12 +284,12 @@ function AI_A2G_PATROL.___PatrolRoute( AIPatrol, Fsm ) end --- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_A2G_PATROL self +-- @param #AI_AIR_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:onafterPatrolRoute( AIPatrol, From, Event, To ) +function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) self:F2() @@ -295,34 +321,82 @@ function AI_A2G_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) true ) PatrolRoute[#PatrolRoute+1] = FromWP - - --- Create a route point of type air. - local ToWP = ToTargetCoord:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - PatrolRoute[#PatrolRoute+1] = ToWP - - local Tasks = {} - Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2G_PATROL.___PatrolRoute", self ) - PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) - - AIPatrol:OptionROEReturnFire() - AIPatrol:OptionROTEvadeFire() + if self.racetrack then + + -- Random heading. + local heading = math.random(self.racetrackheadingmin, self.racetrackheadingmax) + + -- Random leg length. + local leg=math.random(self.racetracklegmin, self.racetracklegmax) + + -- Random duration if any. + local duration = self.racetrackdurationmin + if self.racetrackdurationmax then + duration=math.random(self.racetrackdurationmin, self.racetrackdurationmax) + end + + -- CAP coordinate. + local c0=self.PatrolZone:GetRandomCoordinate() + if self.racetrackcapcoordinates and #self.racetrackcapcoordinates>0 then + c0=self.racetrackcapcoordinates[math.random(#self.racetrackcapcoordinates)] + end + + -- Race track points. + local c1=c0:SetAltitude(altitude) --Core.Point#COORDINATE + local c2=c1:Translate(leg, heading):SetAltitude(altitude) + + self:SetTargetDistance(c0) -- For RTB status check + + -- Debug: + self:T(string.format("Patrol zone race track: v=%.1f knots, h=%.1f ft, heading=%03d, leg=%d m, t=%s sec", UTILS.KmphToKnots(speedkmh), UTILS.MetersToFeet(altitude), heading, leg, tostring(duration))) + --c1:MarkToAll("Race track c1") + --c2:MarkToAll("Race track c2") - AIPatrol:Route( PatrolRoute, self.TaskDelay ) + -- Task to orbit. + local taskOrbit=AIPatrol:TaskOrbit(c1, altitude, UTILS.KmphToMps(speedkmh), c2) + + -- Task function to redo the patrol at other random position. + local taskPatrol=AIPatrol:TaskFunction("AI_A2A_PATROL.PatrolRoute", self) + + -- Controlled task with task condition. + local taskCond=AIPatrol:TaskCondition(nil, nil, nil, nil, duration, nil) + local taskCont=AIPatrol:TaskControlled(taskOrbit, taskCond) + + -- Second waypoint + PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskCont, taskPatrol}, "CAP Orbit") + + else + + --- Create a route point of type air. + local ToWP = ToTargetCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + PatrolRoute[#PatrolRoute+1] = ToWP + + local Tasks = {} + Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_AIR_PATROL.___PatrolRoute", self ) + PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) + + AIPatrol:OptionROEReturnFire() + AIPatrol:OptionROTEvadeFire() + + AIPatrol:Route( PatrolRoute, self.TaskDelay ) + end + end end --- @param Wrapper.Group#GROUP AIPatrol -function AI_A2G_PATROL.Resume( AIPatrol, Fsm ) +function AI_AIR_PATROL.Resume( AIPatrol, Fsm ) - AIPatrol:F( { "AI_A2G_PATROL.Resume:", AIPatrol:GetName() } ) + AIPatrol:F( { "AI_AIR_PATROL.Resume:", AIPatrol:GetName() } ) if AIPatrol:IsAlive() then Fsm:__Reset( Fsm.TaskDelay ) Fsm:__PatrolRoute( Fsm.TaskDelay ) diff --git a/Moose Development/Moose/Core/RadioSpeech.lua b/Moose Development/Moose/Core/RadioSpeech.lua index 618cb5161..cb8ce9cfd 100644 --- a/Moose Development/Moose/Core/RadioSpeech.lua +++ b/Moose Development/Moose/Core/RadioSpeech.lua @@ -58,6 +58,14 @@ RADIOSPEECH.Vocabulary.EN = { ["80"] = { "80", 0.26 }, ["90"] = { "90", 0.36 }, ["100"] = { "100", 0.55 }, + ["200"] = { "200", 0.55 }, + ["300"] = { "300", 0.61 }, + ["400"] = { "400", 0.60 }, + ["500"] = { "500", 0.61 }, + ["600"] = { "600", 0.65 }, + ["700"] = { "700", 0.70 }, + ["800"] = { "800", 0.54 }, + ["900"] = { "900", 0.60 }, ["1000"] = { "1000", 1 }, ["chevy"] = { "chevy", 0.35 }, @@ -70,7 +78,6 @@ RADIOSPEECH.Vocabulary.EN = { ["uzi"] = { "uzi", 0.28 }, ["degrees"] = { "degrees", 0.5 }, - ["�"] = { "degrees", 0.5 }, ["kilometers"] = { "kilometers", 0.65 }, ["km"] = { "kilometers", 0.65 }, ["miles"] = { "miles", 0.45 }, @@ -81,7 +88,7 @@ RADIOSPEECH.Vocabulary.EN = { ["returning to base"] = { "returning_to_base", 0.85 }, - ["moving on to ground target"] = { "moving_on_to_ground_target", 1.20 }, + ["on route to ground target"] = { "on_route_to_ground_target", 1.05 }, ["engaging ground target"] = { "engaging_ground_target", 1.20 }, ["wheels up"] = { "wheels_up", 0.42 }, ["landing at base"] = { "landing at base", 0.8 }, diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 0a499fe25..da8a13e96 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -71,17 +71,15 @@ __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_Air_Patrol.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Air_Engage.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' ) From fb875077d76a58ad85da3d8bbf96ec9176db954d Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sun, 22 Sep 2019 20:30:59 +0200 Subject: [PATCH 370/485] Speech optimizations for A2A dispatcher --- .../Moose/AI/AI_A2A_Dispatcher.lua | 66 ++++++++++++++----- Moose Development/Moose/Core/RadioQueue.lua | 2 +- Moose Development/Moose/Core/RadioSpeech.lua | 13 +++- 3 files changed, 61 insertions(+), 20 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 43d74920d..b03fec008 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -3157,15 +3157,15 @@ do -- AI_A2A_DISPATCHER if DefenderCAP then - local Fsm = AI_A2A_CAP:New2( DefenderCAP, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.EngageFloorAltitude, Cap.EngageCeilingAltitude, Cap.EngageAltType, Cap.Zone, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.PatrolFloorAltitude, Cap.PatrolCeilingAltitude, Cap.PatrolAltType ) - 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 ) + local AI_A2A_Fsm = AI_A2A_CAP:New2( DefenderCAP, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.EngageFloorAltitude, Cap.EngageCeilingAltitude, Cap.EngageAltType, Cap.Zone, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.PatrolFloorAltitude, Cap.PatrolCeilingAltitude, Cap.PatrolAltType ) + AI_A2A_Fsm:SetDispatcher( self ) + AI_A2A_Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + AI_A2A_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + AI_A2A_Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + AI_A2A_Fsm:SetDisengageRadius( self.DisengageRadius ) + AI_A2A_Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) if DefenderSquadron.Racetrack or self.DefenderDefault.Racetrack then - Fsm:SetRaceTrackPattern(DefenderSquadron.RacetrackLengthMin or self.DefenderDefault.RacetrackLengthMin, + AI_A2A_Fsm:SetRaceTrackPattern(DefenderSquadron.RacetrackLengthMin or self.DefenderDefault.RacetrackLengthMin, DefenderSquadron.RacetrackLengthMax or self.DefenderDefault.RacetrackLengthMax, DefenderSquadron.RacetrackHeadingMin or self.DefenderDefault.RacetrackHeadingMin, DefenderSquadron.RacetrackHeadingMax or self.DefenderDefault.RacetrackHeadingMax, @@ -3173,25 +3173,39 @@ do -- AI_A2A_DISPATCHER DefenderSquadron.RacetrackDurationMax or self.DefenderDefault.RacetrackDurationMax, DefenderSquadron.RacetrackCoordinates or self.DefenderDefault.RacetrackCoordinates) end - Fsm:Start() + AI_A2A_Fsm:Start() - self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", Fsm ) + self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", AI_A2A_Fsm ) - function Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + function AI_A2A_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) self:F({"CAP Takeoff", DefenderGroup:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) local DefenderName = DefenderGroup:GetCallsign() - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER + local Dispatcher = AI_A2A_Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then Dispatcher:MessageToPlayers( DefenderName .. " Wheels up.", DefenderGroup ) - Fsm:__Patrol( 2 ) -- Start Patrolling + AI_A2A_Fsm:__Patrol( 2 ) -- Start Patrolling end end - function Fsm:onafterRTB( DefenderGroup, From, Event, To ) + function AI_A2A_Fsm:onafterPatrolRoute( DefenderGroup, From, Event, To ) + self:F({"CAP PatrolRoute", DefenderGroup:GetName()}) + self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if Squadron then + Dispatcher:MessageToPlayers( DefenderName .. ", patrolling.", DefenderGroup ) + end + + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end + + function AI_A2A_Fsm:onafterRTB( DefenderGroup, From, Event, To ) self:F({"CAP RTB", DefenderGroup:GetName()}) @@ -3205,7 +3219,7 @@ do -- AI_A2A_DISPATCHER end --- @param #AI_A2A_DISPATCHER self - function Fsm:onafterHome( Defender, From, Event, To, Action ) + function AI_A2A_Fsm:onafterHome( Defender, From, Event, To, Action ) self:F({"CAP Home", Defender:GetName()}) self:GetParent(self).onafterHome( self, Defender, From, Event, To ) @@ -3278,7 +3292,7 @@ do -- AI_A2A_DISPATCHER for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) - Fsm:__EngageRoute( 1, AttackerSet ) -- Engage on the TargetSetUnit + Fsm:__EngageRoute( 0.1, AttackerSet ) -- Engage on the TargetSetUnit self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) @@ -3395,7 +3409,7 @@ do -- AI_A2A_DISPATCHER local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - if Squadron then + if Squadron and AttackSetUnit:Count() > 0 then local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE @@ -3404,6 +3418,22 @@ do -- AI_A2A_DISPATCHER self:GetParent( Fsm ).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) end + function Fsm:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"GCI Engage", DefenderGroup:GetName()}) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron and AttackSetUnit:Count() > 0 then + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + + Dispatcher:MessageToPlayers( DefenderName .. ", engaging bogeys at " .. Coordinate:ToStringA2A( DefenderGroup ), DefenderGroup ) + end + self:GetParent( Fsm ).onafterEngage( self, DefenderGroup, From, Event, To, AttackSetUnit ) + end + function Fsm:onafterRTB( DefenderGroup, From, Event, To ) self:F({"GCI RTB", DefenderGroup:GetName()}) self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) @@ -3437,7 +3467,7 @@ do -- AI_A2A_DISPATCHER local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing at base.", DefenderGroup ) + Dispatcher:MessageToPlayers( DefenderName .. " landing at base.", DefenderGroup ) if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) diff --git a/Moose Development/Moose/Core/RadioQueue.lua b/Moose Development/Moose/Core/RadioQueue.lua index 8ee0d7749..f1980102c 100644 --- a/Moose Development/Moose/Core/RadioQueue.lua +++ b/Moose Development/Moose/Core/RadioQueue.lua @@ -470,7 +470,7 @@ function RADIOQUEUE:_GetRadioSenderCoord() end -- Now try a static. - local sender=STATIC:FindByName(self.sendername) + local sender=STATIC:FindByName( self.sendername, false ) -- Check that sender is alive and an aircraft. if sender then diff --git a/Moose Development/Moose/Core/RadioSpeech.lua b/Moose Development/Moose/Core/RadioSpeech.lua index cb8ce9cfd..e54068c3c 100644 --- a/Moose Development/Moose/Core/RadioSpeech.lua +++ b/Moose Development/Moose/Core/RadioSpeech.lua @@ -66,7 +66,15 @@ RADIOSPEECH.Vocabulary.EN = { ["700"] = { "700", 0.70 }, ["800"] = { "800", 0.54 }, ["900"] = { "900", 0.60 }, - ["1000"] = { "1000", 1 }, + ["1000"] = { "1000", 0.60 }, + ["2000"] = { "2000", 0.61 }, + ["3000"] = { "3000", 0.64 }, + ["4000"] = { "4000", 0.62 }, + ["5000"] = { "5000", 0.69 }, + ["6000"] = { "6000", 0.69 }, + ["7000"] = { "7000", 0.75 }, + ["8000"] = { "8000", 0.59 }, + ["9000"] = { "9000", 0.65 }, ["chevy"] = { "chevy", 0.35 }, ["colt"] = { "colt", 0.35 }, @@ -81,6 +89,7 @@ RADIOSPEECH.Vocabulary.EN = { ["kilometers"] = { "kilometers", 0.65 }, ["km"] = { "kilometers", 0.65 }, ["miles"] = { "miles", 0.45 }, + ["meters"] = { "meters", 0.41 }, ["mi"] = { "miles", 0.45 }, ["br"] = { "br", 1.1 }, @@ -89,7 +98,9 @@ RADIOSPEECH.Vocabulary.EN = { ["returning to base"] = { "returning_to_base", 0.85 }, ["on route to ground target"] = { "on_route_to_ground_target", 1.05 }, + ["intercepting bogeys"] = { "intercepting_bogeys", 1.00 }, ["engaging ground target"] = { "engaging_ground_target", 1.20 }, + ["engaging bogeys"] = { "engaging_bogeys", 0.81 }, ["wheels up"] = { "wheels_up", 0.42 }, ["landing at base"] = { "landing at base", 0.8 }, ["patrolling"] = { "patrolling", 0.55 }, From 5cdaf53727e5931bd4fa56e10b11e3a85c445e73 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Wed, 25 Sep 2019 17:53:11 +0300 Subject: [PATCH 371/485] - Added a NoTrace option to the scheduler, so for those schedulers that have a schedule in microseconds, you may wanna use the function NoTrace(), to avoid spamming the dcs.log. - Started with the implementation of multiple languages of speech. Got now a Russian and English prototype working. - Moved radio frequency settings to a squadron, so multiple squadrons can communicate in their own radio frequency. --- .../Moose/AI/AI_A2A_Dispatcher.lua | 98 +++++++- .../Moose/AI/AI_A2G_Dispatcher.lua | 51 +++- .../Moose/AI/AI_Air_Dispatcher.lua | 20 +- Moose Development/Moose/Core/Point.lua | 65 +++-- Moose Development/Moose/Core/RadioQueue.lua | 1 + Moose Development/Moose/Core/RadioSpeech.lua | 229 ++++++++++++------ .../Moose/Core/ScheduleDispatcher.lua | 13 +- Moose Development/Moose/Core/Scheduler.lua | 8 + .../Moose/Tasking/DetectionManager.lua | 32 +-- Moose Development/Moose/Wrapper/Client.lua | 1 + 10 files changed, 359 insertions(+), 159 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index b03fec008..ddf3192f3 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -1685,6 +1685,8 @@ do -- AI_A2A_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:SetSquadronLanguage( SquadronName, "EN" ) -- Squadrons speak English by default. + self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) return self @@ -2830,6 +2832,59 @@ do -- AI_A2A_DISPATCHER end + --- Set the squadron language. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #string Language A string defining the language to be embedded within the miz file. + -- @return #AI_A2A_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Set for English. + -- A2ADispatcher:SetSquadronLanguage( "SquadronName", "EN" ) -- This squadron speaks English. + -- + -- -- Set for Russian. + -- A2ADispatcher:SetSquadronLanguage( "SquadronName", "RU" ) -- This squadron speaks Russian. + function AI_A2A_DISPATCHER:SetSquadronLanguage( SquadronName, Language ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Language = Language + + if DefenderSquadron.RadioQueue then + DefenderSquadron.RadioQueue:SetLanguage( Language ) + end + + return self + end + + + --- Set the frequency of communication and the mode of communication for voice overs. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number RadioFrequency The frequency of communication. + -- @param #number RadioModulation The modulation of communication. + -- @param #number RadioPower The power in Watts of communication. + function AI_A2A_DISPATCHER:SetSquadronRadioFrequency( SquadronName, RadioFrequency, RadioModulation, RadioPower ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.RadioFrequency = RadioFrequency + DefenderSquadron.RadioModulation = RadioModulation or radio.modulation.AM + DefenderSquadron.RadioPower = RadioPower or 100 + + if DefenderSquadron.RadioQueue then + DefenderSquadron.RadioQueue:Stop() + end + + DefenderSquadron.RadioQueue = nil + + DefenderSquadron.RadioQueue = RADIOSPEECH:New( DefenderSquadron.RadioFrequency, DefenderSquadron.RadioModulation ) + DefenderSquadron.RadioQueue.power = DefenderSquadron.RadioPower + DefenderSquadron.RadioQueue:Start( 0.5 ) + + DefenderSquadron.RadioQueue:SetLanguage( DefenderSquadron.Language ) + end --- Add defender to squadron. Resource count will get smaller. -- @param #AI_A2A_DISPATCHER self @@ -3186,7 +3241,7 @@ do -- AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - Dispatcher:MessageToPlayers( DefenderName .. " Wheels up.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " Wheels up.", DefenderGroup ) AI_A2A_Fsm:__Patrol( 2 ) -- Start Patrolling end end @@ -3199,7 +3254,7 @@ do -- AI_A2A_DISPATCHER local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - Dispatcher:MessageToPlayers( DefenderName .. ", patrolling.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup ) end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) @@ -3214,7 +3269,7 @@ do -- AI_A2A_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( DefenderName .. " returning to base.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end @@ -3396,7 +3451,11 @@ do -- AI_A2A_DISPATCHER local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup ) if DefenderTarget then - Dispatcher:MessageToPlayers( DefenderName .. " wheels up.", DefenderGroup ) + if Squadron.Language == "EN" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " wheels up.", DefenderGroup ) + elseif Squadron.Language == "RU" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " колеÑа вверх.", DefenderGroup ) + end --Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end @@ -3412,8 +3471,14 @@ do -- AI_A2A_DISPATCHER if Squadron and AttackSetUnit:Count() > 0 then local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - - Dispatcher:MessageToPlayers( DefenderName .. ", intercepting bogeys at " .. Coordinate:ToStringA2A( DefenderGroup ), DefenderGroup ) + + if Squadron.Language == "EN" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", intercepting bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + elseif Squadron.Language == "RU" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", перехват Ñамолетов в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + elseif Squadron.Language == "DE" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", Eindringlinge abfangen bei" .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + end end self:GetParent( Fsm ).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) end @@ -3429,7 +3494,11 @@ do -- AI_A2A_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - Dispatcher:MessageToPlayers( DefenderName .. ", engaging bogeys at " .. Coordinate:ToStringA2A( DefenderGroup ), DefenderGroup ) + if Squadron.Language == "EN" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + elseif Squadron.Language == "RU" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие Ñамолеты в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + end end self:GetParent( Fsm ).onafterEngage( self, DefenderGroup, From, Event, To, AttackSetUnit ) end @@ -3441,7 +3510,14 @@ do -- AI_A2A_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( DefenderName .. " returning to base.", DefenderGroup ) + + if Squadron then + if Squadron.Language == "EN" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) + elseif Squadron.Language == "RU" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", возвращаÑÑÑŒ на базу.", DefenderGroup ) + end + end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end @@ -3467,7 +3543,11 @@ do -- AI_A2A_DISPATCHER local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( DefenderName .. " landing at base.", DefenderGroup ) + if Squadron.Language == "EN" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " landing at base.", DefenderGroup ) + elseif Squadron.Language == "RU" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие Ñамолеты в поÑадка на базу.", DefenderGroup ) + end if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 3628fca69..d747bcf58 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -3313,6 +3313,31 @@ do -- AI_A2G_DISPATCHER end + --- Set the frequency of communication and the mode of communication for voice overs. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number RadioFrequency The frequency of communication. + -- @param #number RadioModulation The modulation of communication. + -- @param #number RadioPower The power in Watts of communication. + function AI_A2G_DISPATCHER:SetSquadronRadioFrequency( SquadronName, RadioFrequency, RadioModulation, RadioPower ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.RadioFrequency = RadioFrequency + DefenderSquadron.RadioModulation = RadioModulation or radio.modulation.AM + DefenderSquadron.RadioPower = RadioPower or 100 + + if DefenderSquadron.RadioQueue then + DefenderSquadron.RadioQueue:Stop() + end + + DefenderSquadron.RadioQueue = nil + + DefenderSquadron.RadioQueue = RADIOSPEECH:New( DefenderSquadron.RadioFrequency, DefenderSquadron.RadioModulation ) + DefenderSquadron.RadioQueue.power = DefenderSquadron.RadioPower + DefenderSquadron.RadioQueue:Start( 0.5 ) + + DefenderSquadron.RadioQueue:SetLanguage( DefenderSquadron.Language ) + end --- @param #AI_A2G_DISPATCHER self @@ -3674,7 +3699,7 @@ do -- AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - Dispatcher:MessageToPlayers( DefenderName .. ", wheels up.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup ) AI_A2G_Fsm:Patrol() -- Engage on the TargetSetUnit end end @@ -3687,7 +3712,7 @@ do -- AI_A2G_DISPATCHER local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - Dispatcher:MessageToPlayers( DefenderName .. ", patrolling.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup ) end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) @@ -3706,7 +3731,7 @@ do -- AI_A2G_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - Dispatcher:MessageToPlayers( DefenderName .. ", moving on to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", moving on to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) end end @@ -3721,7 +3746,7 @@ do -- AI_A2G_DISPATCHER if FirstUnit then local Coordinate = FirstUnit:GetCoordinate() - Dispatcher:MessageToPlayers( DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) end end @@ -3732,7 +3757,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( DefenderName .. ", returning to base.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup ) Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end @@ -3745,7 +3770,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( DefenderName .. ", lost control." ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", lost control." ) if DefenderGroup:IsAboveRunway() then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) DefenderGroup:Destroy() @@ -3760,7 +3785,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( DefenderName .. ", landing at base.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup ) if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) @@ -3817,7 +3842,7 @@ do -- AI_A2G_DISPATCHER self:F( { DefenderTarget = DefenderTarget } ) if DefenderTarget then - Dispatcher:MessageToPlayers( DefenderName .. ", wheels up.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup ) AI_A2G_Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end @@ -3833,7 +3858,7 @@ do -- AI_A2G_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - Dispatcher:MessageToPlayers( DefenderName .. ", on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) end self:GetParent(self).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) end @@ -3849,7 +3874,7 @@ do -- AI_A2G_DISPATCHER if FirstUnit then local Coordinate = FirstUnit:GetCoordinate() - Dispatcher:MessageToPlayers( DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) end end @@ -3859,7 +3884,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( DefenderName .. ", returning to base.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup ) self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) @@ -3874,7 +3899,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - --Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) + --Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) if DefenderGroup:IsAboveRunway() then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) @@ -3890,7 +3915,7 @@ do -- AI_A2G_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( DefenderName .. ", landing at base.", DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup ) if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) diff --git a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua index 23531f689..459a97aab 100644 --- a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua @@ -2502,7 +2502,7 @@ do -- AI_AIR_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) if Squadron then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne." ) + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne." ) Fsm:Patrol() -- Engage on the TargetSetUnit end end @@ -2514,7 +2514,7 @@ do -- AI_AIR_DISPATCHER local DefenderName = Defender:GetName() local Dispatcher = self:GetDispatcher() -- #AI_AIR_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) Dispatcher:ClearDefenderTaskTarget( Defender ) end @@ -2527,7 +2527,7 @@ do -- AI_AIR_DISPATCHER local DefenderName = Defender:GetName() local Dispatcher = Fsm:GetDispatcher() -- #AI_AIR_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) if Defender:IsAboveRunway() then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) Defender:Destroy() @@ -2542,7 +2542,7 @@ do -- AI_AIR_DISPATCHER local DefenderName = Defender:GetName() local Dispatcher = self:GetDispatcher() -- #AI_AIR_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) @@ -2599,7 +2599,7 @@ do -- AI_AIR_DISPATCHER self:F( { DefenderTarget = DefenderTarget } ) if DefenderTarget then - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne." ) + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne." ) Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end @@ -2614,7 +2614,7 @@ do -- AI_AIR_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route, bearing " .. Coordinate:ToString( Defender ) ) + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route, bearing " .. Coordinate:ToString( Defender ) ) end function Fsm:OnAfterEngage( Defender, From, Event, To, AttackSetUnit ) @@ -2627,7 +2627,7 @@ do -- AI_AIR_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging target, bearing " .. Coordinate:ToString( Defender ) ) + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging target, bearing " .. Coordinate:ToString( Defender ) ) end function Fsm:onafterRTB( Defender, From, Event, To ) @@ -2636,7 +2636,7 @@ do -- AI_AIR_DISPATCHER local DefenderName = Defender:GetName() local Dispatcher = self:GetDispatcher() -- #AI_AIR_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " RTB." ) + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " RTB." ) self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) @@ -2651,7 +2651,7 @@ do -- AI_AIR_DISPATCHER local DefenderName = Defender:GetName() local Dispatcher = Fsm:GetDispatcher() -- #AI_AIR_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) if Defender:IsAboveRunway() then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) @@ -2667,7 +2667,7 @@ do -- AI_AIR_DISPATCHER local DefenderName = Defender:GetName() local Dispatcher = self:GetDispatcher() -- #AI_AIR_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) + Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 3dce6e9a9..8e2dc7648 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -867,7 +867,7 @@ do -- COORDINATE -- @param #number Precision The precision. -- @param Core.Settings#SETTINGS Settings -- @return #string The bearing text in degrees. - function COORDINATE:GetBearingText( AngleRadians, Precision, Settings ) + function COORDINATE:GetBearingText( AngleRadians, Precision, Settings, Language ) local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS @@ -883,16 +883,25 @@ do -- COORDINATE -- @param #number Distance The distance in meters. -- @param Core.Settings#SETTINGS Settings -- @return #string The distance text expressed in the units of measurement. - function COORDINATE:GetDistanceText( Distance, Settings ) + function COORDINATE:GetDistanceText( Distance, Settings, Language ) local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS + local Language = Language or "EN" local DistanceText if Settings:IsMetric() then - DistanceText = " for " .. UTILS.Round( Distance / 1000, 2 ) .. " km" + if Language == "EN" then + DistanceText = " for " .. UTILS.Round( Distance / 1000, 2 ) .. " km" + elseif Language == "RU" then + DistanceText = " за " .. UTILS.Round( Distance / 1000, 2 ) .. " километров" + end else - DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " miles" + if Language == "EN" then + DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " miles" + elseif Language == "RU" then + DistanceText = " за " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " миль" + end end return DistanceText @@ -901,14 +910,24 @@ do -- COORDINATE --- Return the altitude text of the COORDINATE. -- @param #COORDINATE self -- @return #string Altitude text. - function COORDINATE:GetAltitudeText( Settings ) + function COORDINATE:GetAltitudeText( Settings, Language ) local Altitude = self.y local Settings = Settings or _SETTINGS + local Language = Language or "EN" + if Altitude ~= 0 then if Settings:IsMetric() then - return " at " .. UTILS.Round( self.y, -3 ) .. " meters" + if Language == "EN" then + return " at " .. UTILS.Round( self.y, -3 ) .. " meters" + elseif Language == "RU" then + return " в " .. UTILS.Round( self.y, -3 ) .. " метры" + end else - return " at " .. UTILS.Round( UTILS.MetersToFeet( self.y ), -3 ) .. " feet" + if Language == "EN" then + return " at " .. UTILS.Round( UTILS.MetersToFeet( self.y ), -3 ) .. " feet" + elseif Language == "RU" then + return " в " .. UTILS.Round( self.y, -3 ) .. " ноги" + end end else return "" @@ -954,12 +973,12 @@ do -- COORDINATE -- @param #number Distance The distance -- @param Core.Settings#SETTINGS Settings -- @return #string The BR Text - function COORDINATE:GetBRText( AngleRadians, Distance, Settings ) + function COORDINATE:GetBRText( AngleRadians, Distance, Settings, Language ) local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS - local BearingText = self:GetBearingText( AngleRadians, 0, Settings ) - local DistanceText = self:GetDistanceText( Distance, Settings ) + local BearingText = self:GetBearingText( AngleRadians, 0, Settings, Language ) + local DistanceText = self:GetDistanceText( Distance, Settings, Language ) local BRText = BearingText .. DistanceText @@ -972,13 +991,13 @@ do -- COORDINATE -- @param #number Distance The distance -- @param Core.Settings#SETTINGS Settings -- @return #string The BRA Text - function COORDINATE:GetBRAText( AngleRadians, Distance, Settings ) + function COORDINATE:GetBRAText( AngleRadians, Distance, Settings, Language ) local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS - local BearingText = self:GetBearingText( AngleRadians, 0, Settings ) - local DistanceText = self:GetDistanceText( Distance, Settings ) - local AltitudeText = self:GetAltitudeText( Settings ) + local BearingText = self:GetBearingText( AngleRadians, 0, Settings, Language ) + local DistanceText = self:GetDistanceText( Distance, Settings, Language ) + local AltitudeText = self:GetAltitudeText( Settings, Language ) local BRAText = BearingText .. DistanceText .. AltitudeText -- When the POINT is a VEC2, there will be no altitude shown. @@ -1867,12 +1886,12 @@ do -- COORDINATE -- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from. -- @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 BR text. - function COORDINATE:ToStringBRA( FromCoordinate, Settings ) + function COORDINATE:ToStringBRA( FromCoordinate, Settings, Language ) local DirectionVec3 = FromCoordinate:GetDirectionVec3( self ) local AngleRadians = self:GetAngleRadians( DirectionVec3 ) local Distance = FromCoordinate:Get2DDistance( self ) local Altitude = self:GetAltitudeText() - return "BRA, " .. self:GetBRAText( AngleRadians, Distance, Settings ) + return "BRA, " .. self:GetBRAText( AngleRadians, Distance, Settings, Language ) end --- Return a BULLS string out of the BULLS of the coalition to the COORDINATE. @@ -2023,7 +2042,7 @@ 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:ToStringA2A( Controllable, Settings ) -- R2.2 + function COORDINATE:ToStringA2A( Controllable, Settings, Language ) -- R2.2 self:F2( { Controllable = Controllable and Controllable:GetName() } ) @@ -2032,23 +2051,23 @@ do -- COORDINATE if Settings:IsA2A_BRAA() then if Controllable then local Coordinate = Controllable:GetCoordinate() - return self:ToStringBRA( Coordinate, Settings ) + return self:ToStringBRA( Coordinate, Settings, Language ) else - return self:ToStringMGRS( Settings ) + return self:ToStringMGRS( Settings, Language ) end end if Settings:IsA2A_BULLS() then local Coalition = Controllable:GetCoalition() - return self:ToStringBULLS( Coalition, Settings ) + return self:ToStringBULLS( Coalition, Settings, Language ) end if Settings:IsA2A_LL_DMS() then - return self:ToStringLLDMS( Settings ) + return self:ToStringLLDMS( Settings, Language ) end if Settings:IsA2A_LL_DDM() then - return self:ToStringLLDDM( Settings ) + return self:ToStringLLDDM( Settings, Language ) end if Settings:IsA2A_MGRS() then - return self:ToStringMGRS( Settings ) + return self:ToStringMGRS( Settings, Language ) end return nil diff --git a/Moose Development/Moose/Core/RadioQueue.lua b/Moose Development/Moose/Core/RadioQueue.lua index f1980102c..302b3fd4c 100644 --- a/Moose Development/Moose/Core/RadioQueue.lua +++ b/Moose Development/Moose/Core/RadioQueue.lua @@ -81,6 +81,7 @@ function RADIOQUEUE:New(frequency, modulation) -- Scheduler self.scheduler=SCHEDULER:New() + self.scheduler:NoTrace() return self end diff --git a/Moose Development/Moose/Core/RadioSpeech.lua b/Moose Development/Moose/Core/RadioSpeech.lua index e54068c3c..d4cf22af1 100644 --- a/Moose Development/Moose/Core/RadioSpeech.lua +++ b/Moose Development/Moose/Core/RadioSpeech.lua @@ -11,7 +11,7 @@ -- -- ### Authors: FlightControl -- --- @module Core.Speech +-- @module Core.RadioSpeech -- @image Core_Radio.JPG --- Makes the radio speak. @@ -25,6 +25,8 @@ RADIOSPEECH = { ClassName = "RADIOSPEECH", Vocabulary = { EN = {}, + DE = {}, + RU = {}, } } @@ -91,6 +93,7 @@ RADIOSPEECH.Vocabulary.EN = { ["miles"] = { "miles", 0.45 }, ["meters"] = { "meters", 0.41 }, ["mi"] = { "miles", 0.45 }, + ["feet"] = { "feet", 0.29 }, ["br"] = { "br", 1.1 }, ["bra"] = { "bra", 0.3 }, @@ -112,7 +115,81 @@ RADIOSPEECH.Vocabulary.EN = { ["defender"] = { "defender", 0.45 }, } +RADIOSPEECH.Vocabulary.RU = { + ["1"] = { "1", 0.34 }, + ["2"] = { "2", 0.30 }, + ["3"] = { "3", 0.23 }, + ["4"] = { "4", 0.51 }, + ["5"] = { "5", 0.31 }, + ["6"] = { "6", 0.44 }, + ["7"] = { "7", 0.25 }, + ["8"] = { "8", 0.43 }, + ["9"] = { "9", 0.45 }, + ["10"] = { "10", 0.53 }, + ["11"] = { "11", 0.66 }, + ["12"] = { "12", 0.70 }, + ["13"] = { "13", 0.66 }, + ["14"] = { "14", 0.80 }, + ["15"] = { "15", 0.65 }, + ["16"] = { "16", 0.75 }, + ["17"] = { "17", 0.74 }, + ["18"] = { "18", 0.85 }, + ["19"] = { "19", 0.80 }, + ["20"] = { "20", 0.58 }, + ["30"] = { "30", 0.51 }, + ["40"] = { "40", 0.51 }, + ["50"] = { "50", 0.67 }, + ["60"] = { "60", 0.76 }, + ["70"] = { "70", 0.68 }, + ["80"] = { "80", 0.84 }, + ["90"] = { "90", 0.71 }, + ["100"] = { "100", 0.35 }, + ["200"] = { "200", 0.59 }, + ["300"] = { "300", 0.53 }, + ["400"] = { "400", 0.70 }, + ["500"] = { "500", 0.50 }, + ["600"] = { "600", 0.58 }, + ["700"] = { "700", 0.64 }, + ["800"] = { "800", 0.77 }, + ["900"] = { "900", 0.75 }, + ["1000"] = { "1000", 0.87 }, + ["2000"] = { "2000", 0.83 }, + ["3000"] = { "3000", 0.84 }, + ["4000"] = { "4000", 1.00 }, + ["5000"] = { "5000", 0.77 }, + ["6000"] = { "6000", 0.90 }, + ["7000"] = { "7000", 0.77 }, + ["8000"] = { "8000", 0.92 }, + ["9000"] = { "9000", 0.87 }, + ["Ñтепени"] = { "degrees", 0.5 }, + ["километров"] = { "kilometers", 0.65 }, + ["km"] = { "kilometers", 0.65 }, + ["миль"] = { "miles", 0.45 }, + ["mi"] = { "miles", 0.45 }, + ["метры"] = { "meters", 0.41 }, + ["m"] = { "meters", 0.41 }, + ["ноги"] = { "feet", 0.37 }, + + ["br"] = { "br", 1.1 }, + ["bra"] = { "bra", 0.3 }, + + + ["возвращаÑÑÑŒ на базу"] = { "returning_to_base", 1.40 }, + ["на пути к наземной цели"] = { "on_route_to_ground_target", 1.45 }, + ["перехват Ñамолетов"] = { "intercepting_bogeys", 1.22 }, + ["поражение наземной цели"] = { "engaging_ground_target", 1.53 }, + ["захватывающие Ñамолеты"] = { "engaging_bogeys", 1.68 }, + ["колеÑа вверх"] = { "wheels_up", 0.92 }, + ["поÑадка на базу"] = { "landing at base", 1.04 }, + ["патрулирующий"] = { "patrolling", 0.96 }, + + ["за"] = { "for", 0.27 }, + ["и"] = { "and", 0.17 }, + ["в"] = { "at", 0.19 }, + ["dot"] = { "dot", 0.51 }, + ["defender"] = { "defender", 0.45 }, +} --- Create a new RADIOSPEECH object for a given radio frequency/modulation. -- @param #RADIOSPEECH self @@ -131,6 +208,11 @@ function RADIOSPEECH:New(frequency, modulation) return self end +function RADIOSPEECH:SetLanguage( Langauge ) + + self.Language = Langauge +end + --- Add Sentence to the Speech collection. -- @param #RADIOSPEECH self @@ -143,7 +225,7 @@ function RADIOSPEECH:AddSentenceToSpeech( RemainingSentence, Speech, Sentence, D self:I( { RemainingSentence, Speech, Sentence, Data } ) - local Token, RemainingSentence = RemainingSentence:match( "^ *(%w+)(.*)" ) + local Token, RemainingSentence = RemainingSentence:match( "^ *([^ ]+)(.*)" ) self:I( { Token = Token, RemainingSentence = RemainingSentence } ) -- Is there a Token? @@ -198,122 +280,117 @@ end --- Speak a sentence. -- @param #RADIOSPEECH self -- @param #string Sentence The sentence to be spoken. -function RADIOSPEECH:SpeakWords( Sentence, Speech ) +function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) - local Word, RemainderSentence = Sentence:match( "^[^%d%a]*(%a*)(.*)" ) + local OriginalSentence = Sentence + + -- lua does not parse UTF-8, so the match statement will fail on cyrillic using %a. + -- therefore, the only way to parse the statement is to use blank, comma or dot as a delimiter. + -- and then check if the character can be converted to a number or not. + local Word, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" ) self:I( { Word = Word, Speech = Speech[Word], RemainderSentence = RemainderSentence } ) + - if Word and Word ~= "" then + if Word then + if Word ~= "" and tonumber(Word) == nil then - -- Construct of words - Word = Word:lower() - if Speech[Word] then - -- The end of the sentence has been reached. Now Speech.Next should be nil, otherwise there is an error. - if Speech[Word].Next == nil then - self:I( { Sentence = Speech[Word].Sentence, Data = Speech[Word].Data } ) - self:NewTransmission( Speech[Word].Data[1] .. ".wav", Speech[Word].Data[2], "EN/" ) - else - if RemainderSentence and RemainderSentence ~= "" then - RemainderSentence = self:SpeakWords( RemainderSentence, Speech[Word].Next ) + -- Construct of words + Word = Word:lower() + if Speech[Word] then + -- The end of the sentence has been reached. Now Speech.Next should be nil, otherwise there is an error. + if Speech[Word].Next == nil then + self:I( { Sentence = Speech[Word].Sentence, Data = Speech[Word].Data } ) + self:NewTransmission( Speech[Word].Data[1] .. ".wav", Speech[Word].Data[2], Language .. "/" ) + else + if RemainderSentence and RemainderSentence ~= "" then + return self:SpeakWords( RemainderSentence, Speech[Word].Next, Language ) + end end end + return RemainderSentence end + return OriginalSentence + else + return "" end - return RemainderSentence - end --- Speak a sentence. -- @param #RADIOSPEECH self -- @param #string Sentence The sentence to be spoken. -function RADIOSPEECH:SpeakDigits( Sentence, Speech ) +function RADIOSPEECH:SpeakDigits( Sentence, Speech, Langauge ) - local Digits, RemainderSentence = Sentence:match( "^[^%a%d]*(%d*)(.*)" ) + local OriginalSentence = Sentence + + -- lua does not parse UTF-8, so the match statement will fail on cyrillic using %a. + -- therefore, the only way to parse the statement is to use blank, comma or dot as a delimiter. + -- and then check if the character can be converted to a number or not. + local Digits, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" ) self:I( { Digits = Digits, Speech = Speech[Digits], RemainderSentence = RemainderSentence } ) - if Digits and Digits ~= "" then + if Digits then + if Digits ~= "" and tonumber( Digits ) ~= nil then - -- Construct numbers - local Number = tonumber( Digits ) - local Multiple = nil - while Number >= 0 do - if Number > 1000 then - Multiple = math.floor( Number / 1000 ) * 1000 - elseif Number > 100 then - Multiple = math.floor( Number / 100 ) * 100 - elseif Number > 20 then - Multiple = math.floor( Number / 10 ) * 10 - elseif Number >= 0 then - Multiple = Number + -- Construct numbers + local Number = tonumber( Digits ) + local Multiple = nil + while Number >= 0 do + if Number > 1000 then + Multiple = math.floor( Number / 1000 ) * 1000 + elseif Number > 100 then + Multiple = math.floor( Number / 100 ) * 100 + elseif Number > 20 then + Multiple = math.floor( Number / 10 ) * 10 + elseif Number >= 0 then + Multiple = Number + end + Sentence = tostring( Multiple ) + if Speech[Sentence] then + self:I( { Speech = Speech[Sentence].Sentence, Data = Speech[Sentence].Data } ) + self:NewTransmission( Speech[Sentence].Data[1] .. ".wav", Speech[Sentence].Data[2], Langauge .. "/" ) + end + Number = Number - Multiple + Number = ( Number == 0 ) and -1 or Number end - Sentence = tostring( Multiple ) - if Speech[Sentence] then - self:I( { Speech = Speech[Sentence].Sentence, Data = Speech[Sentence].Data } ) - self:NewTransmission( Speech[Sentence].Data[1] .. ".wav", Speech[Sentence].Data[2], "EN/" ) - end - Number = Number - Multiple - Number = ( Number == 0 ) and -1 or Number + return RemainderSentence end + return OriginalSentence + else + return "" end - return RemainderSentence end + --- Speak a sentence. -- @param #RADIOSPEECH self -- @param #string Sentence The sentence to be spoken. -function RADIOSPEECH:SpeakSymbols( Sentence, Speech ) +function RADIOSPEECH:Speak( Sentence, Language ) - local Symbol, RemainderSentence = Sentence:match( "^[^%a%d]*(°*)(.*)" ) + self:I( { Sentence, Language } ) - self:I( { Sentence = Sentence, Symbol = Symbol, Speech = Speech[Symbol], RemainderSentence = RemainderSentence } ) - - if Symbol and Symbol ~= "" then - local Word = nil - if Symbol == "°" then - Word = "degrees" - end - if Word then - if Speech[Word] then - self:I( { Speech = Speech[Word].Sentence, Data = Speech[Word].Data } ) - self:NewTransmission( Speech[Word].Data[1] .. ".wav", Speech[Word].Data[2], "EN/" ) - end - end - end - - return RemainderSentence -end - - ---- Speak a sentence. --- @param #RADIOSPEECH self --- @param #string Sentence The sentence to be spoken. -function RADIOSPEECH:Speak( Sentence, Speech ) - - self:I( { Sentence, Speech } ) - - local Language = self.Language + local Language = Language or "EN" + + self:I( { Language = Language } ) -- If there is no node for Speech, then we start at the first nodes of the language. - if not Speech then - Speech = self.Speech[Language] - end + local Speech = self.Speech[Language] self:I( { Speech = Speech, Language = Language } ) - self:NewTransmission( "_In.wav", 0.52, "EN/" ) + self:NewTransmission( "_In.wav", 0.52, Language .. "/" ) repeat - Sentence = self:SpeakWords( Sentence, Speech ) + Sentence = self:SpeakWords( Sentence, Speech, Language ) self:I( { Sentence = Sentence } ) - Sentence = self:SpeakDigits( Sentence, Speech ) + Sentence = self:SpeakDigits( Sentence, Speech, Language ) self:I( { Sentence = Sentence } ) @@ -323,6 +400,6 @@ function RADIOSPEECH:Speak( Sentence, Speech ) until not Sentence or Sentence == "" - self:NewTransmission( "_Out.wav", 0.28, "EN/" ) + self:NewTransmission( "_Out.wav", 0.28, Language .. "/" ) end diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index b87b0685c..2fb596fd8 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -162,6 +162,7 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr local Randomize = Schedule.Randomize or 0 local Stop = Schedule.Stop or 0 local ScheduleID = Schedule.ScheduleID + local ShowTrace = Scheduler.ShowTrace local Prefix = ( Repeat == 0 ) and "--->" or "+++>" @@ -169,13 +170,17 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr --self:E( { SchedulerObject = SchedulerObject } ) if SchedulerObject then local function Timer() - SchedulerObject:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) + if ShowTrace then + SchedulerObject:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) + end return ScheduleFunction( SchedulerObject, unpack( ScheduleArguments ) ) end Status, Result = xpcall( Timer, ErrorHandler ) else local function Timer() - self:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) + if ShowTrace then + self:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) + end return ScheduleFunction( unpack( ScheduleArguments ) ) end Status, Result = xpcall( Timer, ErrorHandler ) @@ -274,5 +279,9 @@ function SCHEDULEDISPATCHER:Clear( Scheduler ) end end +function SCHEDULEDISPATCHER:NoTrace( Scheduler ) + self:F2( { Scheduler = Scheduler } ) + Scheduler.ShowTrace = nil +end diff --git a/Moose Development/Moose/Core/Scheduler.lua b/Moose Development/Moose/Core/Scheduler.lua index df54ced90..a0541c440 100644 --- a/Moose Development/Moose/Core/Scheduler.lua +++ b/Moose Development/Moose/Core/Scheduler.lua @@ -301,6 +301,14 @@ function SCHEDULER:Clear() _SCHEDULEDISPATCHER:Clear( self ) end +--- No tracing for this scheduler. +-- @param #SCHEDULER self +-- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +function SCHEDULER:NoTrace() + + _SCHEDULEDISPATCHER:NoTrace( self ) +end + diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index e40121654..3e16fad29 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -261,37 +261,17 @@ do -- DETECTION MANAGER return self.CC end - --- Set the frequency of communication and the mode of communication for voice overs. - -- @param #DETECTION_MANAGER self - -- @param #number RadioFrequency The frequency of communication. - -- @param #number RadioModulation The modulation of communication. - -- @param #number RadioPower The power in Watts of communication. - function DETECTION_MANAGER:SetRadioFrequency( RadioFrequency, RadioModulation, RadioPower ) - - self.RadioFrequency = RadioFrequency - self.RadioModulation = RadioModulation or radio.modulation.AM - self.RadioPower = RadioPower or 100 - - if self.RadioQueue then - self.RadioQueue:Stop() - end - - self.RadioQueue = nil - - self.RadioQueue = RADIOSPEECH:New( self.RadioFrequency, self.RadioModulation ) - self.RadioQueue.power = self.RadioPower - self.RadioQueue:Start( 0.5 ) - end --- Send an information message to the players reporting to the command center. -- @param #DETECTION_MANAGER self + -- @param #table Squadron The squadron table. -- @param #string Message The message to be sent. -- @param #string SoundFile The name of the sound file .wav or .ogg. -- @param #number SoundDuration The duration of the sound. -- @param #string SoundPath The path pointing to the folder in the mission file. -- @param Wrapper.Group#GROUP DefenderGroup The defender group sending the message. -- @return #DETECTION_MANGER self - function DETECTION_MANAGER:MessageToPlayers( Message, DefenderGroup ) + function DETECTION_MANAGER:MessageToPlayers( Squadron, Message, DefenderGroup ) self:F( { Message = Message } ) @@ -306,18 +286,18 @@ do -- DETECTION MANAGER self.CC:MessageToCoalition( Message ) end - Message = Message:gsub( "°", "degrees" ) - Message = Message:gsub( "(%d)%.(%d)", "%1dot%2" ) + Message = Message:gsub( "°", " degrees " ) + Message = Message:gsub( "(%d)%.(%d)", "%1 dot %2" ) -- Here we handle the transmission of the voice over. -- If for a certain reason the Defender does not exist, we use the coordinate of the airbase to send the message from. - local RadioQueue = self.RadioQueue -- Core.RadioQueue#RADIOQUEUE + local RadioQueue = Squadron.RadioQueue -- Core.RadioSpeech#RADIOSPEECH if RadioQueue then local DefenderUnit = DefenderGroup:GetUnit(1) if DefenderUnit and DefenderUnit:IsAlive() then RadioQueue:SetSenderUnitName( DefenderUnit:GetName() ) end - RadioQueue:Speak( Message ) + RadioQueue:Speak( Message, Squadron.Language ) end return self diff --git a/Moose Development/Moose/Wrapper/Client.lua b/Moose Development/Moose/Wrapper/Client.lua index cef00651d..4e6cce0de 100644 --- a/Moose Development/Moose/Wrapper/Client.lua +++ b/Moose Development/Moose/Wrapper/Client.lua @@ -143,6 +143,7 @@ function CLIENT:Register( ClientName ) --self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 ) self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. ClientName }, 1, 5, 0.5 ) + self.AliveCheckScheduler:NoTrace() self:F( self ) return self From 4ea44308bc110f06d757ac7a4e7c77a6186335dd Mon Sep 17 00:00:00 2001 From: FlightControl Date: Thu, 26 Sep 2019 19:36:42 +0300 Subject: [PATCH 372/485] Fix for older A2A dispatcher CAP api altitude problem during takeoff ... --- Moose Development/Moose/AI/AI_A2A_Dispatcher.lua | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index ddf3192f3..5647c4dab 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -1844,15 +1844,13 @@ do -- AI_A2A_DISPATCHER --- Set a CAP for a Squadron. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @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 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 Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. -- @param #number PatrolFloorAltitude The minimum altitude at which the cap can be executed. -- @param #number PatrolCeilingAltitude 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_A2A_DISPATCHER -- @usage @@ -1870,7 +1868,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) -- - function AI_A2A_DISPATCHER:SetSquadronCap( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, EngageMinSpeed, EngageMaxSpeed, AltType ) + function AI_A2A_DISPATCHER:SetSquadronCap( SquadronName, Zone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) return self:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType ) end From ad2ce8df78d175855128574677b7d6a0c5d105d1 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Mon, 30 Sep 2019 10:35:29 +0200 Subject: [PATCH 373/485] Improvements to A2A and A2G Dispatchers --- Moose Development/Moose/AI/AI_A2A_Cap.lua | 2 + .../Moose/AI/AI_A2A_Dispatcher.lua | 93 +- Moose Development/Moose/AI/AI_A2G_BAI.lua | 4 - Moose Development/Moose/AI/AI_A2G_CAS.lua | 4 - Moose Development/Moose/AI/AI_A2G_SEAD.lua | 4 - Moose Development/Moose/AI/AI_Air.lua | 37 +- .../Moose/AI/AI_Air_Dispatcher.lua | 2685 ++++++++--------- Moose Development/Moose/AI/AI_Air_Engage.lua | 11 +- .../Moose/AI/AI_Air_Squadron.lua | 289 ++ Moose Development/Moose/Core/Scheduler.lua | 1 + 10 files changed, 1682 insertions(+), 1448 deletions(-) create mode 100644 Moose Development/Moose/AI/AI_Air_Squadron.lua diff --git a/Moose Development/Moose/AI/AI_A2A_Cap.lua b/Moose Development/Moose/AI/AI_A2A_Cap.lua index 460d5d614..d44d9763c 100644 --- a/Moose Development/Moose/AI/AI_A2A_Cap.lua +++ b/Moose Development/Moose/AI/AI_A2A_Cap.lua @@ -125,6 +125,7 @@ function AI_A2A_CAP:New2( AICap, EngageMinSpeed, EngageMaxSpeed, EngageFloorAlti self:SetDamageThreshold( 0.4 ) self:SetDisengageRadius( 70000 ) + return self end @@ -203,6 +204,7 @@ function AI_A2A_CAP:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageA if AttackUnit:IsAlive() and AttackUnit:IsAir() then -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition() -- Maybe the detected set also contains + self:T( { "Attacking Task:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) end end diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 5647c4dab..d25dc2ce9 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -3267,7 +3267,9 @@ do -- AI_A2A_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) + if Squadron then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) + end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end @@ -3313,7 +3315,7 @@ do -- AI_A2A_DISPATCHER for DefenderID, Defender in pairs( Defenders ) do local Fsm = self:GetDefenderTaskFsm( Defender ) - Fsm:__EngageRoute( 1, AttackerDetection.Set ) -- Engage on the TargetSetUnit + Fsm:EngageRoute( AttackerDetection.Set ) -- Engage on the TargetSetUnit self:SetDefenderTaskTarget( Defender, AttackerDetection ) @@ -3630,6 +3632,32 @@ do -- AI_A2A_DISPATCHER return nil, nil end + + --- Assigns A2G AI Tasks in relation to the detected items. + -- @param #AI_A2G_DISPATCHER self + function AI_A2A_DISPATCHER:Order( DetectedItem ) + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ShortestDistance = 999999999 + + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + + self:I( { DefenderSquadron = DefenderSquadron.Name } ) + + local Airbase = DefenderSquadron.Airbase + local AirbaseCoordinate = Airbase:GetCoordinate() + + local EvaluateDistance = AttackCoordinate:Get2DDistance( AirbaseCoordinate ) + + if EvaluateDistance <= ShortestDistance then + ShortestDistance = EvaluateDistance + end + end + + return ShortestDistance + end + + --- Shows the tactical display. -- @param #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:ShowTacticalDisplay( Detection ) @@ -3645,7 +3673,8 @@ do -- AI_A2A_DISPATCHER local DefenderGroupCount = 0 -- Now that all obsolete tasks are removed, loop through the detected targets. - for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT @@ -3761,7 +3790,9 @@ do -- AI_A2A_DISPATCHER local DefenderGroupCount = 0 -- Now that all obsolete tasks are removed, loop through the detected targets. - for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + -- Closest detected targets to be considered first! + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT @@ -3790,62 +3821,10 @@ do -- AI_A2A_DISPATCHER self:GCI( 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 and Defender:IsAlive() then - DefenderGroupCount = DefenderGroupCount + 1 - local Fuel = Defender:GetFuelMin() * 100 - local Damage = Defender:GetLife() / Defender:GetLife0() * 100 - Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", - Defender:GetName(), - Defender:GetSize(), - Defender:GetInitialSize(), - 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*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", - Defender:GetName(), - Defender:GetSize(), - Defender:GetInitialSize(), - 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 ) + self:ShowTacticalDisplay( Detection ) end return true diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 8b8b8e880..f35009033 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -50,10 +50,6 @@ function AI_A2G_BAI:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAl local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) local self = BASE:Inherit( self, AI_Air_Engage ) - 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 00cd73383..de24056c9 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -50,10 +50,6 @@ function AI_A2G_CAS:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAl local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) local self = BASE:Inherit( self, AI_Air_Engage ) - 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_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index b72080558..6a0a32e3c 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -100,10 +100,6 @@ function AI_A2G_SEAD:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorA local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) local self = BASE:Inherit( self, AI_Air_Engage ) - 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_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 8c6dbb9e0..3477a3d2a 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -308,7 +308,7 @@ end -- @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:F( { RTBMinSpeed, RTBMaxSpeed } ) self.RTBMinSpeed = RTBMinSpeed self.RTBMaxSpeed = RTBMaxSpeed @@ -425,7 +425,18 @@ function AI_AIR:onafterStart( Controllable, From, Event, To ) Controllable:OptionROTVertical() end +--- Coordinates the approriate returning action. +-- @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:onafterReturn( Controllable, From, Event, To ) + self:__RTB( self.TaskDelay ) + +end --- @param #AI_AIR self function AI_AIR:onbeforeStatus() @@ -481,11 +492,14 @@ function AI_AIR:onafterStatus() OldAIControllable:SetTask( TimedOrbitTask, 10 ) self:Fuel() - RTB = true end else end end + + if self:Is( "Fuel" ) and not self:Is( "Home" ) and not self:is( "Refuelling" ) then + RTB = true + end -- TODO: Check GROUP damage function. local Damage = self.Controllable:GetLife() @@ -581,7 +595,13 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) local FromCoord = AIGroup:GetCoordinate() local ToTargetCoord = self.HomeAirbase:GetCoordinate() - local ToTargetSpeed = math.random( self.RTBMinSpeed, self.RTBMaxSpeed ) + + if not self.RTBMinSpeed and not self.RTBMaxSpeed then + local RTBSpeedMax = AIGroup:GetSpeedMax() + self:SetRTBSpeed( RTBSpeedMax * 0.25, RTBSpeedMax * 0.25 ) + end + + local RTBSpeed = math.random( self.RTBMinSpeed, self.RTBMaxSpeed ) local ToAirbaseAngle = FromCoord:GetAngleDegrees( FromCoord:GetDirectionVec3( ToTargetCoord ) ) local Distance = FromCoord:Get2DDistance( ToTargetCoord ) @@ -593,12 +613,19 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) return end + if not AIGroup:InAir() == true then + self:I( "Not anymore in the air, considered Home." ) + 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, + RTBSpeed, true ) @@ -607,7 +634,7 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, + RTBSpeed, true ) diff --git a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua index 459a97aab..7565f7a64 100644 --- a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua @@ -1,16 +1,265 @@ ---- **AI** - Intermediary class that realized the common methods for the A2G and A2A dispatchers. +--- **AI** - Create an automated AIR defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. -- -- === -- -- Features: -- --- * Common methods for A2G and A2A dispatchers. +-- * Setup quickly an AIR defense system for a coalition. +-- * Setup multiple defense zones to defend specific coordinates in your battlefield. +-- * Setup (SEAD) Suppression of Air Defense squadrons, to gain control in the air of enemy grounds. +-- * Setup (CAS) Controlled Air Support squadrons, to attack closeby enemy ground units near friendly installations. +-- * Setup (BAI) Battleground Air Interdiction squadrons to attack remote enemy ground units and targets. +-- * Define and use a detection network controlled by recce. +-- * Define AIR defense squadrons at airbases, farps and carriers. +-- * Enable airbases for AIR defenses. +-- * Add different planes and helicopter templates to 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 AIR defenses. +-- * Setup specific settings for specific squadrons. +-- +-- === +-- +-- ## Missions: +-- +-- [AID-AIR - AI AIR Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-AIR%20-%20AI%20AIR%20Dispatching) +-- +-- === +-- +-- ## YouTube Channel: +-- +-- [DCS WORLD - MOOSE - AIR GCICAP - Build an automatic AIR Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) +-- +-- === +-- +-- # QUICK START GUIDE +-- +-- The following class is available to model an AIR defense system. +-- +-- AI_AIR_DISPATCHER is the main AIR defense class that models the AIR defense system. +-- +-- Before you start using the AI_AIR_DISPATCHER, ask youself the following questions. +-- +-- +-- ## 1. Which coalition am I modeling an AIR defense system for? blue or red? +-- +-- One AI_AIR_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_AIR_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 based, or also airborne? +-- +-- 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! +-- +-- 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 the defenses decide **when and where to engage** on approaching enemy units? +-- +-- The AIR 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 AIR 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 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 AIR 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. 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**. +-- +-- 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 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 AIR defenses. +-- 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? +-- +-- 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 AIR 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 AIR 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 AIR 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 +-- AIR 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 AIR 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_Air_Dispatcher +-- @module AI.AI_AIR_Dispatcher -- @image AI_Air_To_Ground_Dispatching.JPG @@ -21,10 +270,622 @@ do -- AI_AIR_DISPATCHER -- @type AI_AIR_DISPATCHER -- @extends Tasking.DetectionManager#DETECTION_MANAGER - --- Intermediary class that realized the common methods for the A2G and A2A dispatchers. + --- Create an automated AIR 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 AIR 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 AIR 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\_AIR\_DISPATCHER constructor: + -- + -- ![Banner Image](..\Presentations\AI_AIR_DISPATCHER\AI_AIR_DISPATCHER-ME_1.JPG) + -- + -- + -- The @{#AI_AIR_DISPATCHER.New}() method creates a new AI_AIR_DISPATCHER instance. + -- + -- ### 1.1. Define the **reconnaissance network**: + -- + -- As part of the AI_AIR_DISPATCHER :New() constructor, a reconnaissance network must be given as the first parameter. + -- A reconnaissance network is provided by passing a @{Functional.Detection} object. + -- The most effective reconnaissance for the AIR dispatcher would be to use the @{Functional.Detection#DETECTION_AREAS} object. + -- + -- A 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_AIR_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 AIR 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. + -- AIRDispatcher = AI_AIR_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_AIR_DISPATCHER.New}() method to indicate the reconnaissance network + -- configuration and setup the AIR defense detection mechanism. + -- + -- ### 1.2. Setup the AIR 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 AIR 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 ) + -- AIRDispatcherRed = AI_AIR_DISPATCHER:New( DetectionRed ) + -- + -- And for the blue coalition: + -- + -- DetectionBlue = DETECTION_AREAS:New( DetectionSetGroupBlue, 1000 ) + -- AIRDispatcherBlue = AI_AIR_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_AIR_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. + -- AIRDispatcher: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_AIR_DISPATCHER.AddDefenseCoordinate}() adds a new defense coordinate to the `AIRDispatcher` 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 AIR dispatcher. + -- + -- **REMEMBER!** + -- + -- - **Defense coordinates are the center of the AIR 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_AIR_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: + -- + -- AIRDispatcher:SetDefenseRadius( 30000 ) + -- + -- This defines an AIR 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 AIR 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 AIR defense will engage to the enemy ground target, then + -- use the @{#AI_AIR_DISPATCHER.SetDefenseReactivityLow}() method. For medium and high reactivity, use the methods + -- @{#AI_AIR_DISPATCHER.SetDefenseReactivityMedium}() and @{#AI_AIR_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: + -- + -- AIRDispatcher:SetDefenseReactivityHigh() + -- + -- This defines an AIR dispatcher with high defense reactivity. + -- + -- ## 3. **Squadrons**. + -- + -- The AIR dispatcher works with **Squadrons**, that need to be defined using the different methods available. + -- + -- Use the method @{#AI_AIR_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 AIR 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 `AIRDispatcher:SetSquadron...()` or `AIRDispatcher: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_AIR_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: + -- + -- AIRDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50" }, 10 ) + -- AIRDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50" }, 10 ) + -- AIRDispatcher: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 types**. + -- + -- 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 + -- 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_AIR_DISPATCHER.SetSquadronSead}() method. + -- - For CAS, use the @{#AI_AIR_DISPATCHER.SetSquadronCas}() method. + -- - For BAI, use the @{#AI_AIR_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: + -- + -- AIRDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- AIRDispatcher:SetSquadronSead( "Maykop SEAD", 120, 250 ) + -- + -- AIRDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- AIRDispatcher:SetSquadronCas( "Maykop CAS", 120, 250 ) + -- + -- AIRDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- AIRDispatcher: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 AIR 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_AIR_DISPATCHER.SetSquadronSeadPatrol}() method. + -- - For CAS, use the @{#AI_AIR_DISPATCHER.SetSquadronCasPatrol}() method. + -- - For BAI, use the @{#AI_AIR_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_AIR_DISPATCHER.SetSquadronSeadPatrolInterval}() method. + -- - For CAS, use the @{#AI_AIR_DISPATCHER.SetSquadronCasPatrolInterval}() method. + -- - For BAI, use the @{#AI_AIR_DISPATCHER.SetSquadronBaiPatrolInterval}() method. + -- + -- Here an example to setup patrols of various task types: + -- + -- AIRDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- AIRDispatcher:SetSquadronSeadPatrol( "Maykop SEAD", PatrolZone, 300, 500, 50, 80, 250, 300 ) + -- AIRDispatcher:SetSquadronPatrolInterval( "Maykop SEAD", 2, 30, 60, 1, "SEAD" ) + -- + -- AIRDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- AIRDispatcher:SetSquadronCasPatrol( "Maykop CAS", PatrolZone, 600, 700, 50, 80, 250, 300 ) + -- AIRDispatcher:SetSquadronPatrolInterval( "Maykop CAS", 2, 30, 60, 1, "CAS" ) + -- + -- AIRDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- AIRDispatcher:SetSquadronBaiPatrol( "Maykop BAI", PatrolZone, 800, 900, 50, 80, 250, 300 ) + -- AIRDispatcher: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_AIR_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_AIR_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. + -- * @{#AI_AIR_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_AIR_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER.SetSquadronTakeoffInAirAltitude}() to set the altitude for a specific squadron. + -- The default takeoff altitude can be modified or set using the method @{#AI_AIR_DISPATCHER.SetSquadronTakeoffInAirAltitude}(). + -- As part of the method @{#AI_AIR_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 AIR 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_AIR_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_AIR_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_AIR_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. + -- * @{#AI_AIR_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. + -- * @{#AI_AIR_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_AIR_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_AIR_DISPATCHER.Landing.AtRunway ) + -- + -- + -- ### 3.7. Set squadron **grouping**. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetSquadronGrouping}() to set the grouping of aircraft when spawned in. + -- + -- ![Banner Image](..\Presentations\AI_AIR_DISPATCHER\Dia12.JPG) + -- + -- In the case of **on call** engagement, the @{#AI_AIR_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 AIR 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_AIR_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_AIR_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 AIR missiles payload, may still be less effective than a Su-23 with short range AIR missiles... + -- So in this case, one may want to use the @{#AI_AIR_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 AIR 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_AIR_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_AIR_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_AIR_DISPATCHER. + -- Use the method @{#AI_AIR_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_AIR_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_AIR_DISPATCHER.SetDefaultTakeoffInAir}() will spawn by default new aircraft from the squadron directly in the air. + -- * @{#AI_AIR_DISPATCHER.SetDefaultTakeoffFromParkingCold}() will spawn by default new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_AIR_DISPATCHER.SetDefaultTakeoffFromParkingHot}() will spawn by default new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_AIR_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_AIR_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_AIR_DISPATCHER.SetDefaultLandingNearAirbase}() will despawn by default the returning aircraft in the air when near the airfield. + -- * @{#AI_AIR_DISPATCHER.SetDefaultLandingAtRunway}() will despawn by default the returning aircraft directly after landing at the runway. + -- * @{#AI_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher. + -- Use the method @{#AI_AIR_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_AIR_DISPATCHER\AI_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER AI_AIR_DISPATCHER = { ClassName = "AI_AIR_DISPATCHER", @@ -47,6 +908,9 @@ do -- AI_AIR_DISPATCHER -- @type AI_AIR_DISPATCHER.DefenseCoordinates -- @map <#string,Core.Point#COORDINATE> A list of all defense coordinates mapped per defense coordinate name. + --- @field #AI_AIR_DISPATCHER.DefenseCoordinates DefenseCoordinates + AI_AIR_DISPATCHER.DefenseCoordinates = {} + --- Enumerator for spawns at airbases -- @type AI_AIR_DISPATCHER.Takeoff -- @extends Wrapper.Group#GROUP.Takeoff @@ -55,7 +919,7 @@ do -- AI_AIR_DISPATCHER AI_AIR_DISPATCHER.Takeoff = GROUP.Takeoff --- Defnes Landing location. - -- @field Landing + -- @field #AI_AIR_DISPATCHER.Landing AI_AIR_DISPATCHER.Landing = { NearAirbase = 1, AtRunway = 2, @@ -86,12 +950,35 @@ do -- AI_AIR_DISPATCHER AI_AIR_DISPATCHER.DefenseQueue = {} - + --- Defense approach types + -- @type #AI_AIR_DISPATCHER.DefenseApproach + AI_AIR_DISPATCHER.DefenseApproach = { + Random = 1, + Distance = 2, + } --- AI_AIR_DISPATCHER constructor. + -- This is defining the AIR 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_AIR_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. -- @return #AI_AIR_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 AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) -- + -- function AI_AIR_DISPATCHER:New( Detection ) -- Inherits from DETECTION_MANAGER @@ -99,23 +986,31 @@ do -- AI_AIR_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:SetDefenseLimit( nil ) + self:SetDefenseApproach( AI_AIR_DISPATCHER.DefenseApproach.Random ) self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. + self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. self:SetDefaultTakeoff( AI_AIR_DISPATCHER.Takeoff.Air ) self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. self:SetDefaultLanding( AI_AIR_DISPATCHER.Landing.NearAirbase ) - self:SetDefaultEngageRadius( 150000 ) - 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. @@ -130,7 +1025,7 @@ do -- AI_AIR_DISPATCHER -- @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 Tasking.Task_AIR#AI_AIR Task -- @param Wrapper.Unit#UNIT TaskUnit -- @param #string PlayerName @@ -234,9 +1129,11 @@ do -- AI_AIR_DISPATCHER self.DefenderPatrolIndex = 0 + self:SetDefenseReactivityMedium() + self.TakeoffScheduleID = self:ScheduleRepeat( 10, 10, 0, nil, self.ResourceTakeoff, self ) - self:__Start( 5 ) + self:__Start( 1 ) return self end @@ -253,7 +1150,56 @@ do -- AI_AIR_DISPATCHER for Resource = 1, DefenderSquadron.ResourceCount or 0 do self:ResourcePark( DefenderSquadron ) end + self:I( "Parked resources for squadron " .. DefenderSquadron.Name ) end + + end + + --- Locks the DefenseItem from being defended. + -- @param #AI_AIR_DISPATCHER self + -- @param #string DetectedItemIndex The index of the detected item. + function AI_AIR_DISPATCHER:Lock( DetectedItemIndex ) + self:F( { DetectedItemIndex = DetectedItemIndex } ) + local DetectedItem = self.Detection:GetDetectedItemByIndex( DetectedItemIndex ) + if DetectedItem then + self:F( { Locked = DetectedItem } ) + self.Detection:LockDetectedItem( DetectedItem ) + end + end + + + --- Unlocks the DefenseItem from being defended. + -- @param #AI_AIR_DISPATCHER self + -- @param #string DetectedItemIndex The index of the detected item. + function AI_AIR_DISPATCHER:Unlock( DetectedItemIndex ) + self:F( { DetectedItemIndex = DetectedItemIndex } ) + self:F( { Index = self.Detection.DetectedItemsByIndex } ) + local DetectedItem = self.Detection:GetDetectedItemByIndex( DetectedItemIndex ) + if DetectedItem then + self:F( { Unlocked = DetectedItem } ) + self.Detection:UnlockDetectedItem( DetectedItem ) + end + end + + + --- Sets maximum zones to be engaged at one time by defenders. + -- @param #AI_AIR_DISPATCHER self + -- @param #number DefenseLimit The maximum amount of detected items to be engaged at the same time. + function AI_AIR_DISPATCHER:SetDefenseLimit( DefenseLimit ) + self:F( { DefenseLimit = DefenseLimit } ) + + self.DefenseLimit = DefenseLimit + end + + + --- Sets the method of the tactical approach of the defenses. + -- @param #AI_AIR_DISPATCHER self + -- @param #number DefenseApproach Use the structure AI_AIR_DISPATCHER.DefenseApproach to set the defense approach. + -- The default defense approach is AI_AIR_DISPATCHER.DefenseApproach.Random. + function AI_AIR_DISPATCHER:SetDefenseApproach( DefenseApproach ) + self:F( { DefenseApproach = DefenseApproach } ) + + self._DefenseApproach = DefenseApproach end @@ -347,9 +1293,55 @@ do -- AI_AIR_DISPATCHER end end + do -- Manage the defensive behaviour - --- Define the defense radius to check if a target can be engaged by a squadron group. - -- When targets are detected that are still really far off, you don't want the dispatcher to launch defenders, as they might need to travel too far. + --- @param #AI_AIR_DISPATCHER self + -- @param #string DefenseCoordinateName The name of the coordinate to be defended by AIR defenses. + -- @param Core.Point#COORDINATE DefenseCoordinate The coordinate to be defended by AIR defenses. + function AI_AIR_DISPATCHER:AddDefenseCoordinate( DefenseCoordinateName, DefenseCoordinate ) + self.DefenseCoordinates[DefenseCoordinateName] = DefenseCoordinate + end + + --- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:SetDefenseReactivityLow() + self.DefenseReactivity = 0.05 + end + + --- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:SetDefenseReactivityMedium() + self.DefenseReactivity = 0.15 + end + + --- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:SetDefenseReactivityHigh() + self.DefenseReactivity = 0.5 + end + + end + + + --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters. + -- @param #AI_AIR_DISPATCHER self + -- @param #number DisengageRadius (Optional, Default = 300000) The radius to disengage a target when too far from the home base. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Set 50km as the Disengage Radius. + -- AIRDispatcher:SetDisengageRadius( 50000 ) + -- + -- -- Set 100km as the Disengage Radius. + -- AIRDispatcher:SetDisngageRadius() -- 300000 is the default value. + -- + function AI_AIR_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_AIR_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**. @@ -365,14 +1357,14 @@ do -- AI_AIR_DISPATCHER -- @return #AI_AIR_DISPATCHER -- @usage -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) -- -- -- Set 100km as the radius to defend from detected targets from the nearest airbase. - -- A2GDispatcher:SetDefendRadius( 100000 ) + -- AIRDispatcher:SetDefendRadius( 100000 ) -- -- -- Set 200km as the radius to defend. - -- A2GDispatcher:SetDefendRadius() -- 200000 is the default value. + -- AIRDispatcher:SetDefendRadius() -- 200000 is the default value. -- function AI_AIR_DISPATCHER:SetDefenseRadius( DefenseRadius ) @@ -383,7 +1375,6 @@ do -- AI_AIR_DISPATCHER return self end - --- Define a border area to simulate a **cold war** scenario. @@ -396,19 +1387,19 @@ do -- AI_AIR_DISPATCHER -- @return #AI_AIR_DISPATCHER -- @usage -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) -- - -- -- Set one ZONE_POLYGON object as the border for the A2G dispatcher. + -- -- Set one ZONE_POLYGON object as the border for the AIR dispatcher. -- local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit. - -- A2GDispatcher:SetBorderZone( BorderZone ) + -- AIRDispatcher:SetBorderZone( BorderZone ) -- -- or -- - -- -- Set two ZONE_POLYGON objects as the border for the A2G dispatcher. + -- -- Set two ZONE_POLYGON objects as the border for the AIR 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 } ) + -- AIRDispatcher:SetBorderZone( { BorderZone1, BorderZone2 } ) -- -- function AI_AIR_DISPATCHER:SetBorderZone( BorderZone ) @@ -430,11 +1421,11 @@ do -- AI_AIR_DISPATCHER -- @return #AI_AIR_DISPATCHER -- @usage -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) -- -- -- Now Setup the Tactical Display for debug mode. - -- A2GDispatcher:SetTacticalDisplay( true ) + -- AIRDispatcher:SetTacticalDisplay( true ) -- function AI_AIR_DISPATCHER:SetTacticalDisplay( TacticalDisplay ) @@ -451,11 +1442,11 @@ do -- AI_AIR_DISPATCHER -- @return #AI_AIR_DISPATCHER -- @usage -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) -- -- -- Now Setup the default damage treshold. - -- A2GDispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. + -- AIRDispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. -- function AI_AIR_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) @@ -473,11 +1464,11 @@ do -- AI_AIR_DISPATCHER -- @return #AI_AIR_DISPATCHER -- @usage -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) -- -- -- Now Setup the default Patrol time interval. - -- A2GDispatcher:SetDefaultPatrolTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. + -- AIRDispatcher:SetDefaultPatrolTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. -- function AI_AIR_DISPATCHER:SetDefaultPatrolTimeInterval( PatrolMinSeconds, PatrolMaxSeconds ) @@ -495,11 +1486,11 @@ do -- AI_AIR_DISPATCHER -- @return #AI_AIR_DISPATCHER -- @usage -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) -- -- -- Now Setup the default Patrol limit. - -- A2GDispatcher:SetDefaultPatrolLimit( 2 ) -- Maximum 2 Patrol per squadron. + -- AIRDispatcher:SetDefaultPatrolLimit( 2 ) -- Maximum 2 Patrol per squadron. -- function AI_AIR_DISPATCHER:SetDefaultPatrolLimit( PatrolLimit ) @@ -510,17 +1501,17 @@ do -- AI_AIR_DISPATCHER --- 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 patrol group maximum per squadron. + -- The default eatrol limit is 1, which means one eatrol group maximum per squadron. -- @param #AI_AIR_DISPATCHER self -- @param #number EngageLimit The maximum engages that can be done at the same time per squadron. -- @return #AI_AIR_DISPATCHER -- @usage -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) -- -- -- Now Setup the default Patrol limit. - -- A2GDispatcher:SetDefaultEngageLimit( 2 ) -- Maximum 2 engagements with the enemy per squadron. + -- AIRDispatcher:SetDefaultEngageLimit( 2 ) -- Maximum 2 engagements with the enemy per squadron. -- function AI_AIR_DISPATCHER:SetDefaultEngageLimit( EngageLimit ) @@ -530,7 +1521,6 @@ do -- AI_AIR_DISPATCHER end - function AI_AIR_DISPATCHER:SetIntercept( InterceptDelay ) self.DefenderDefault.InterceptDelay = InterceptDelay @@ -688,7 +1678,7 @@ do -- AI_AIR_DISPATCHER -- * 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. + -- The name of the squadron given acts as the **squadron key** in the AI\_AIR\_DISPATCHER:Squadron...() methods. -- -- Additionally, squadrons have specific configuration options to: -- @@ -724,76 +1714,111 @@ do -- AI_AIR_DISPATCHER -- @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_AIR_DISPATCHER:New( Detection ) + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_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 ) + -- AIRDispatcher: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 that in this implementation, the AIR 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 ) + -- AIRDispatcher: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 ) + -- AIRDispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) + -- AIRDispatcher: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" ) + -- AIRDispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) + -- AIRDispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) -- -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) + + local Squadron = AI_AIR_SQUADRON:New( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) + + return self:SetSquadron2( Squadron ) + end + + --- This is the new method to define Squadrons programmatically. + -- + -- Define a squadron using the AI_AIR_SQUADRON class. + -- + -- 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\_AIR\_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_AIR_DISPATCHER self + -- @param AI.AI_Air_Squadron#AI_AIR_SQUADRON Squadron The Air Squadron to be set active for the Air Dispatcher. + -- + -- @usage + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- @usage + -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... + -- AIRDispatcher: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 AIR 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. + -- AIRDispatcher: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... + -- AIRDispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) + -- AIRDispatcher: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. + -- AIRDispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) + -- AIRDispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) + -- + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadron2( Squadron ) + local SquadronName = Squadron:GetName() -- Retrieves the Squadron Name. 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:SetSquadronTakeoffInterval( SquadronName, 0 ) - - self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) - return self end - --- Get an item from the Squadron table. + --- Get the @{AI.AI_Air_Squadron} object from the Squadron Name given. -- @param #AI_AIR_DISPATCHER self - -- @return #table + -- @param #string SquadronName The Squadron Name to search the Squadron object. + -- @return AI.AI_Air_Squadron#AI_AIR_SQUADRON The Squadron object. function AI_AIR_DISPATCHER:GetSquadron( SquadronName ) local DefenderSquadron = self.DefenderSquadrons[SquadronName] if not DefenderSquadron then - error( "Unknown Squadron:" .. SquadronName ) + error( "Unknown Squadron for Dispatcher:" .. SquadronName ) end return DefenderSquadron @@ -809,7 +1834,7 @@ do -- AI_AIR_DISPATCHER -- @usage -- -- -- Set the Squadron visible before startup of dispatcher. - -- A2GDispatcher:SetSquadronVisible( "Mineralnye" ) + -- AIRDispatcher:SetSquadronVisible( "Mineralnye" ) -- -- TODO: disabling because of bug in queueing. -- function AI_AIR_DISPATCHER:SetSquadronVisible( SquadronName ) @@ -835,7 +1860,7 @@ do -- AI_AIR_DISPATCHER -- @usage -- -- -- Set the Squadron visible before startup of dispatcher. - -- local IsVisible = A2GDispatcher:IsSquadronVisible( "Mineralnye" ) + -- local IsVisible = AIRDispatcher:IsSquadronVisible( "Mineralnye" ) -- function AI_AIR_DISPATCHER:IsSquadronVisible( SquadronName ) @@ -857,7 +1882,7 @@ do -- AI_AIR_DISPATCHER -- @usage -- -- -- Set the Squadron Takeoff interval every 60 seconds for squadron "SQ50", which is good for a FARP cold start. - -- A2GDispatcher:SetSquadronTakeoffInterval( "SQ50", 60 ) + -- AIRDispatcher:SetSquadronTakeoffInterval( "SQ50", 60 ) -- function AI_AIR_DISPATCHER:SetSquadronTakeoffInterval( SquadronName, TakeoffInterval ) @@ -893,8 +1918,8 @@ do -- AI_AIR_DISPATCHER -- -- -- 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" ) + -- AIRDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- AIRDispatcher:SetSquadronPatrolInterval( "Mineralnye", 2, 30, 60, 1, "SEAD" ) -- function AI_AIR_DISPATCHER:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, DefenseTaskType ) @@ -926,144 +1951,7 @@ do -- AI_AIR_DISPATCHER end - - --- Set the squadron Patrol parameters for SEAD tasks. - -- @param #AI_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_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_AIR_DISPATCHER:SetSquadronBaiPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) - - self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "BAI" ) - - end - - - --- - -- @param #AI_AIR_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #AI_AIR_DISPATCHER - function AI_AIR_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_AIR_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #table DefenderSquadron - function AI_AIR_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_AIR_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @return #table DefenderSquadron - function AI_AIR_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. -- @@ -1080,7 +1968,7 @@ do -- AI_AIR_DISPATCHER -- -- -- 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. + -- AIRDispatcher:SetSquadronEngageLimit( "Mineralnye", 2, "SEAD" ) -- Engage maximum 2 groups with the enemy for SEAD defense. -- function AI_AIR_DISPATCHER:SetSquadronEngageLimit( SquadronName, EngageLimit, DefenseTaskType ) @@ -1098,302 +1986,12 @@ do -- AI_AIR_DISPATCHER - --- - -- @param #AI_AIR_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @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. - -- A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200 ) - -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100 ) - -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200 ) - -- - -- @return #AI_AIR_DISPATCHER - function AI_AIR_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} - - local Sead = DefenderSquadron.SEAD - Sead.Name = SquadronName - Sead.EngageMinSpeed = EngageMinSpeed - Sead.EngageMaxSpeed = EngageMaxSpeed - Sead.EngageFloorAltitude = EngageFloorAltitude or 500 - Sead.EngageCeilingAltitude = EngageCeilingAltitude or 1000 - Sead.Defend = true - - self:F( { Sead = Sead } ) - end - - --- Set the squadron SEAD engage limit. - -- @param #AI_AIR_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_AIR_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_AIR_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_AIR_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 (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_AIR_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_AIR_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.PatrolFloorAltitude = FloorAltitude - SeadPatrol.PatrolCeilingAltitude = CeilingAltitude - SeadPatrol.EngageFloorAltitude = FloorAltitude - SeadPatrol.EngageCeilingAltitude = 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_AIR_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @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. - -- A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200 ) - -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100 ) - -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200 ) - -- - -- @return #AI_AIR_DISPATCHER - function AI_AIR_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.CAS = DefenderSquadron.CAS or {} - - local Cas = DefenderSquadron.CAS - Cas.Name = SquadronName - Cas.EngageMinSpeed = EngageMinSpeed - Cas.EngageMaxSpeed = EngageMaxSpeed - Cas.EngageFloorAltitude = EngageFloorAltitude or 500 - Cas.EngageCeilingAltitude = EngageCeilingAltitude or 1000 - Cas.Defend = true - - self:F( { Cas = Cas } ) - end - - - --- Set the squadron CAS engage limit. - -- @param #AI_AIR_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_AIR_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_AIR_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_AIR_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 (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_AIR_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_AIR_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.PatrolFloorAltitude = FloorAltitude - CasPatrol.PatrolCeilingAltitude = CeilingAltitude - CasPatrol.EngageFloorAltitude = FloorAltitude - CasPatrol.EngageCeilingAltitude = 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_AIR_DISPATCHER self - -- @param #string SquadronName The squadron name. - -- @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. - -- A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200 ) - -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100 ) - -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200 ) - -- - -- @return #AI_AIR_DISPATCHER - function AI_AIR_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) - - local DefenderSquadron = self:GetSquadron( SquadronName ) - - DefenderSquadron.BAI = DefenderSquadron.BAI or {} - - local Bai = DefenderSquadron.BAI - Bai.Name = SquadronName - Bai.EngageMinSpeed = EngageMinSpeed - Bai.EngageMaxSpeed = EngageMaxSpeed - Bai.EngageFloorAltitude = EngageFloorAltitude or 500 - Bai.EngageCeilingAltitude = EngageCeilingAltitude or 1000 - Bai.Defend = true - - self:F( { Bai = Bai } ) - end - - - --- Set the squadron BAI engage limit. - -- @param #AI_AIR_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_AIR_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_AIR_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_AIR_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 (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_AIR_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_AIR_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.PatrolFloorAltitude = FloorAltitude - BaiPatrol.PatrolCeilingAltitude = CeilingAltitude - BaiPatrol.EngageFloorAltitude = FloorAltitude - BaiPatrol.EngageCeilingAltitude = 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_AIR_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_AIR_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... + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance AIR 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: -- @@ -1409,14 +2007,14 @@ do -- AI_AIR_DISPATCHER -- -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_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 ) + -- AIRDispatcher:SetDefaultOverhead( 1.5 ) -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetDefaultOverhead( Overhead ) @@ -1432,7 +2030,7 @@ do -- AI_AIR_DISPATCHER -- @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_AIR_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... + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance AIR 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: -- @@ -1448,20 +2046,20 @@ do -- AI_AIR_DISPATCHER -- -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_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 ) + -- AIRDispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.Overhead = Overhead + DefenderSquadron:SetOverhead( Overhead ) return self end @@ -1472,7 +2070,7 @@ do -- AI_AIR_DISPATCHER -- @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_AIR_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... + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance AIR 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: -- @@ -1488,20 +2086,20 @@ do -- AI_AIR_DISPATCHER -- -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_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" ) + -- local SquadronOverhead = AIRDispatcher:GetSquadronOverhead( "SquadronName" ) -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:GetSquadronOverhead( SquadronName ) local DefenderSquadron = self:GetSquadron( SquadronName ) - return DefenderSquadron.Overhead or self.DefenderDefault.Overhead + return DefenderSquadron:GetOverhead() or self.DefenderDefault.Overhead end @@ -1511,10 +2109,10 @@ do -- AI_AIR_DISPATCHER -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Set a grouping by default per 2 airplanes. - -- A2GDispatcher:SetDefaultGrouping( 2 ) + -- AIRDispatcher:SetDefaultGrouping( 2 ) -- -- -- @return #AI_AIR_DISPATCHER @@ -1533,17 +2131,41 @@ do -- AI_AIR_DISPATCHER -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Set a grouping per 2 airplanes. - -- A2GDispatcher:SetSquadronGrouping( "SquadronName", 2 ) + -- AIRDispatcher:SetSquadronGrouping( "SquadronName", 2 ) -- -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.Grouping = Grouping + DefenderSquadron:SetGrouping( Grouping ) + + return self + end + + + --- Sets the engage probability if the squadron will engage on a detected target. + -- This can be configured per squadron, to ensure that each squadron as a specific defensive probability setting. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number EngageProbability The probability when the squadron will consider to engage the detected target. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Set an defense probability for squadron SquadronName of 50%. + -- -- This will result that this squadron has 50% chance to engage on a detected target. + -- AIRDispatcher:SetSquadronEngageProbability( "SquadronName", 0.5 ) + -- + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronEngageProbability( SquadronName, EngageProbability ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron:SetEngageProbability( EngageProbability ) return self end @@ -1554,19 +2176,19 @@ do -- AI_AIR_DISPATCHER -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights by default take-off in the air. - -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Air ) + -- AIRDispatcher:SetDefaultTakeoff( AI_AIR_Dispatcher.Takeoff.Air ) -- -- -- Let new flights by default take-off from the runway. - -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Runway ) + -- AIRDispatcher:SetDefaultTakeoff( AI_AIR_Dispatcher.Takeoff.Runway ) -- -- -- Let new flights by default take-off from the airbase hot. - -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Hot ) + -- AIRDispatcher:SetDefaultTakeoff( AI_AIR_Dispatcher.Takeoff.Hot ) -- -- -- Let new flights by default take-off from the airbase cold. - -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Cold ) + -- AIRDispatcher:SetDefaultTakeoff( AI_AIR_Dispatcher.Takeoff.Cold ) -- -- -- @return #AI_AIR_DISPATCHER @@ -1584,19 +2206,19 @@ do -- AI_AIR_DISPATCHER -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights take-off in the air. - -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Air ) + -- AIRDispatcher:SetSquadronTakeoff( "SquadronName", AI_AIR_Dispatcher.Takeoff.Air ) -- -- -- Let new flights take-off from the runway. - -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Runway ) + -- AIRDispatcher:SetSquadronTakeoff( "SquadronName", AI_AIR_Dispatcher.Takeoff.Runway ) -- -- -- Let new flights take-off from the airbase hot. - -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Hot ) + -- AIRDispatcher:SetSquadronTakeoff( "SquadronName", AI_AIR_Dispatcher.Takeoff.Hot ) -- -- -- Let new flights take-off from the airbase cold. - -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Cold ) + -- AIRDispatcher:SetSquadronTakeoff( "SquadronName", AI_AIR_Dispatcher.Takeoff.Cold ) -- -- -- @return #AI_AIR_DISPATCHER @@ -1604,7 +2226,7 @@ do -- AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff ) local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.Takeoff = Takeoff + DefenderSquadron:SetTakeoff( Takeoff ) return self end @@ -1615,11 +2237,11 @@ do -- AI_AIR_DISPATCHER -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights by default take-off in the air. - -- local TakeoffMethod = A2GDispatcher:GetDefaultTakeoff() - -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then + -- local TakeoffMethod = AIRDispatcher:GetDefaultTakeoff() + -- if TakeOffMethod == , AI_AIR_Dispatcher.Takeoff.InAir then -- ... -- end -- @@ -1634,18 +2256,18 @@ do -- AI_AIR_DISPATCHER -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights take-off in the air. - -- local TakeoffMethod = A2GDispatcher:GetSquadronTakeoff( "SquadronName" ) - -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then + -- local TakeoffMethod = AIRDispatcher:GetSquadronTakeoff( "SquadronName" ) + -- if TakeOffMethod == , AI_AIR_Dispatcher.Takeoff.InAir then -- ... -- end -- function AI_AIR_DISPATCHER:GetSquadronTakeoff( SquadronName ) local DefenderSquadron = self:GetSquadron( SquadronName ) - return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff + return DefenderSquadron:GetTakeoff() or self.DefenderDefault.Takeoff end @@ -1653,10 +2275,10 @@ do -- AI_AIR_DISPATCHER -- @param #AI_AIR_DISPATCHER self -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights by default take-off in the air. - -- A2GDispatcher:SetDefaultTakeoffInAir() + -- AIRDispatcher:SetDefaultTakeoffInAir() -- -- @return #AI_AIR_DISPATCHER -- @@ -1674,10 +2296,10 @@ do -- AI_AIR_DISPATCHER -- @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_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights take-off in the air. - -- A2GDispatcher:SetSquadronTakeoffInAir( "SquadronName" ) + -- AIRDispatcher:SetSquadronTakeoffInAir( "SquadronName" ) -- -- @return #AI_AIR_DISPATCHER -- @@ -1697,10 +2319,10 @@ do -- AI_AIR_DISPATCHER -- @param #AI_AIR_DISPATCHER self -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights by default take-off from the runway. - -- A2GDispatcher:SetDefaultTakeoffFromRunway() + -- AIRDispatcher:SetDefaultTakeoffFromRunway() -- -- @return #AI_AIR_DISPATCHER -- @@ -1717,10 +2339,10 @@ do -- AI_AIR_DISPATCHER -- @param #string SquadronName The name of the squadron. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights take-off from the runway. - -- A2GDispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) + -- AIRDispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) -- -- @return #AI_AIR_DISPATCHER -- @@ -1736,10 +2358,10 @@ do -- AI_AIR_DISPATCHER -- @param #AI_AIR_DISPATCHER self -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights by default take-off at a hot parking spot. - -- A2GDispatcher:SetDefaultTakeoffFromParkingHot() + -- AIRDispatcher:SetDefaultTakeoffFromParkingHot() -- -- @return #AI_AIR_DISPATCHER -- @@ -1755,10 +2377,10 @@ do -- AI_AIR_DISPATCHER -- @param #string SquadronName The name of the squadron. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights take-off in the air. - -- A2GDispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) + -- AIRDispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) -- -- @return #AI_AIR_DISPATCHER -- @@ -1774,10 +2396,10 @@ do -- AI_AIR_DISPATCHER -- @param #AI_AIR_DISPATCHER self -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights take-off from a cold parking spot. - -- A2GDispatcher:SetDefaultTakeoffFromParkingCold() + -- AIRDispatcher:SetDefaultTakeoffFromParkingCold() -- -- @return #AI_AIR_DISPATCHER -- @@ -1794,10 +2416,10 @@ do -- AI_AIR_DISPATCHER -- @param #string SquadronName The name of the squadron. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights take-off from a cold parking spot. - -- A2GDispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) + -- AIRDispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) -- -- @return #AI_AIR_DISPATCHER -- @@ -1814,10 +2436,10 @@ do -- AI_AIR_DISPATCHER -- @param #number TakeoffAltitude The altitude in meters above the ground. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_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. + -- AIRDispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. -- -- @return #AI_AIR_DISPATCHER -- @@ -1834,10 +2456,10 @@ do -- AI_AIR_DISPATCHER -- @param #number TakeoffAltitude The altitude in meters above the ground. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_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. + -- AIRDispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. -- -- @return #AI_AIR_DISPATCHER -- @@ -1855,16 +2477,16 @@ do -- AI_AIR_DISPATCHER -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights by default despawn near the airbase when returning. - -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase ) + -- AIRDispatcher:SetDefaultLanding( AI_AIR_Dispatcher.Landing.NearAirbase ) -- -- -- Let new flights by default despawn after landing land at the runway. - -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtRunway ) + -- AIRDispatcher:SetDefaultLanding( AI_AIR_Dispatcher.Landing.AtRunway ) -- -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. - -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtEngineShutdown ) + -- AIRDispatcher:SetDefaultLanding( AI_AIR_Dispatcher.Landing.AtEngineShutdown ) -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetDefaultLanding( Landing ) @@ -1881,22 +2503,22 @@ do -- AI_AIR_DISPATCHER -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let new flights despawn near the airbase when returning. - -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase ) + -- AIRDispatcher:SetSquadronLanding( "SquadronName", AI_AIR_Dispatcher.Landing.NearAirbase ) -- -- -- Let new flights despawn after landing land at the runway. - -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtRunway ) + -- AIRDispatcher:SetSquadronLanding( "SquadronName", AI_AIR_Dispatcher.Landing.AtRunway ) -- -- -- Let new flights despawn after landing and parking, and after engine shutdown. - -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtEngineShutdown ) + -- AIRDispatcher:SetSquadronLanding( "SquadronName", AI_AIR_Dispatcher.Landing.AtEngineShutdown ) -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetSquadronLanding( SquadronName, Landing ) local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.Landing = Landing + DefenderSquadron:SetLanding( Landing ) return self end @@ -1907,11 +2529,11 @@ do -- AI_AIR_DISPATCHER -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_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 + -- local LandingMethod = AIRDispatcher:GetDefaultLanding( AI_AIR_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_AIR_Dispatcher.Landing.NearAirbase then -- ... -- end -- @@ -1927,18 +2549,18 @@ do -- AI_AIR_DISPATCHER -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_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 + -- local LandingMethod = AIRDispatcher:GetSquadronLanding( "SquadronName", AI_AIR_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_AIR_Dispatcher.Landing.NearAirbase then -- ... -- end -- function AI_AIR_DISPATCHER:GetSquadronLanding( SquadronName ) local DefenderSquadron = self:GetSquadron( SquadronName ) - return DefenderSquadron.Landing or self.DefenderDefault.Landing + return DefenderSquadron:GetLanding() or self.DefenderDefault.Landing end @@ -1946,10 +2568,10 @@ do -- AI_AIR_DISPATCHER -- @param #AI_AIR_DISPATCHER self -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let flights by default to land near the airbase and despawn. - -- A2GDispatcher:SetDefaultLandingNearAirbase() + -- AIRDispatcher:SetDefaultLandingNearAirbase() -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetDefaultLandingNearAirbase() @@ -1965,10 +2587,10 @@ do -- AI_AIR_DISPATCHER -- @param #string SquadronName The name of the squadron. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let flights to land near the airbase and despawn. - -- A2GDispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) + -- AIRDispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) @@ -1983,10 +2605,10 @@ do -- AI_AIR_DISPATCHER -- @param #AI_AIR_DISPATCHER self -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let flights by default land at the runway and despawn. - -- A2GDispatcher:SetDefaultLandingAtRunway() + -- AIRDispatcher:SetDefaultLandingAtRunway() -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetDefaultLandingAtRunway() @@ -2002,10 +2624,10 @@ do -- AI_AIR_DISPATCHER -- @param #string SquadronName The name of the squadron. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let flights land at the runway and despawn. - -- A2GDispatcher:SetSquadronLandingAtRunway( "SquadronName" ) + -- AIRDispatcher:SetSquadronLandingAtRunway( "SquadronName" ) -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) @@ -2020,10 +2642,10 @@ do -- AI_AIR_DISPATCHER -- @param #AI_AIR_DISPATCHER self -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let flights by default land and despawn at engine shutdown. - -- A2GDispatcher:SetDefaultLandingAtEngineShutdown() + -- AIRDispatcher:SetDefaultLandingAtEngineShutdown() -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetDefaultLandingAtEngineShutdown() @@ -2039,10 +2661,10 @@ do -- AI_AIR_DISPATCHER -- @param #string SquadronName The name of the squadron. -- @usage: -- - -- local A2GDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) -- -- -- Let flights land and despawn at engine shutdown. - -- A2GDispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) + -- AIRDispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) -- -- @return #AI_AIR_DISPATCHER function AI_AIR_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) @@ -2059,11 +2681,11 @@ do -- AI_AIR_DISPATCHER -- @return #AI_AIR_DISPATCHER -- @usage -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) -- -- -- Now Setup the default fuel treshold. - -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- AIRDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. -- function AI_AIR_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) @@ -2081,16 +2703,16 @@ do -- AI_AIR_DISPATCHER -- @return #AI_AIR_DISPATCHER -- @usage -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_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. + -- AIRDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. -- function AI_AIR_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.FuelThreshold = FuelThreshold + DefenderSquadron:SetFuelThreshold( FuelThreshold ) return self end @@ -2101,14 +2723,14 @@ do -- AI_AIR_DISPATCHER -- @return #AI_AIR_DISPATCHER -- @usage -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) -- -- -- Now Setup the default fuel treshold. - -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- AIRDispatcher: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. + -- AIRDispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. function AI_AIR_DISPATCHER:SetDefaultTanker( TankerName ) self.DefenderDefault.TankerName = TankerName @@ -2124,47 +2746,67 @@ do -- AI_AIR_DISPATCHER -- @return #AI_AIR_DISPATCHER -- @usage -- - -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. - -- A2GDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_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. + -- AIRDispatcher: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. + -- AIRDispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. function AI_AIR_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) local DefenderSquadron = self:GetSquadron( SquadronName ) - DefenderSquadron.TankerName = TankerName + DefenderSquadron:SetTankerName( TankerName ) return self end + --- Set the frequency of communication and the mode of communication for voice overs. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number RadioFrequency The frequency of communication. + -- @param #number RadioModulation The modulation of communication. + -- @param #number RadioPower The power in Watts of communication. + function AI_AIR_DISPATCHER:SetSquadronRadioFrequency( SquadronName, RadioFrequency, RadioModulation, RadioPower, Language ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron:SetRadio( RadioFrequency, RadioModulation, RadioPower, DefenderSquadron.Language ) + end + -- TODO: Need to model the resources in a squadron. + --- @param #AI_AIR_DISPATCHER self + -- @param AI.AI_Air_Squadron#AI_AIR_SQUADRON Squadron function AI_AIR_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 + local SquadronResourceCount = Squadron:GetResourceCount() + if SquadronResourceCount then + Squadron:RemoveResources( Size ) end self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) end --- @param #AI_AIR_DISPATCHER self + -- @param AI.AI_Air_Squadron#AI_AIR_SQUADRON Squadron function AI_AIR_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() - if Squadron.ResourceCount then - Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize() + local SquadronResourceCount = Squadron:GetResourceCount() + if SquadronResourceCount then + Squadron:AddResources( Defender:GetSize() ) end self.Defenders[ DefenderName ] = nil - self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + self:F( { DefenderName = DefenderName, SquadronResourceCount = SquadronResourceCount } ) end + --- @param #AI_AIR_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender + -- @return AI.AI_Air_Squadron#AI_AIR_SQUADRON The Squadron. function AI_AIR_DISPATCHER:GetSquadronFromDefender( Defender ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() @@ -2379,32 +3021,6 @@ do -- AI_AIR_DISPATCHER return nil, nil end - --- - -- @param #AI_AIR_DISPATCHER self - function AI_AIR_DISPATCHER:onafterPatrol( From, Event, To, SquadronName, DefenseTaskType ) - - local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType ) - - -- Determine if there are sufficient resources to form a complete group for patrol. - if DefenderSquadron then - local DefendersNeeded - local DefendersGrouping = ( DefenderSquadron.Grouping or self.DefenderDefault.Grouping ) - if DefenderSquadron.ResourceCount == nil then - DefendersNeeded = DefendersGrouping - else - if DefenderSquadron.ResourceCount >= DefendersGrouping then - DefendersNeeded = DefendersGrouping - else - DefendersNeeded = DefenderSquadron.ResourceCount - end - end - - if Patrol then - self:ResourceQueue( true, DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, nil, SquadronName ) - end - end - - end --- -- @param #AI_AIR_DISPATCHER self @@ -2428,699 +3044,48 @@ do -- AI_AIR_DISPATCHER end - --- + + + + --- Shows the tactical display. -- @param #AI_AIR_DISPATCHER self - function AI_AIR_DISPATCHER:ResourceTakeoff() + function AI_AIR_DISPATCHER:ShowTacticalDisplay( Detection ) - 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_AIR_DISPATCHER self - function AI_AIR_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_AIR_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - - if Squadron then - Dispatcher:MessageToPlayers( Squadron, "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_AIR_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " returning." ) - - Dispatcher:ClearDefenderTaskTarget( Defender ) - end - - --- @param #AI_AIR_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_AIR_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) - if Defender:IsAboveRunway() then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - end - - --- @param #AI_AIR_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_AIR_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) - - if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - - if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_AIR_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - Dispatcher:ResourcePark( Squadron, Defender ) - end - end - end - - end - - - --- - -- @param #AI_AIR_DISPATCHER self - function AI_AIR_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_AIR_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_AIR_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) - - self:F( { DefenderTarget = DefenderTarget } ) - - if DefenderTarget then - Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " airborne." ) - Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit - end - end - - function Fsm:OnAfterEngageRoute( Defender, From, Event, To, AttackSetUnit ) - self:F({"Engage Route", Defender:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - - local DefenderName = Defender:GetName() - local Dispatcher = Fsm:GetDispatcher() -- #AI_AIR_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - local FirstUnit = AttackSetUnit:GetFirst() - local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - - Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " on route, bearing " .. Coordinate:ToString( Defender ) ) - end - - function Fsm:OnAfterEngage( Defender, From, Event, To, AttackSetUnit ) - self:F({"Engage Route", Defender:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - - local DefenderName = Defender:GetName() - local Dispatcher = Fsm:GetDispatcher() -- #AI_AIR_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - local FirstUnit = AttackSetUnit:GetFirst() - local Coordinate = FirstUnit:GetCoordinate() - - Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " engaging target, bearing " .. Coordinate:ToString( Defender ) ) - end - - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"Defender RTB", Defender:GetName()}) - - local DefenderName = Defender:GetName() - local Dispatcher = self:GetDispatcher() -- #AI_AIR_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " RTB." ) - - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) - - Dispatcher:ClearDefenderTaskTarget( Defender ) - end - - --- @param #AI_AIR_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_AIR_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) - - if Defender:IsAboveRunway() then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - end - - --- @param #AI_AIR_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_AIR_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " landing." ) - - if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - - if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_AIR_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - Dispatcher:ResourcePark( Squadron, Defender ) - end - end - end - end - - --- - -- @param #AI_AIR_DISPATCHER self - function AI_AIR_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( AttackerDetection.Set ) -- Engage on the TargetSetUnit - - self:SetDefenderTaskTarget( Defender, AttackerDetection ) - - end - end - end - - --- - -- @param #AI_AIR_DISPATCHER self - function AI_AIR_DISPATCHER:HasDefenseLine( DefenseCoordinate, DetectedItem ) - - local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) - local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) - - -- Now check if this coordinate is not in a danger zone, meaning, that the attack line is not crossing other coordinates. - -- (y1 – y2)x + (x2 – x1)y + (x1y2 – x2y1) = 0 - - local c1 = DefenseCoordinate - local c2 = AttackCoordinate - - local a = c1.z - c2.z -- Calculate a - local b = c2.x - c1.x -- Calculate b - local c = c1.x * c2.z - c2.x * c1.z -- calculate c - - local ok = true - - -- Now we check if each coordinate radius of about 30km of each attack is crossing a defense line. If yes, then this is not a good attack! - for AttackItemID, CheckAttackItem in pairs( self.Detection:GetDetectedItems() ) do - - -- Only compare other detected coordinates. - if AttackItemID ~= DetectedItem.ID then - - local CheckAttackCoordinate = self.Detection:GetDetectedItemCoordinate( CheckAttackItem ) - - local x = CheckAttackCoordinate.x - local y = CheckAttackCoordinate.z - local r = 5000 - - -- now we check if the coordinate is intersecting with the defense line. - - local IntersectDistance = ( math.abs( a * x + b * y + c ) ) / math.sqrt( a * a + b * b ) - self:F( { IntersectDistance = IntersectDistance, x = x, y = y } ) - - local IntersectAttackDistance = CheckAttackCoordinate:Get2DDistance( DefenseCoordinate ) - - self:F( { IntersectAttackDistance=IntersectAttackDistance, EvaluateDistance=EvaluateDistance } ) - - -- If the distance of the attack coordinate is larger than the test radius; then the line intersects, and this is not a good coordinate. - if IntersectDistance < r and IntersectAttackDistance < EvaluateDistance then - ok = false - break - end - end - end - - return ok - end - - --- - -- @param #AI_AIR_DISPATCHER self - function AI_AIR_DISPATCHER:onafterDefend( From, Event, To, DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, DefenderFriendlies, DefenseTaskType ) - - self:F( { From, Event, To, DetectedItem.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } ) - - DetectedItem.Type = DefenseTaskType -- This is set to report the task type in the status panel. - - local AttackerSet = DetectedItem.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 - - -- Here we check if the defenders have a defense line to the attackers. - -- If the attackers are behind enemy lines or too close to an other defense line; then don´t engage. - local DefenseCoordinate = DefenderGroup:GetCoordinate() - local HasDefenseLine = self:HasDefenseLine( DefenseCoordinate, DetectedItem ) - - if HasDefenseLine == true then - local SquadronName = self:GetDefenderTask( DefenderGroup ).SquadronName - local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) - - local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) - Fsm:EngageRoute( AttackerSet ) -- Engage on the TargetSetUnit - - self:SetDefenderTaskTarget( DefenderGroup, DetectedItem ) - - local DefenderGroupSize = DefenderGroup:GetSize() - DefendersMissing = DefendersMissing - DefenderGroupSize / SquadronOverhead - DefendersTotal = DefendersTotal + DefenderGroupSize / SquadronOverhead - end - - 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 AirbaseCoordinate = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE - local AttackerCoord = AttackerUnit:GetCoordinate() - local InterceptCoord = DetectedItem.InterceptCoord - self:F( { InterceptCoord = InterceptCoord } ) - if InterceptCoord then - local InterceptDistance = AirbaseCoordinate:Get2DDistance( InterceptCoord ) - local AirbaseDistance = AirbaseCoordinate: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 - - -- Check if there is a defense line... - local HasDefenseLine = self:HasDefenseLine( AirbaseCoordinate, DetectedItem ) - if HasDefenseLine == true then - ClosestDistance = InterceptDistance - ClosestDefenderSquadronName = SquadronName - end - 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 - self:ResourceQueue( false, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, DetectedItem, ClosestDefenderSquadronName ) - DefendersNeeded = DefendersNeeded - DefenderGrouping - DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead - 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_AIR_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_AIR_DISPATCHER:Evaluate_SEAD( DetectedItem ) - self:F( { DetectedItem.ItemID } ) - - local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT - local AttackerCount = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group, then the amount of radar emitters will be returned; that need to be attacked. - - if ( AttackerCount > 0 ) then - - -- First, count the active defenders, engaging the DetectedItem. - local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) - - 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_AIR_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_AIR_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? - - if IsCas == true then - - -- First, count the active defenders, engaging the DetectedItem. - local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) - - 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_AIR_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_AIR_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, AttackerCount ) - - 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_AIR_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_AIR_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 - 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 - 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 TargetCount = DefenderTask.Target.Set:Count() - if TargetCount == 0 then - self:F( { "All Targets destroyed in Target, removing:", DefenderTask.Target.Index } ) - self:ClearDefenderTask( DefenderGroup ) - end - end - end - end - end - end + local DefenseTotal = 0 local Report = REPORT:New( "\nTactical Overview" ) local DefenderGroupCount = 0 - local DefendersTotal = 0 -- Now that all obsolete tasks are removed, loop through the detected targets. - for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) 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 AttackCoordinate = 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 EngageCoordinate = nil - - for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do - local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE - - local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) - - 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 - EngageCoordinate = DefenseCoordinate - break - end - end - end - - if EngageCoordinate 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", EngageCoordinate ) - end - end + if not self.Detection:IsDetectedItemLocked( DetectedItem ) == true then + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone - 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", EngageCoordinate ) - end - end + self:F( { "Target ID", DetectedItem.ItemID } ) + + self:F( { DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed - 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", EngageCoordinate ) - 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 - local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() - Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "â– ", ThreatLevel ) ) ) + local ThreatLevel = DetectedItem.Set:CalculateThreatLevelAIR() + Report:Add( string.format( " - %1s%s ( %04s ): ( #%02d - %-4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "â– ", ThreatLevel ) ) ) for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do local Defender = Defender -- Wrapper.Group#GROUP if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then @@ -3142,52 +3107,50 @@ do -- AI_AIR_DISPATCHER 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 + 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 - Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) + 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_AIR_DISPATCHER.DefenseQueueItem - Report:Add( string.format( " - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) ) - - end + Report:Add( string.format( "\n - %d Queued Aircraft Launches", #self.DefenseQueue ) ) + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + local DefenseQueueItem = DefenseQueueItem -- #AI_AIR_DISPATCHER.DefenseQueueItem + Report:Add( string.format( " - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) ) - Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) ) - for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do - Report:Add( string.format( " - %s - %d", DefenderSquadronName, DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount or "n/a" ) ) - end - - self:F( Report:Text( "\n" ) ) - trigger.action.outText( Report:Text( "\n" ), 25 ) end - return true - end + Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) ) + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + Report:Add( string.format( " - %s - %d", DefenderSquadronName, DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount or "n/a" ) ) + end + self:F( Report:Text( "\n" ) ) + trigger.action.outText( Report:Text( "\n" ), 25 ) + + end + end + do --- Calculates which HUMAN friendlies are nearby the area. @@ -3203,7 +3166,7 @@ do local PlayersCount = 0 if PlayersNearBy then - local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelAIR() for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT local PlayerName = PlayerUnit:GetPlayerName() @@ -3249,7 +3212,7 @@ do local FriendliesCount = 0 if FriendlyUnitsNearBy then - local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelAIR() for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT if FriendlyUnit:IsAirPlane() then @@ -3280,18 +3243,6 @@ do return FriendliesCount, FriendlyTypesReport end - --- Schedules a new Patrol for the given SquadronName. - -- @param #AI_AIR_DISPATCHER self - -- @param #string SquadronName The squadron name. - function AI_AIR_DISPATCHER:SchedulerPatrol( SquadronName ) - local PatrolTaskTypes = { "SEAD", "CAS", "BAI" } - local PatrolTaskType = PatrolTaskTypes[math.random(1,3)] - self:Patrol( SquadronName, PatrolTaskType ) - end - - - end - diff --git a/Moose Development/Moose/AI/AI_Air_Engage.lua b/Moose Development/Moose/AI/AI_Air_Engage.lua index a14d604aa..bbfd82497 100644 --- a/Moose Development/Moose/AI/AI_Air_Engage.lua +++ b/Moose Development/Moose/AI/AI_Air_Engage.lua @@ -364,7 +364,6 @@ end function AI_AIR_ENGAGE:onafterAbort( AIGroup, From, Event, To ) AIGroup:ClearTasks() self:Return() - self:__RTB( self.TaskDelay ) end @@ -500,7 +499,6 @@ function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, Attac else self:I( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() - self:__RTB( self.TaskDelay ) end end @@ -574,14 +572,14 @@ function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetU if TargetDistance <= EngageDistance * 3 then - local AttackUnitTasks = self:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) + local AttackUnitTasks = self:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) -- Polymorphic if #AttackUnitTasks == 0 then - self:I( DefenderGroupName .. ": No targets found -> Going RTB") + self:I( DefenderGroupName .. ": No valid targets found -> Going RTB") self:Return() - self:__RTB( self.TaskDelay ) return else + self:I( DefenderGroupName .. ": Engaging targets " ) DefenderGroup:OptionROEOpenFire() DefenderGroup:OptionROTEvadeFire() DefenderGroup:OptionKeepWeaponsOnThreat() @@ -597,9 +595,8 @@ function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetU end else - self:I( DefenderGroupName .. ": No targets found -> Going RTB") + self:I( DefenderGroupName .. ": No targets found -> returning.") self:Return() - self:__RTB( self.TaskDelay ) return end end diff --git a/Moose Development/Moose/AI/AI_Air_Squadron.lua b/Moose Development/Moose/AI/AI_Air_Squadron.lua new file mode 100644 index 000000000..d55e65ded --- /dev/null +++ b/Moose Development/Moose/AI/AI_Air_Squadron.lua @@ -0,0 +1,289 @@ +--- **AI** -- Models squadrons for airplanes and helicopters. +-- +-- This is a class used in the @{AI_Air_Dispatcher} and derived dispatcher classes. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Air_Squadron +-- @image AI_Air_To_Air_Engage.JPG + + + +--- @type AI_AIR_SQUADRON +-- @extends Core.Base#BASE + + +--- Implements the core functions modeling squadrons for airplanes and helicopters. +-- +-- === +-- +-- @field #AI_AIR_SQUADRON +AI_AIR_SQUADRON = { + ClassName = "AI_AIR_SQUADRON", +} + + + +--- Creates a new AI_AIR_SQUADRON object +-- @param #AI_AIR_SQUADRON self +-- @return #AI_AIR_SQUADRON +function AI_AIR_SQUADRON:New( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) + + self:I( { Air_Squadron = { SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) + + local AI_Air_Squadron = BASE:New() -- #AI_AIR_SQUADRON + + AI_Air_Squadron.Name = SquadronName + AI_Air_Squadron.Airbase = AIRBASE:FindByName( AirbaseName ) + AI_Air_Squadron.AirbaseName = AI_Air_Squadron.Airbase:GetName() + if not AI_Air_Squadron.Airbase then + error( "Cannot find airbase with name:" .. AirbaseName ) + end + + AI_Air_Squadron.Spawn = {} + if type( TemplatePrefixes ) == "string" then + local SpawnTemplate = TemplatePrefixes + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + AI_Air_Squadron.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 ) + AI_Air_Squadron.Spawn[#AI_Air_Squadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] + end + end + AI_Air_Squadron.ResourceCount = ResourceCount + AI_Air_Squadron.TemplatePrefixes = TemplatePrefixes + AI_Air_Squadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured. + + self:SetSquadronLanguage( SquadronName, "EN" ) -- Squadrons speak English by default. + + return AI_Air_Squadron +end + +--- Set the Name of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #string Name The Squadron Name. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetName( Name ) + + self.Name = Name + + return self +end + +--- Get the Name of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #string The Squadron Name. +function AI_AIR_SQUADRON:GetName() + + return self.Name +end + +--- Set the ResourceCount of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number ResourceCount The Squadron ResourceCount. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetResourceCount( ResourceCount ) + + self.ResourceCount = ResourceCount + + return self +end + +--- Get the ResourceCount of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron ResourceCount. +function AI_AIR_SQUADRON:GetResourceCount() + + return self.ResourceCount +end + +--- Add Resources to the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number Resources The Resources to be added. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:AddResources( Resources ) + + self.ResourceCount = self.ResourceCount + Resources + + return self +end + +--- Remove Resources to the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number Resources The Resources to be removed. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:RemoveResources( Resources ) + + self.ResourceCount = self.ResourceCount - Resources + + return self +end + +--- Set the Overhead of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number Overhead The Squadron Overhead. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetOverhead( Overhead ) + + self.Overhead = Overhead + + return self +end + +--- Get the Overhead of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron Overhead. +function AI_AIR_SQUADRON:GetOverhead() + + return self.Overhead +end + +--- Set the Grouping of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number Grouping The Squadron Grouping. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetGrouping( Grouping ) + + self.Grouping = Grouping + + return self +end + +--- Get the Grouping of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron Grouping. +function AI_AIR_SQUADRON:GetGrouping() + + return self.Grouping +end + +--- Set the FuelThreshold of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number FuelThreshold The Squadron FuelThreshold. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetFuelThreshold( FuelThreshold ) + + self.FuelThreshold = FuelThreshold + + return self +end + +--- Get the FuelThreshold of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron FuelThreshold. +function AI_AIR_SQUADRON:GetFuelThreshold() + + return self.FuelThreshold +end + +--- Set the EngageProbability of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number EngageProbability The Squadron EngageProbability. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetEngageProbability( EngageProbability ) + + self.EngageProbability = EngageProbability + + return self +end + +--- Get the EngageProbability of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron EngageProbability. +function AI_AIR_SQUADRON:GetEngageProbability() + + return self.EngageProbability +end + +--- Set the Takeoff of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number Takeoff The Squadron Takeoff. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetTakeoff( Takeoff ) + + self.Takeoff = Takeoff + + return self +end + +--- Get the Takeoff of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron Takeoff. +function AI_AIR_SQUADRON:GetTakeoff() + + return self.Takeoff +end + +--- Set the Landing of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number Landing The Squadron Landing. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetLanding( Landing ) + + self.Landing = Landing + + return self +end + +--- Get the Landing of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron Landing. +function AI_AIR_SQUADRON:GetLanding() + + return self.Landing +end + +--- Set the TankerName of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #string TankerName The Squadron Tanker Name. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetTankerName( TankerName ) + + self.TankerName = TankerName + + return self +end + +--- Get the Tanker Name of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #string The Squadron Tanker Name. +function AI_AIR_SQUADRON:GetTankerName() + + return self.TankerName +end + + +--- Set the Radio of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number RadioFrequency The frequency of communication. +-- @param #number RadioModulation The modulation of communication. +-- @param #number RadioPower The power in Watts of communication. +-- @param #string Language The language of the radio speech. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetRadio( RadioFrequency, RadioModulation, RadioPower, Language ) + + self.RadioFrequency = RadioFrequency + self.RadioModulation = RadioModulation or radio.modulation.AM + self.RadioPower = RadioPower or 100 + + if self.RadioSpeech then + self.RadioSpeech:Stop() + end + + self.RadioSpeech = nil + + self.RadioSpeech = RADIOSPEECH:New( RadioFrequency, RadioModulation ) + self.RadioSpeech.power = RadioPower + self.RadioSpeech:Start( 0.5 ) + + self.RadioSpeech:SetLanguage( Language ) + + return self +end + + diff --git a/Moose Development/Moose/Core/Scheduler.lua b/Moose Development/Moose/Core/Scheduler.lua index a0541c440..c5f796185 100644 --- a/Moose Development/Moose/Core/Scheduler.lua +++ b/Moose Development/Moose/Core/Scheduler.lua @@ -214,6 +214,7 @@ function SCHEDULER:New( SchedulerObject, SchedulerFunction, SchedulerArguments, local ScheduleID = nil self.MasterObject = SchedulerObject + self.ShowTrace = true if SchedulerFunction then ScheduleID = self:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, 4 ) From 6cc233cac76cb4250b74a0bd77ec9ca8673c38cd Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 2 Oct 2019 19:45:37 +0200 Subject: [PATCH 374/485] ATIS - COORDINATE - RADIO - RADIOQUEUE - ATIS - UTILS - CONTROLLABLE --- Moose Development/Moose/Core/Point.lua | 42 +- Moose Development/Moose/Core/Radio.lua | 5 +- Moose Development/Moose/Core/RadioQueue.lua | 36 +- Moose Development/Moose/Ops/ATIS.lua | 1077 +++++++++++++++++ Moose Development/Moose/Utilities/Utils.lua | 38 +- .../Moose/Wrapper/Controllable.lua | 929 +++++++------- 6 files changed, 1625 insertions(+), 502 deletions(-) create mode 100644 Moose Development/Moose/Ops/ATIS.lua diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 8e2dc7648..7c600e697 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -562,7 +562,7 @@ do -- COORDINATE --- Return the height of the land at the coordinate. -- @param #COORDINATE self - -- @return #number + -- @return #number Land height (ASL) in meters. function COORDINATE:GetLandHeight() local Vec2 = { x = self.x, y = self.z } return land.getHeight( Vec2 ) @@ -1100,10 +1100,8 @@ do -- COORDINATE elseif AirbaseCategory == Airbase.Category.AIRDROME then RoutePoint.airdromeId = AirbaseID else - self:T("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") + self:E("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") end - - --self:MarkToAll(string.format("Landing waypoint at airbase %s, ID=%d, Category=%d", airbase:GetName(), AirbaseID, AirbaseCategory )) end -- Time in minutes to stay at the airbase before resuming route. @@ -1270,17 +1268,23 @@ do -- COORDINATE -- Loop over all airbases. for _,_airbase in pairs(airbases) do local airbase=_airbase --Wrapper.Airbase#AIRBASE - local category=airbase:GetDesc().category - if Category and Category==category or Category==nil then - local dist=self:Get2DDistance(airbase:GetCoordinate()) - if closest==nil then - distmin=dist - closest=airbase - else - if dist Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- Update status. + self:AddTransition("*", "Broadcast", "*") -- Broadcast ATIS message. + self:AddTransition("*", "CheckQueue", "*") -- Check if radio queue is empty. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the ATIS. + -- @function [parent=#ATIS] Start + -- @param #ATIS self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#ATIS] __Start + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the ATIS. + -- @param #ATIS self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#ATIS] __Stop + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set sound files folder within miz file. +-- @param #ATIS self +-- @param #string path Path for sound files. Default "ATIS Soundfiles/". Mind the slash "/" at the end! +-- @return #ATIS self +function ATIS:SetSoundfilesPath(path) + self.soundpath=tostring(path or "ATIS Soundfiles/") + self:I(self.lid..string.format("Setting sound files path to %s", self.soundpath)) + return self +end + +--- Set airborne unit (airplane or helicopter), used to transmit radio messages including subtitles. +-- Best is to place the unit on a parking spot of the airbase and set it to *uncontrolled* in the mission editor. +-- @param #ATIS self +-- @param #string unitname Name of the unit. +-- @return #ATIS self +function ATIS:SetRadioRelayUnitName(unitname) + self.relayunitname=unitname + self:I(self.lid..string.format("Setting radio relay unit to %s", self.relayunitname)) + return self +end + +--- Set tower frequencies. +-- @param #ATIS self +-- @param #table freqs Table of frequencies in MHz. A single frequency can be given as a plain number (i.e. must not be table). +-- @return #ATIS self +function ATIS:SetTowerFrequencies(freqs) + if type(freqs)=="table" then + -- nothing to do + else + freqs={freqs} + end + self.towerfrequency=freqs + return self +end + +--- Set active runway. This can be used if the automatic runway determination via the wind direction gives incorrect results. +-- For example, use this if there are two runways with the same directions. +-- @param #ATIS self +-- @param #string runway Active runway, e.g. "31L". +-- @return #ATIS self +function ATIS:SetActiveRunway(runway) + self.activerunway=tostring(runway) + return self +end + +--- Set duration how long subtitles are displayed. +-- @param #ATIS self +-- @param #number duration Duration in seconds. Default 10 seconds. +-- @return #ATIS self +function ATIS:SetSubtitleDuration(duration) + self.subduration=tonumber(duration) or 10 + return self +end + +--- Set unit system to metric units. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetMetricUnits() + self.metric=true + return self +end + +--- Set unit system to imperial units. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetImperialUnits() + self.metric=false + return self +end + +--- Set pressure unit to millimeters of mercury (mmHg). +-- Default is inHg for imperial and hPa (=mBar) for metric units. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetPressureMillimetersMercury() + self.PmmHg=true + return self +end + +--- Set temperature to be given in degrees Fahrenheit. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetTemperatureFahrenheit() + self.TDegF=true + return self +end + +--- Set magnetic declination/variation at the airport. +-- +-- Default is per map: +-- +-- * Caucasus +6 (East), year ~ 2011 +-- * NTTR +12 (East), year ~ 2011 +-- * Normandy -10 (West), year ~ 1944 +-- * Persian Gulf +2 (East), year ~ 2011 +-- +-- @param #ATIS self +-- @param #number magvar Magnetic variation in degrees. +-- @return #ATIS self +function ATIS:SetMagneticDeclination(magvar) + self.magvar=magvar + return self +end + +--- Set time local difference with respect to Zulu time. +-- Default is per map: +-- +-- * Caucasus +4 +-- * Nevada -7 +-- * Normandy +1 +-- * Persian Gulf +4 +-- +-- @param #ATIS self +-- @param #number delta Time difference in hours. +-- @return #ATIS self +function ATIS:SetZuluTimeDifference(delta) + self.zuludiff=delta + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start ATIS FSM. +-- @param #ATIS self +function ATIS:onafterStart(From, Event, To) + + -- Info. + self:I(self.lid..string.format("Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation)) + + -- Start radio queue. + self.radioqueue=RADIOQUEUE:New(self.frequency, self.modulation) + + -- Send coordinate is airbase coord. + self.radioqueue:SetSenderCoordinate(self.airbase:GetCoordinate()) + + -- Set relay unit if we have one. + self.radioqueue:SetSenderUnitName(self.relayunitname) + + -- Init numbers. + self.radioqueue:SetDigit(0, "N-0.ogg", 0.55, self.soundpath) + self.radioqueue:SetDigit(1, "N-1.ogg", 0.40, self.soundpath) + self.radioqueue:SetDigit(2, "N-2.ogg", 0.35, self.soundpath) + self.radioqueue:SetDigit(3, "N-3.ogg", 0.40, self.soundpath) + self.radioqueue:SetDigit(4, "N-4.ogg", 0.36, self.soundpath) + self.radioqueue:SetDigit(5, "N-5.ogg", 0.42, self.soundpath) + self.radioqueue:SetDigit(6, "N-6.ogg", 0.53, self.soundpath) + self.radioqueue:SetDigit(7, "N-7.ogg", 0.42, self.soundpath) + self.radioqueue:SetDigit(8, "N-8.ogg", 0.37, self.soundpath) + self.radioqueue:SetDigit(9, "N-9.ogg", 0.38, self.soundpath) + + -- Start radio queue. + self.radioqueue:Start(1, 0.1) + + -- Init status updates. + self:__Status(-2) + self:__CheckQueue(-3) +end + +--- Update status. +-- @param #ATIS self +function ATIS:onafterStatus(From, Event, To) + + -- Get FSM state. + local fsmstate=self:GetState() + + -- Info text. + local text=string.format("State %s", fsmstate) + self:I(self.lid..text) + + self:__Status(30) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if radio queue is empty. If so, start broadcasting the message again. +-- @param #ATIS self +function ATIS:onafterCheckQueue(From, Event, To) + + if #self.radioqueue.queue==0 then + self:T(self.lid..string.format("Radio queue empty. Repeating message.")) + self:Broadcast() + else + self:T2(self.lid..string.format("Radio queue %d transmissions queued.", #self.radioqueue.queue)) + end + + -- Check back in 5 seconds. + self:__CheckQueue(5) +end + +--- Broadcast ATIS radio message. +-- @param #ATIS self +function ATIS:onafterBroadcast(From, Event, To) + + -- Get current coordinate. + local coord=self.airbase:GetCoordinate() + + -- Get elevation. + local height=coord:GetLandHeight()+10 + + ---------------- + --- Pressure --- + ---------------- + + -- Pressure in hPa. + local qfe=coord:GetPressure(height) + local qnh=coord:GetPressure(0) + + -- Convert to inHg. + if self.PmmHg then + qfe=UTILS.hPa2mmHg(qfe) + qnh=UTILS.hPa2mmHg(qnh) + else + if not self.metric then + qfe=UTILS.hPa2inHg(qfe) + qnh=UTILS.hPa2inHg(qnh) + end + end + + local QFE=UTILS.Split(string.format("%.2f", qfe), ".") + local QNH=UTILS.Split(string.format("%.2f", qnh), ".") + + if self.PmmHg then + QFE=UTILS.Split(string.format("%.1f", qfe), ".") + QNH=UTILS.Split(string.format("%.1f", qnh), ".") + else + if self.metric then + QFE=UTILS.Split(string.format("%.1f", qfe), ".") + QNH=UTILS.Split(string.format("%.1f", qnh), ".") + end + end + + -------------- + --- Runway --- + -------------- + + -- Get runway based on wind direction. + local runway=self.airbase:GetActiveRunway(self.magvar).idx + + -- Left or right in case there are two runways with the same heading. + local rleft=false + local rright=false + + -- Check if user explicitly specified a runway. + if self.activerunway then + local runwayno=self.activerunway:gsub("%D+", "") + if runwayno~="" then + runway=runwayno + end + rleft=self.activerunway:lower():find("l") + rright=self.activerunway:lower():find("r") + end + + ------------ + --- Wind --- + ------------ + + -- Get wind direction and speed in m/s. + local windFrom, windSpeed=coord:GetWind(height) + + + local WINDFROM=string.format("%03d", windFrom) + local WINDSPEED=string.format("%d", UTILS.MpsToKnots(windSpeed)) + + if self.metric then + WINDSPEED=string.format("%d", windSpeed) + end + + ------------ + --- Time --- + ------------ + local time=timer.getAbsTime() + + -- Conversion to Zulu time. + if self.zuludiff then + -- User specified. + time=time-self.zuludiff*60*60 + else + if self.theatre==DCSMAP.Caucasus then + time=time-4*60*60 -- Caucasus UTC+4 hours + elseif self.theatre==DCSMAP.PersianGulf then + time=time-4*60*60 -- Abu Dhabi UTC+4 hours + elseif self.theatre==DCSMAP.NTTR then + time=time+7*60*60 -- Las Vegas UTC-7 hours + elseif self.theatre==DCSMAP.Normandy then + time=time-1*60*60 -- Calais UTC+1 hour + end + end + + local clock=UTILS.SecondsToClock(time) + local zulu=UTILS.Split(clock, ":") + local ZULU=string.format("%s%s", zulu[1], zulu[2]) + + + -- NATO time stamp. 0=Alfa, 1=Bravo, 2=Charlie, etc. + local NATO=ATIS.Alphabet[tonumber(zulu[1])+1] + + -- Debug. + self:T3({nato=NATO}) + + ------------------- + --- Temperature --- + ------------------- + + -- Temperature in °C. + local temperature=coord:GetTemperature(height) + + local TEMPERATURE=string.format("%d", temperature) + + if self.TDegF then + TEMPERATURE=string.format("%d", UTILS.CelciusToFarenheit(temperature)) + end + + --------------- + --- Weather --- + --------------- + + -- Get mission weather info. Most of this is static. + local clouds, visibility, turbulence, fog, dust, static=self:GetMissionWeather() + + -- Check that fog is actually "thick" enough to reach the airport. If an airport is in the mountains, fog might not affect it as it is measured from sea level. + if fog and fog.thicknessUTILS.FeetToMeters(1500) then + dust=nil + end + + ------------------ + --- Visibility --- + ------------------ + + -- Get min visibility. + local visibilitymin=visibility + + if fog then + if fog.visibility=9 then + -- Overcast 9,10 + CLOUDSogg="CloudsOvercast.ogg" + CLOUDSsub="Overcast" + CLOUDSdur=0.85 + elseif clouddens>=7 then + -- Broken 7,8 + CLOUDSogg="CloudsBroken.ogg" + CLOUDSsub="Broken clouds" + CLOUDSdur=1.10 + elseif clouddens>=4 then + -- Scattered 4,5,6 + CLOUDSogg="CloudsScattered.ogg" + CLOUDSsub="Scattered clouds" + CLOUDSdur=1.20 + elseif clouddens>=1 then + -- Few 1,2,3 + CLOUDSogg="CloudsFew.ogg" + CLOUDSsub="Few clouds" + CLOUDSdur=1.00 + else + -- No clouds + CLOUDBASE=nil + CLOUDCEIL=nil + CLOUDSogg="CloudsNo.ogg" + CLOUDSsub="No clouds" + CLOUDSdur=1.00 + end + end + + -------------------- + --- Transmission --- + -------------------- + + local subduration=self.subduration + local subtitle="" + + --Airbase name + subtitle=string.format("%s", self.airbasename) + if self.airbasename:find("AFB")==nil and self.airbasename:find("Airport")==nil and self.airbasename:find("Airstrip")==nil and self.airbasename:find("airfield")==nil and self.airbasename:find("AB")==nil then + subtitle=subtitle.." Airport" + end + self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, subduration) + + -- Information tag + subtitle=string.format("Information %s", NATO) + self.radioqueue:NewTransmission("Information.ogg", 0.85, self.soundpath, nil, 0.5, subtitle, subduration) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + + -- Zulu Time + subtitle=string.format("%s Zulu Time", ZULU) + self.radioqueue:Number2Transmission(ZULU, nil, 0.5) + self.radioqueue:NewTransmission("TimeZulu.ogg", 0.89, self.soundpath, nil, 0.2, subtitle, subduration) + + -- Visibility + if self.metric then + subtitle=string.format("Visibility %s km", VISIBILITY) + else + subtitle=string.format("Visibility %s NM", VISIBILITY) + end + self.radioqueue:NewTransmission("Visibility.ogg", 0.8, self.soundpath, nil, 1.0, subtitle, subduration) + self.radioqueue:Number2Transmission(VISIBILITY) + if self.metric then + self.radioqueue:NewTransmission("Kilometers.ogg", 0.78, self.soundpath, nil, 0.2) + else + self.radioqueue:NewTransmission("NauticalMiles.ogg", 1.05, self.soundpath, nil, 0.2) + end + + -- Cloud base + self.radioqueue:NewTransmission(CLOUDSogg, CLOUDSdur, self.soundpath, nil, 1.0, CLOUDSsub, subduration) + if CLOUDBASE and static then + -- Base + if self.metric then + subtitle=string.format("Cloudbase %s, ceiling %s meters", CLOUDBASE, CLOUDCEIL) + else + subtitle=string.format("Cloudbase %s, ceiling %s ft", CLOUDBASE, CLOUDCEIL) + end + self.radioqueue:NewTransmission("CloudBase.ogg", 0.81, self.soundpath, nil, 1.0, subtitle, subduration) + if tonumber(CLOUDBASE1000)>0 then + self.radioqueue:Number2Transmission(CLOUDBASE1000) + self.radioqueue:NewTransmission("Thousand.ogg", 0.55, self.soundpath, nil, 0.1) + end + if tonumber(CLOUDBASE0100)>0 then + self.radioqueue:Number2Transmission(CLOUDBASE0100) + self.radioqueue:NewTransmission("Hundred.ogg", 0.47, self.soundpath, nil, 0.1) + end + -- Ceiling + self.radioqueue:NewTransmission("CloudCeiling.ogg", 0.62, self.soundpath, nil, 0.5) + if tonumber(CLOUDCEIL1000)>0 then + self.radioqueue:Number2Transmission(CLOUDCEIL1000) + self.radioqueue:NewTransmission("Thousand.ogg", 0.55, self.soundpath, nil, 0.1) + end + if tonumber(CLOUDCEIL0100)>0 then + self.radioqueue:Number2Transmission(CLOUDCEIL0100) + self.radioqueue:NewTransmission("Hundred.ogg", 0.47, self.soundpath, nil, 0.1) + end + if self.metric then + self.radioqueue:NewTransmission("Meters.ogg", 0.59, self.soundpath, nil, 0.1) + else + self.radioqueue:NewTransmission("Feet.ogg", 0.45, self.soundpath, nil, 0.1) + end + end + + -- Weather phenomena + local wp=false + local wpsub="" + if precepitation==1 then + wp=true + wpsub=wpsub.." rain" + elseif precepitation==2 then + if wp then + wpsub=wpsub.."," + end + wpsub=wpsub.." thunderstorm" + wp=true + end + if fog then + if wp then + wpsub=wpsub.."," + end + wpsub=wpsub.." fog" + wp=true + end + if dust then + if wp then + wpsub=wpsub.."," + end + wpsub=wpsub.." dust" + wp=true + end + -- Actual output + if wp then + self.radioqueue:NewTransmission("WeatherPhenomena.ogg", 1.07, self.soundpath, nil, 1.0, string.format("Weather phenomena:%s", wpsub), subduration) + if precepitation==1 then + self.radioqueue:NewTransmission("Rain.ogg", 0.41, self.soundpath, nil, 0.5) + elseif precepitation==2 then + self.radioqueue:NewTransmission("ThunderStorm.ogg", 0.81, self.soundpath, nil, 0.5) + end + if fog then + self.radioqueue:NewTransmission("Fog.ogg", 0.81, self.soundpath, nil, 0.5) + end + if dust then + self.radioqueue:NewTransmission("Dust.ogg", 0.81, self.soundpath, nil, 0.5) + end + end + + -- Altimeter QNH/QFE. + if self.PmmHg then + subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2]) + else + if self.metric then + subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2]) + else + subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2]) + end + end + self.radioqueue:NewTransmission("Altimeter.ogg", 0.7, self.soundpath, nil, 1.0, subtitle, subduration) + self.radioqueue:NewTransmission("QNH.ogg", 0.70, self.soundpath, nil, 0.5) + self.radioqueue:Number2Transmission(QNH[1]) + self.radioqueue:NewTransmission("Decimal.ogg", 0.58, self.soundpath, nil, 0.2) + self.radioqueue:Number2Transmission(QNH[2]) + self.radioqueue:NewTransmission("QFE.ogg", 0.62, self.soundpath, nil, 0.2) + self.radioqueue:Number2Transmission(QFE[1]) + self.radioqueue:NewTransmission("Decimal.ogg", 0.58, self.soundpath, nil, 0.2) + self.radioqueue:Number2Transmission(QFE[2]) + if self.PmmHg then + self.radioqueue:NewTransmission("MillimetersOfMercury.ogg", 1.53, self.soundpath, nil, 0.1) + else + if self.metric then + self.radioqueue:NewTransmission("HectoPascal.ogg", 1.15, self.soundpath, nil, 0.1) + else + self.radioqueue:NewTransmission("InchesOfMercury.ogg", 1.16, self.soundpath, nil, 0.1) + end + end + + -- Temperature + if self.TDegF then + subtitle=string.format("Temperature %s °F", TEMPERATURE) + else + subtitle=string.format("Temperature %s °C", TEMPERATURE) + end + self.radioqueue:NewTransmission("Temperature.ogg", 0.55, self.soundpath, nil, 1.0, subtitle, subduration) + self.radioqueue:Number2Transmission(TEMPERATURE) + if self.TDegF then + self.radioqueue:NewTransmission("DegreesFahrenheit.ogg", 1.23, self.soundpath, nil, 0.2) + else + self.radioqueue:NewTransmission("DegreesCelsius.ogg", 1.28, self.soundpath, nil, 0.2) + end + + -- Wind + if self.metric then + subtitle=string.format("Wind from %s at %s m/s", WINDFROM, WINDSPEED) + else + subtitle=string.format("Wind from %s at %s knots", WINDFROM, WINDSPEED) + end + if turbulence>0 then + subtitle=subtitle..", gusting" + end + self.radioqueue:NewTransmission("WindFrom.ogg", 0.60, self.soundpath, nil, 1.0, subtitle, subduration) + self.radioqueue:Number2Transmission(WINDFROM) + self.radioqueue:NewTransmission("At.ogg", 0.40, self.soundpath, nil, 0.2) + self.radioqueue:Number2Transmission(WINDSPEED) + if self.metric then + self.radioqueue:NewTransmission("MetersPerSecond.ogg", 1.14, self.soundpath, nil, 0.2) + else + self.radioqueue:NewTransmission("Knots.ogg", 0.60, self.soundpath, nil, 0.2) + end + if turbulence>0 then + self.radioqueue:NewTransmission("Gusting.ogg", 0.55, self.soundpath, nil, 0.2) + end + + -- Active runway. + local subtitle=string.format("Active runway %s", runway) + if rleft then + subtitle=subtitle.." Left" + elseif rright then + subtitle=subtitle.." Right" + end + self.radioqueue:NewTransmission("ActiveRunway.ogg", 1.05, self.soundpath, nil, 1.0, subtitle, subduration) + self.radioqueue:Number2Transmission(runway) + if rleft then + self.radioqueue:NewTransmission("Left.ogg", 0.53, self.soundpath, nil, 0.2) + elseif rright then + self.radioqueue:NewTransmission("Right.ogg", 0.43, self.soundpath, nil, 0.2) + end + + -- Tower frequency. + if self.towerfrequency then + local freqs="" + for i,freq in pairs(self.towerfrequency) do + freqs=freqs..string.format("%.3f MHz", freq) + if i<#self.towerfrequency then + freqs=freqs..", " + end + end + self.radioqueue:NewTransmission("TowerFrequency.ogg", 1.19, self.soundpath, nil, 1.0, string.format("Tower frequency %s", freqs), subduration) + for _,freq in pairs(self.towerfrequency) do + local f=string.format("%.3f", freq) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + self.radioqueue:NewTransmission("Decimal.ogg", 0.58, self.soundpath, nil, 0.2) + self.radioqueue:Number2Transmission(f[2]) + self.radioqueue:NewTransmission("MegaHertz.ogg", 0.86, self.soundpath, nil, 0.2) + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get weather of this mission from env.mission.weather variable. +-- @param #ATIS self +-- @return #table Clouds table which has entries "thickness", "density", "base", "iprecptns". +-- @return #number Visibility distance in meters. +-- @return #number Ground turbulence in m/s. +-- @return #table Fog table, which has entries "thickness", "visibility" or nil if fog is disabled in the mission. +-- @return #number Dust density or nil if dust is disabled in the mission. +-- @return #boolean static If true, static weather is used. If false, dynamic weather is used. +function ATIS:GetMissionWeather() + + -- Weather data from mission file. + local weather=env.mission.weather + + -- Clouds + --[[ + ["clouds"] = + { + ["thickness"] = 430, + ["density"] = 7, + ["base"] = 0, + ["iprecptns"] = 1, + }, -- end of ["clouds"] + ]] + local clouds=weather.clouds + + -- 0=static, 1=dynamic + local static=weather.atmosphere_type==0 + + -- Visibilty distance in meters. + local visibility=weather.visibility.distance + + -- Ground turbulence. + local turbulence=weather.groundTurbulence + + -- 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=nil + if weather.enable_fog==true then + fog=weather.fog + end + + self:T("FF weather:") + self:T({clouds=clouds}) + self:T({visibility=visibility}) + self:T({turbulence=turbulence}) + self:T({fog=fog}) + self:T({dust=dust}) + self:T({static=static}) + return clouds, visibility, turbulence, fog, dust, static +end + + +--- Get thousands of a number. +-- @param #ATIS self +-- @param #number n Number, e.g. 4359. +-- @return #string Thousands of n, e.g. "4" for 4359. +-- @return #string Hundreds of n, e.g. "4" for 4359 because its rounded. +function ATIS:_GetThousandsAndHundreds(n) + + local N=UTILS.Round(n/1000, 1) + + local S=UTILS.Split(string.format("%.1f", N), ".") + + local t=S[1] + local h=S[2] + + return t, h +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 62f81945a..acb5748b6 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -432,12 +432,13 @@ UTILS.tostringLL = function( lat, lon, acc, DMS) if acc <= 0 then -- no decimal place. secFrmtStr = '%02d' else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. + local width = 3 + acc -- 01.310 - that's a width of 6, for example. Acc is limited to 2 for DMS! secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' end - return string.format('%03d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%03d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi + -- 024° 23' 12"N or 024° 23' 12.03"N + return string.format('%03d°', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' + .. string.format('%03d°', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi else -- degrees, decimal minutes. latMin = UTILS.Round(latMin, acc) @@ -461,8 +462,9 @@ UTILS.tostringLL = function( lat, lon, acc, DMS) minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' end - return string.format('%03d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%03d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi + -- 024 23'N or 024 23.123'N + return string.format('%03d°', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' + .. string.format('%03d°', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi end end @@ -657,8 +659,9 @@ end --- Convert time in seconds to hours, minutes and seconds. -- @param #number seconds Time in seconds, e.g. from timer.getAbsTime() function. +-- @param #boolean short (Optional) If true, use short output, i.e. (HH:)MM:SS without day. -- @return #string Time in format Hours:Minutes:Seconds+Days (HH:MM:SS+D). -function UTILS.SecondsToClock(seconds) +function UTILS.SecondsToClock(seconds, short) -- Nil check. if seconds==nil then @@ -678,7 +681,15 @@ function UTILS.SecondsToClock(seconds) local mins = string.format("%02.f", math.floor(_seconds/60 - (hours*60))) local secs = string.format("%02.f", math.floor(_seconds - hours*3600 - mins *60)) local days = string.format("%d", seconds/(60*60*24)) - return hours..":"..mins..":"..secs.."+"..days + local clock=hours..":"..mins..":"..secs.."+"..days + if short then + if hours=="00" then + clock=mins..":"..secs + else + clock=hours..":"..mins..":"..secs + end + end + return clock end end @@ -983,3 +994,16 @@ function UTILS.FileExists(file) return nil end end + +--- Checks the current memory usage collectgarbage("count"). Info is printed to the DCS log file. Time stamp is the current mission runtime. +-- @param #boolean output If true, print to DCS log file. +-- @return #number Memory usage in kByte. +function UTILS.CheckMemory(output) + local time=timer.getTime() + local clock=UTILS.SecondsToClock(time) + local mem=collectgarbage("count") + if output then + env.info(string.format("T=%s Memory usage %d kByte = %.2f MByte", clock, mem, mem/1024)) + end + return mem +end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index bce6e4feb..b5d773b98 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1,13 +1,13 @@ --- **Wrapper** -- CONTROLLABLE is an intermediate class wrapping Group and Unit classes "controllers". --- +-- -- === --- +-- -- ### Author: **FlightControl** --- --- ### Contributions: --- +-- +-- ### Contributions: +-- -- === --- +-- -- @module Wrapper.Controllable -- @image Wrapper_Controllable.JPG @@ -27,26 +27,26 @@ -- * Manage the "state" of the DCS Controllable. -- -- # 1) CONTROLLABLE constructor --- +-- -- The CONTROLLABLE class provides the following functions to construct a CONTROLLABLE instance: -- -- * @{#CONTROLLABLE.New}(): Create a CONTROLLABLE instance. -- -- # 2) CONTROLLABLE Task methods --- --- Several controllable task methods are available that help you to prepare tasks. +-- +-- Several controllable task methods are available that help you to prepare tasks. -- These methods return a string consisting of the task description, which can then be given to either a @{Wrapper.Controllable#CONTROLLABLE.PushTask} or @{Wrapper.Controllable#SetTask} method to assign the task to the CONTROLLABLE. --- Tasks are specific for the category of the CONTROLLABLE, more specific, for AIR, GROUND or AIR and GROUND. +-- Tasks are specific for the category of the CONTROLLABLE, more specific, for AIR, GROUND or AIR and GROUND. -- Each task description where applicable indicates for which controllable category the task is valid. -- There are 2 main subdivisions of tasks: Assigned tasks and EnRoute tasks. --- +-- -- ## 2.1) Task assignment --- +-- -- Assigned task methods make the controllable execute the task where the location of the (possible) targets of the task are known before being detected. -- This is different from the EnRoute tasks, where the targets of the task need to be detected before the task can be executed. --- +-- -- Find below a list of the **assigned task** methods: --- +-- -- * @{#CONTROLLABLE.TaskAttackGroup}: (AIR) Attack a Controllable. -- * @{#CONTROLLABLE.TaskAttackMapObject}: (AIR) Attacking the map object (building, structure, e.t.c). -- * @{#CONTROLLABLE.TaskAttackUnit}: (AIR) Attack the Unit. @@ -54,7 +54,7 @@ -- * @{#CONTROLLABLE.TaskBombingRunway}: (AIR) Delivering weapon on the runway. -- * @{#CONTROLLABLE.TaskEmbarking}: (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. -- * @{#CONTROLLABLE.TaskEmbarkToTransport}: (GROUND) Embark to a Transport landed at a location. --- * @{#CONTROLLABLE.TaskEscort}: (AIR) Escort another airborne controllable. +-- * @{#CONTROLLABLE.TaskEscort}: (AIR) Escort another airborne controllable. -- * @{#CONTROLLABLE.TaskFAC_AttackGroup}: (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. -- * @{#CONTROLLABLE.TaskFireAtPoint}: (GROUND) Fire some or all ammunition at a VEC2 point. -- * @{#CONTROLLABLE.TaskFollow}: (AIR) Following another airborne controllable. @@ -72,9 +72,9 @@ -- * @{#CONTROLLABLE.TaskReturnToBase}: (AIR) Route the controllable to an airbase. -- -- ## 2.2) EnRoute assignment --- +-- -- EnRoute tasks require the targets of the task need to be detected by the controllable (using its sensors) before the task can be executed: --- +-- -- * @{#CONTROLLABLE.EnRouteTaskAWACS}: (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. -- * @{#CONTROLLABLE.EnRouteTaskEngageControllable}: (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. -- * @{#CONTROLLABLE.EnRouteTaskEngageTargets}: (AIR) Engaging targets of defined types. @@ -83,96 +83,96 @@ -- * @{#CONTROLLABLE.EnRouteTaskFAC}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. -- * @{#CONTROLLABLE.EnRouteTaskFAC_EngageControllable}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. -- * @{#CONTROLLABLE.EnRouteTaskTanker}: (AIR) Aircraft will act as a tanker for friendly units. No parameters. --- +-- -- ## 2.3) Task preparation --- +-- -- There are certain task methods that allow to tailor the task behaviour: -- -- * @{#CONTROLLABLE.TaskWrappedAction}: Return a WrappedAction Task taking a Command. -- * @{#CONTROLLABLE.TaskCombo}: Return a Combo Task taking an array of Tasks. -- * @{#CONTROLLABLE.TaskCondition}: Return a condition section for a controlled task. -- * @{#CONTROLLABLE.TaskControlled}: Return a Controlled Task taking a Task and a TaskCondition. --- +-- -- ## 2.4) Call a function as a Task --- +-- -- A function can be called which is part of a Task. The method @{#CONTROLLABLE.TaskFunction}() prepares -- a Task that can call a GLOBAL function from within the Controller execution. -- This method can also be used to **embed a function call when a certain waypoint has been reached**. -- See below the **Tasks at Waypoints** section. --- +-- -- Demonstration Mission: [GRP-502 - Route at waypoint to random point](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/GRP - Group Commands/GRP-502 - Route at waypoint to random point) --- +-- -- ## 2.5) Tasks at Waypoints --- +-- -- Special Task methods are available to set tasks at certain waypoints. -- The method @{#CONTROLLABLE.SetTaskWaypoint}() helps preparing a Route, embedding a Task at the Waypoint of the Route. --- +-- -- This creates a Task element, with an action to call a function as part of a Wrapped Task. --- +-- -- ## 2.6) Obtain the mission from controllable templates --- +-- -- Controllable templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a controllable and assign it to another: --- +-- -- * @{#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. -- -- # 3) Command methods --- +-- -- Controllable **command methods** prepare the execution of commands using the @{#CONTROLLABLE.SetCommand} method: --- +-- -- * @{#CONTROLLABLE.CommandDoScript}: Do Script command. -- * @{#CONTROLLABLE.CommandSwitchWayPoint}: Perform a switch waypoint command. --- +-- -- # 4) Routing of Controllables --- +-- -- Different routing methods exist to route GROUPs and UNITs to different locations: --- --- * @{#CONTROLLABLE.Route}(): Make the Controllable to follow a given route. +-- +-- * @{#CONTROLLABLE.Route}(): Make the Controllable to follow a given route. -- * @{#CONTROLLABLE.RouteGroundTo}(): Make the GROUND Controllable to drive towards a specific coordinate. --- * @{#CONTROLLABLE.RouteAirTo}(): Make the AIR Controllable to fly towards a specific coordinate. --- +-- * @{#CONTROLLABLE.RouteAirTo}(): Make the AIR Controllable to fly towards a specific coordinate. +-- -- # 5) Option methods --- +-- -- Controllable **Option methods** change the behaviour of the Controllable while being alive. --- +-- -- ## 5.1) Rule of Engagement: --- --- * @{#CONTROLLABLE.OptionROEWeaponFree} +-- +-- * @{#CONTROLLABLE.OptionROEWeaponFree} -- * @{#CONTROLLABLE.OptionROEOpenFire} -- * @{#CONTROLLABLE.OptionROEReturnFire} -- * @{#CONTROLLABLE.OptionROEEvadeFire} --- +-- -- To check whether an ROE option is valid for a specific controllable, use: --- --- * @{#CONTROLLABLE.OptionROEWeaponFreePossible} +-- +-- * @{#CONTROLLABLE.OptionROEWeaponFreePossible} -- * @{#CONTROLLABLE.OptionROEOpenFirePossible} -- * @{#CONTROLLABLE.OptionROEReturnFirePossible} -- * @{#CONTROLLABLE.OptionROEEvadeFirePossible} --- +-- -- ## 5.2) Reaction On Thread: --- +-- -- * @{#CONTROLLABLE.OptionROTNoReaction} -- * @{#CONTROLLABLE.OptionROTPassiveDefense} -- * @{#CONTROLLABLE.OptionROTEvadeFire} -- * @{#CONTROLLABLE.OptionROTVertical} --- +-- -- To test whether an ROT option is valid for a specific controllable, use: --- +-- -- * @{#CONTROLLABLE.OptionROTNoReactionPossible} -- * @{#CONTROLLABLE.OptionROTPassiveDefensePossible} -- * @{#CONTROLLABLE.OptionROTEvadeFirePossible} -- * @{#CONTROLLABLE.OptionROTVerticalPossible} --- +-- -- ## 5.3) Alarm state: --- +-- -- * @{#CONTROLLABLE.OptionAlarmStateAuto} -- * @{#CONTROLLABLE.OptionAlarmStateGreen} -- * @{#CONTROLLABLE.OptionAlarmStateRed} --- +-- -- ## 5.4) Jettison weapons: --- +-- -- * @{#CONTROLLABLE.OptionAllowJettisonWeaponsOnThreat} -- * @{#CONTROLLABLE.OptionKeepWeaponsOnThreat} --- +-- -- @field #CONTROLLABLE CONTROLLABLE = { ClassName = "CONTROLLABLE", @@ -188,7 +188,7 @@ function CONTROLLABLE:New( ControllableName ) local self = BASE:Inherit( self, POSITIONABLE:New( ControllableName ) ) -- #CONTROLLABLE --self:F( ControllableName ) self.ControllableName = ControllableName - + self.TaskScheduler = SCHEDULER:New( self ) return self end @@ -215,12 +215,12 @@ end --- Returns the health. Dead controllables have health <= 1.0. -- @param #CONTROLLABLE self -- @return #number The controllable health value (unit or group average). --- @return #nil The controllable is not existing or alive. +-- @return #nil The controllable is not existing or alive. function CONTROLLABLE:GetLife() self:F2( self.ControllableName ) local DCSControllable = self:GetDCSObject() - + if DCSControllable then local UnitLife = 0 local Units = self:GetUnits() @@ -237,19 +237,19 @@ function CONTROLLABLE:GetLife() end return UnitLife end - + return nil end --- Returns the initial health. -- @param #CONTROLLABLE self -- @return #number The controllable health value (unit or group average). --- @return #nil The controllable is not existing or alive. +-- @return #nil The controllable is not existing or alive. function CONTROLLABLE:GetLife0() self:F2( self.ControllableName ) local DCSControllable = self:GetDCSObject() - + if DCSControllable then local UnitLife = 0 local Units = self:GetUnits() @@ -266,14 +266,14 @@ function CONTROLLABLE:GetLife0() end return UnitLife end - + return nil end --- Returns relative minimum amount of fuel (from 0.0 to 1.0) a unit or group has in its internal tanks. -- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. -- @param #CONTROLLABLE self --- @return #nil The CONTROLLABLE is not existing or alive. +-- @return #nil The CONTROLLABLE is not existing or alive. function CONTROLLABLE:GetFuelMin() self:F( self.ControllableName ) @@ -283,7 +283,7 @@ end --- Returns relative average amount of fuel (from 0.0 to 1.0) a unit or group has in its internal tanks. -- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. -- @param #CONTROLLABLE self --- @return #nil The CONTROLLABLE is not existing or alive. +-- @return #nil The CONTROLLABLE is not existing or alive. function CONTROLLABLE:GetFuelAve() self:F( self.ControllableName ) @@ -293,7 +293,7 @@ end --- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. -- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. -- @param #CONTROLLABLE self --- @return #nil The CONTROLLABLE is not existing or alive. +-- @return #nil The CONTROLLABLE is not existing or alive. function CONTROLLABLE:GetFuel() self:F( self.ControllableName ) @@ -346,13 +346,13 @@ function CONTROLLABLE:PushTask( DCSTask, WaitTime ) local DCSControllable = self:GetDCSObject() if DCSControllable then - + 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() @@ -387,7 +387,7 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) if DCSControllable then local DCSControllableName = self:GetName() - + 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. @@ -399,7 +399,7 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) local Controller = self:_GetController() --self:I( "Before SetTask" ) Controller:setTask( DCSTask ) - -- AI_FORMATION class (used by RESCUEHELO) calls SetTask twice per second! hence spamming the DCS log file ==> setting this to trace. + -- 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 } ) @@ -441,24 +441,24 @@ end --- Return a condition section for a controlled task. -- @param #CONTROLLABLE self --- @param DCS#Time time --- @param #string userFlag --- @param #boolean userFlagValue --- @param #string condition --- @param DCS#Time duration --- @param #number lastWayPoint +-- @param DCS#Time time DCS mission time. +-- @param #string userFlag Name of the user flag. +-- @param #boolean userFlagValue User flag value *true* or *false*. Could also be numeric, i.e. either 0=*false* or 1=*true*. Other numeric values don't work! +-- @param #string condition Lua string. +-- @param DCS#Time duration Duration in seconds. +-- @param #number lastWayPoint Last waypoint. -- return DCS#Task function CONTROLLABLE:TaskCondition( time, userFlag, userFlagValue, condition, duration, lastWayPoint ) self:F2( { time, userFlag, userFlagValue, condition, duration, lastWayPoint } ) --[[ - StopCondition = { - time = Time, - userFlag = string, - userFlagValue = boolean, - condition = string, - duration = Time, - lastWaypoint = number, + StopCondition = { + time = Time, + userFlag = string, + userFlagValue = boolean, + condition = string, + duration = Time, + lastWaypoint = number, } --]] @@ -511,7 +511,7 @@ function CONTROLLABLE:TaskCombo( DCSTasks ) tasks = DCSTasks } } - + for TaskID, Task in ipairs( DCSTasks ) do self:T( Task ) end @@ -528,7 +528,7 @@ function CONTROLLABLE:TaskWrappedAction( DCSCommand, Index ) self:F2( { DCSCommand } ) local DCSTaskWrappedAction - + DCSTaskWrappedAction = { id = "WrappedAction", enabled = true, @@ -585,14 +585,14 @@ end -- @usage -- --- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. -- HeliGroup = GROUP:FindByName( "Helicopter" ) --- +-- -- --- Route the helicopter back to the FARP after 60 seconds. -- -- We use the SCHEDULER class to do this. -- SCHEDULER:New( nil, -- function( HeliGroup ) -- local CommandRTB = HeliGroup:CommandSwitchWayPoint( 2, 8 ) -- HeliGroup:SetCommand( CommandRTB ) --- end, { HeliGroup }, 90 +-- end, { HeliGroup }, 90 -- ) function CONTROLLABLE:CommandSwitchWayPoint( FromWayPoint, ToWayPoint ) self:F2( { FromWayPoint, ToWayPoint } ) @@ -613,11 +613,11 @@ end -- Use the result in the method @{#CONTROLLABLE.SetCommand}(). -- A value of true will make the ground group stop, a value of false will make it continue. -- Note that this can only work on GROUP level, although individual UNITs can be commanded, the whole GROUP will react. --- --- Example missions: --- +-- +-- Example missions: +-- -- * GRP-310 --- +-- -- @param #CONTROLLABLE self -- @param #boolean StopRoute true if the ground unit needs to stop, false if it needs to continue to move. -- @return DCS#Task @@ -642,7 +642,7 @@ end -- @return #CONTROLLABLE self function CONTROLLABLE:StartUncontrolled(delay) if delay and delay>0 then - SCHEDULER:New(nil, CONTROLLABLE.StartUncontrolled, {self}, delay) + SCHEDULER:New(nil, CONTROLLABLE.StartUncontrolled, {self}, delay) else self:SetCommand({id='Start', params={}}) end @@ -668,13 +668,13 @@ function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Cha AA=AA or self:IsAir() UnitID=UnitID or self:GetID() - + -- Command local CommandActivateBeacon= { id = "ActivateBeacon", params = { ["type"] = Type, - ["system"] = System, + ["system"] = System, ["frequency"] = Frequency, ["unitId"] = UnitID, ["channel"] = Channel, @@ -684,13 +684,13 @@ function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Cha ["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 + SCHEDULER:New(nil, self.CommandActivateBeacon, {self, Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing}, Delay) + else self:SetCommand(CommandActivateBeacon) end - + return self end @@ -710,14 +710,14 @@ function CONTROLLABLE:CommandActivateICLS(Channel, UnitID, Callsign, Delay) params= { ["type"] = BEACON.Type.ICLS, ["channel"] = Channel, - ["unitId"] = UnitID, + ["unitId"] = UnitID, ["callsign"] = Callsign, } } if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandActivateICLS, {self}, Delay) - else + SCHEDULER:New(nil, self.CommandActivateICLS, {self}, Delay) + else self:SetCommand(CommandActivateICLS) end @@ -731,13 +731,13 @@ end -- @return #CONTROLLABLE self function CONTROLLABLE:CommandDeactivateBeacon(Delay) self:F() - - -- Command to deactivate + + -- Command to deactivate local CommandDeactivateBeacon={id='DeactivateBeacon', params={}} if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandActivateBeacon, {self}, Delay) - else + SCHEDULER:New(nil, self.CommandActivateBeacon, {self}, Delay) + else self:SetCommand(CommandDeactivateBeacon) end @@ -750,13 +750,13 @@ end -- @return #CONTROLLABLE self function CONTROLLABLE:CommandDeactivateICLS(Delay) self:F() - - -- Command to deactivate + + -- Command to deactivate local CommandDeactivateICLS={id='DeactivateICLS', params={}} if Delay and Delay>0 then - SCHEDULER:New(nil, self.CommandDeactivateICLS, {self}, Delay) - else + SCHEDULER:New(nil, self.CommandDeactivateICLS, {self}, Delay) + else self:SetCommand(CommandDeactivateICLS) end @@ -771,13 +771,13 @@ end -- @return #CONTROLLABLE self function CONTROLLABLE:CommandSetCallsign(CallName, CallNumber, Delay) self:F() - - -- Command to set the callsign. + + -- 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 + else self:SetCommand(CommandSetCallsign) end @@ -791,15 +791,15 @@ end -- @return #CONTROLLABLE self function CONTROLLABLE:CommandEPLRS(SwitchOnOff, Delay) self:F() - + if SwitchOnOff==nil then SwitchOnOff=true end - + -- ID local _id=self:GetID() - - -- Command to set the callsign. + + -- Command to set the callsign. local CommandEPLRS={id='EPLRS', params={value=SwitchOnOff, groupId=_id}} if Delay and Delay>0 then @@ -821,16 +821,17 @@ function CONTROLLABLE:TaskEPLRS(SwitchOnOff, idx) -- ID local _id=self:GetID() - - -- Command to set the callsign. + + -- Command to set the callsign. local CommandEPLRS={id='EPLRS', params={value=SwitchOnOff, groupId=_id}} return self:TaskWrappedAction(CommandEPLRS, idx or 1) - + end -- TASKS FOR AIR CONTROLLABLES + --- (AIR) Attack a Controllable. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. @@ -862,11 +863,17 @@ function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, At local DirectionEnabled = nil if Direction then DirectionEnabled = true + else + DirectionEnabled = false + Direction=0 end local AltitudeEnabled = nil if Altitude then AltitudeEnabled = true + else + AltitudeEnabled = false + Altitude=0 end local DCSTask @@ -895,14 +902,14 @@ end -- @param DCS#AI.Task.WeaponExpend WeaponExpend (Optional) Determines how many weapons 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) Limits maximal quantity of attack. The aicraft/controllable will not make more attacks 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 DCS#Azimuth Direction (Optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. --- @param #number Altitude (Optional) The (minimum) altitude in meters from where to attack. Default 2000 m. +-- @param #number Altitude (Optional) The (minimum) altitude in meters from where to attack. Default is altitude of unit to attack but at least 1000 m. -- @param #number WeaponType (optional) The WeaponType. See [DCS Enumerator Weapon Type](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) on Hoggit. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType) self:F2({self.ControllableName, AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType}) local DCSTask - DCSTask = { + DCSTask = { id = 'AttackUnit', params = { unitId = AttackUnit:GetID(), @@ -911,7 +918,7 @@ function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, Atta directionEnabled = Direction and true or false, direction = math.rad(Direction or 0), altitudeEnabled = Altitude and true or false, - altitude = Altitude or 2000, + altitude = Altitude or math.max(1000, AttackUnit:GetAltitude()), attackQtyLimit = AttackQty and true or false, attackQty = AttackQty, weaponType = WeaponType @@ -919,12 +926,12 @@ function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, Atta } self:T3( DCSTask ) - + return DCSTask end ---- (AIR) Delivering weapon at the point on the ground. +--- (AIR) Delivering weapon at the point on the ground. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. -- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. @@ -937,34 +944,34 @@ end -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb ) self:F( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb } ) - + local _groupattack=false if GroupAttack then _groupattack=GroupAttack end - + local _direction=0 local _directionenabled=false if Direction then _direction=math.rad(Direction) _directionenabled=true end - + local _altitude=5000 local _altitudeenabled=false if Altitude then _altitude=Altitude _altitudeenabled=true end - + local _attacktype=nil if Divebomb then _attacktype="Dive" end - + local DCSTask - DCSTask = { + DCSTask = { id = 'Bombing', params = { x = Vec2.x, @@ -974,10 +981,10 @@ function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, D attackQtyLimit = false, --AttackQty and true or false, attackQty = AttackQty or 1, directionEnabled = _directionenabled, - direction = _direction, + direction = _direction, altitudeEnabled = _altitudeenabled, altitude = _altitude, - weaponType = WeaponType, + weaponType = WeaponType, --attackType=_attacktype, }, } @@ -986,33 +993,35 @@ function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, D return DCSTask end ---- (AIR) Attacking the map object (building, structure, e.t.c). +--- (AIR) Attacking the map object (building, structure, etc). -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. --- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @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 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 #number Altitude (optional) The altitude from where to attack. --- @param #number WeaponType (optional) The WeaponType. +-- @param #boolean GroupAttack (Optional) If true, all units in the group will attack the Unit when found. +-- @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 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 #number Altitude (Optional) The altitude [meters] from where to attack. Default 30 m. +-- @param #number WeaponType (Optional) The WeaponType. Default Auto=1073741822. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType ) self:F2( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType } ) local DCSTask - DCSTask = { + DCSTask = { id = 'AttackMapObject', params = { point = Vec2, + x = Vec2.x, + y = Vec2.y, groupAttack = GroupAttack or false, expend = WeaponExpend or "Auto", attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, + attackQty = AttackQty, directionEnabled = Direction and true or false, - direction = Direction, + direction = Direction, altitudeEnabled = Altitude and true or false, altitude = Altitude or 30, - weaponType = WeaponType, + weaponType = WeaponType or 1073741822, }, }, @@ -1069,14 +1078,14 @@ 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. +-- @param #number Altitude Altitude in meters of the orbit pattern. Default y component of Coord. +-- @param #number Speed Speed [m/s] flying the orbit pattern. Default 128 m/s = 250 knots. +-- @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 @@ -1090,8 +1099,8 @@ function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) pattern = Pattern, point = P1, point2 = P2, - speed = Speed, - altitude = Altitude, + speed = Speed or UTILS.KnotsToMps(250), + altitude = Altitude or Coord.y, } } @@ -1135,16 +1144,16 @@ end --- (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. See [DCS enum weapon flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag). Default 2147485694 = AnyBomb (GuidedBomb + AnyUnguidedBomb). @@ -1156,16 +1165,16 @@ end function CONTROLLABLE:TaskBombingRunway(Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack) self:F2( { self.ControllableName, Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack } ) --- BombingRunway = { --- id = 'BombingRunway', --- params = { +-- BombingRunway = { +-- id = 'BombingRunway', +-- params = { -- runwayId = AirdromeId, --- weaponType = number, +-- weaponType = number, -- expend = enum AI.Task.WeaponExpend, --- attackQty = number, --- direction = Azimuth, --- groupAttack = boolean, --- } +-- attackQty = number, +-- direction = Azimuth, +-- groupAttack = boolean, +-- } -- } -- Defaults. @@ -1177,11 +1186,11 @@ function CONTROLLABLE:TaskBombingRunway(Airbase, WeaponType, WeaponExpend, Attac DCSTask = { id = 'BombingRunway', params = { runwayId = Airbase:GetID(), - weaponType = WeaponType, + weaponType = WeaponType, expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - groupAttack = GroupAttack, + attackQty = AttackQty, + direction = Direction, + groupAttack = GroupAttack, }, }, @@ -1196,9 +1205,9 @@ end function CONTROLLABLE:TaskRefueling() self:F2( { self.ControllableName } ) --- Refueling = { --- id = 'Refueling', --- params = {} +-- Refueling = { +-- id = 'Refueling', +-- params = {} -- } local DCSTask={id='Refueling', params={}} @@ -1224,22 +1233,22 @@ function CONTROLLABLE:TaskLandAtVec2( Point, Duration ) -- duration = Time -- } -- } - + local DCSTask if Duration and Duration > 0 then - DCSTask = { id = 'Land', - params = { - point = Point, - durationFlag = true, + DCSTask = { id = 'Land', + params = { + point = Point, + durationFlag = true, duration = Duration, - }, + }, } else - DCSTask = { id = 'Land', - params = { - point = Point, - durationFlag = false, - }, + DCSTask = { id = 'Land', + params = { + point = Point, + durationFlag = false, + }, } end @@ -1270,9 +1279,9 @@ end ---- (AIR) Following another airborne controllable. --- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. --- If another controllable is on land the unit / controllable will orbit around. +--- (AIR) Following another airborne controllable. +-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. +-- If another controllable is on land the unit / controllable will orbit around. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be followed. -- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. @@ -1288,22 +1297,25 @@ function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) -- pos = Vec3, -- lastWptIndexFlag = boolean, -- lastWptIndex = number --- } +-- } -- } local LastWaypointIndexFlag = false + local lastWptIndexFlagChangedManually = false if LastWaypointIndex then LastWaypointIndexFlag = true + lastWptIndexFlagChangedManually = true end - + local DCSTask - DCSTask = { + DCSTask = { id = 'Follow', params = { groupId = FollowControllable:GetID(), pos = Vec3, lastWptIndexFlag = LastWaypointIndexFlag, - lastWptIndex = LastWaypointIndex + lastWptIndex = LastWaypointIndex, + lastWptIndexFlagChangedManually = lastWptIndexFlagChangedManually, } } @@ -1312,15 +1324,15 @@ function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) end ---- (AIR) Escort another airborne controllable. --- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. +--- (AIR) Escort another airborne controllable. +-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. -- The unit / controllable will also protect that controllable from threats of specified types. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be escorted. -- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. -- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. --- @param #number EngagementDistance Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. --- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. +-- @param #number EngagementDistance Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. +-- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes } ) @@ -1334,16 +1346,16 @@ function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, E -- lastWptIndex = number, -- engagementDistMax = Distance, -- targetTypes = array of AttributeName, --- } +-- } -- } local LastWaypointIndexFlag = false if LastWaypointIndex then LastWaypointIndexFlag = true end - + TargetTypes=TargetTypes or {} - + local DCSTask DCSTask = { id = 'Escort', params = { @@ -1368,7 +1380,7 @@ end -- @param DCS#Vec2 Vec2 The point to fire at. -- @param DCS#Distance Radius The radius of the zone to deploy the fire at. -- @param #number AmmoCount (optional) Quantity of ammunition to expand (omit to fire until ammunition is depleted). --- @param #number WeaponType (optional) Enum for weapon type ID. This value is only required if you want the group firing to use a specific weapon, for instance using the task on a ship to force it to fire guided missiles at targets within cannon range. See http://wiki.hoggit.us/view/DCS_enum_weapon_flag +-- @param #number WeaponType (optional) Enum for weapon type ID. This value is only required if you want the group firing to use a specific weapon, for instance using the task on a ship to force it to fire guided missiles at targets within cannon range. See http://wiki.hoggit.us/view/DCS_enum_weapon_flag -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType ) self:F2( { self.ControllableName, Vec2, Radius, AmmoCount, WeaponType } ) @@ -1379,12 +1391,13 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType ) -- point = Vec2, -- radius = Distance, -- expendQty = number, - -- expendQtyEnabled = boolean, + -- expendQtyEnabled = boolean, -- } -- } local DCSTask - DCSTask = { id = 'FireAtPoint', + DCSTask = { + id = 'FireAtPoint', params = { point = Vec2, zoneRadius = Radius, @@ -1392,12 +1405,12 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType ) expendQtyEnabled = false, } } - + if AmmoCount then DCSTask.params.expendQty = AmmoCount DCSTask.params.expendQtyEnabled = true end - + if WeaponType then DCSTask.params.weaponType=WeaponType end @@ -1412,10 +1425,10 @@ end function CONTROLLABLE:TaskHold() self:F2( { self.ControllableName } ) --- Hold = { --- id = 'Hold', --- params = { --- } +-- Hold = { +-- id = 'Hold', +-- params = { +-- } -- } local DCSTask @@ -1431,26 +1444,26 @@ end -- TASKS FOR AIRBORNE AND GROUND UNITS/CONTROLLABLES ---- (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. +--- (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. -- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. +-- If the task is assigned to the controllable lead unit will be a FAC. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. -- @param DCS#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink ) self:F2( { self.ControllableName, AttackGroup, WeaponType, Designation, Datalink } ) --- FAC_AttackGroup = { --- id = 'FAC_AttackGroup', --- params = { +-- FAC_AttackGroup = { +-- id = 'FAC_AttackGroup', +-- params = { -- groupId = Group.ID, -- weaponType = number, -- designation = enum AI.Task.Designation, -- datalink = boolean --- } +-- } -- } local DCSTask @@ -1471,28 +1484,28 @@ end --- (AIR) Engaging targets of defined types. -- @param #CONTROLLABLE self --- @param DCS#Distance Distance Maximal distance from the target to a route leg. If the target is on a greater distance it will be ignored. --- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. --- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param DCS#Distance Distance Maximal distance from the target to a route leg. If the target is on a greater distance it will be ignored. +-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. +-- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority ) self:F2( { self.ControllableName, Distance, TargetTypes, Priority } ) --- EngageTargets ={ --- id = 'EngageTargets', --- params = { --- maxDist = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } +-- EngageTargets ={ +-- id = 'EngageTargets', +-- params = { +-- maxDist = Distance, +-- targetTypes = array of AttributeName, +-- priority = number +-- } -- } local DCSTask DCSTask = { id = 'EngageTargets', params = { - maxDist = Distance, - targetTypes = TargetTypes, - priority = Priority + maxDist = Distance, + targetTypes = TargetTypes, + priority = Priority } } @@ -1504,31 +1517,31 @@ end --- (AIR) Engaging a targets of defined types at circle-shaped zone. -- @param #CONTROLLABLE self --- @param DCS#Vec2 Vec2 2D-coordinates of the zone. --- @param DCS#Distance Radius Radius of the zone. --- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param DCS#Vec2 Vec2 2D-coordinates of the zone. +-- @param DCS#Distance Radius Radius of the zone. +-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, Priority ) self:F2( { self.ControllableName, Vec2, Radius, TargetTypes, Priority } ) --- EngageTargetsInZone = { --- id = 'EngageTargetsInZone', --- params = { --- point = Vec2, --- zoneRadius = Distance, --- targetTypes = array of AttributeName, --- priority = number +-- EngageTargetsInZone = { +-- id = 'EngageTargetsInZone', +-- params = { +-- point = Vec2, +-- zoneRadius = Distance, +-- targetTypes = array of AttributeName, +-- priority = number -- } -- } local DCSTask DCSTask = { id = 'EngageTargetsInZone', params = { - point = Vec2, - zoneRadius = Radius, - targetTypes = TargetTypes, - priority = Priority + point = Vec2, + zoneRadius = Radius, + targetTypes = TargetTypes, + priority = Priority } } @@ -1540,7 +1553,7 @@ end --- (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. -- @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. @@ -1601,7 +1614,7 @@ end --- (AIR) Search and attack the Unit. -- @param #CONTROLLABLE self -- @param Wrapper.Unit#UNIT EngageUnit The UNIT. --- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. -- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. -- @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. @@ -1657,10 +1670,10 @@ end function CONTROLLABLE:EnRouteTaskAWACS( ) self:F2( { self.ControllableName } ) --- AWACS = { --- id = 'AWACS', --- params = { --- } +-- AWACS = { +-- id = 'AWACS', +-- params = { +-- } -- } local DCSTask @@ -1680,10 +1693,10 @@ end function CONTROLLABLE:EnRouteTaskTanker( ) self:F2( { self.ControllableName } ) --- Tanker = { --- id = 'Tanker', --- params = { --- } +-- Tanker = { +-- id = 'Tanker', +-- params = { +-- } -- } local DCSTask @@ -1705,10 +1718,10 @@ end function CONTROLLABLE:EnRouteTaskEWR( ) self:F2( { self.ControllableName } ) --- EWR = { --- id = 'EWR', --- params = { --- } +-- EWR = { +-- id = 'EWR', +-- params = { +-- } -- } local DCSTask @@ -1722,30 +1735,30 @@ function CONTROLLABLE:EnRouteTaskEWR( ) end --- En-route tasks for airborne and ground units/controllables +-- En-route tasks for airborne and ground units/controllables ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. +--- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. -- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. +-- If the task is assigned to the controllable lead unit will be a FAC. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. -- @param DCS#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink ) self:F2( { self.ControllableName, AttackGroup, WeaponType, Priority, Designation, Datalink } ) --- FAC_EngageControllable = { --- id = 'FAC_EngageControllable', --- params = { +-- FAC_EngageControllable = { +-- id = 'FAC_EngageControllable', +-- params = { -- groupId = Group.ID, -- weaponType = number, -- designation = enum AI.Task.Designation, -- datalink = boolean, -- priority = number, --- } +-- } -- } local DCSTask @@ -1764,22 +1777,22 @@ function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponT end ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. +--- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. -- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. +-- If the task is assigned to the controllable lead unit will be a FAC. -- @param #CONTROLLABLE self -- @param DCS#Distance Radius The maximal distance from the FAC to a target. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) self:F2( { self.ControllableName, Radius, Priority } ) --- FAC = { --- id = 'FAC', --- params = { +-- FAC = { +-- id = 'FAC', +-- params = { -- radius = Distance, -- priority = number --- } +-- } -- } local DCSTask @@ -1797,7 +1810,7 @@ end --[[ ---- Used in conjunction with the embarking task for a transport helicopter group. The Ground units will move to the specified location and wait to be picked up by a helicopter. +--- Used in conjunction with the embarking task for a transport helicopter group. The Ground units will move to the specified location and wait to be picked up by a helicopter. -- The helicopter will then fly them to their dropoff point defined by another task for the ground forces; DisembarkFromTransport task. -- The controllable has to be an infantry group! -- @param #CONTROLLABLE self @@ -1820,13 +1833,13 @@ function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius) return EmbarkToTransport end ---- Used in conjunction with the EmbarkToTransport task for a ground infantry group, the controlled helicopter flight will land at the specified coordinates, +--- Used in conjunction with the EmbarkToTransport task for a ground infantry group, the controlled helicopter flight will land at the specified coordinates, -- pick up boarding troops and transport them to that groups DisembarkFromTransport task. -- The CONTROLLABLE has to be a helicopter group! -- @param #CONTROLLABLE self -- @param Core.Set#SET_GROUP GroupSet Set of groups to be embarked by the controllable. -- @param Core.Point#COORDINATE Coordinate Coordinate of embarking. --- @param #number Duration Duration of embarking in seconds. +-- @param #number Duration Duration of embarking in seconds. -- @return #table Embarking task. function CONTROLLABLE:TaskEmbarking(GroupSet, Coordinate, Duration) @@ -1836,14 +1849,14 @@ function CONTROLLABLE:TaskEmbarking(GroupSet, Coordinate, Duration) local group=_group --Wrapper.Group#GROUP table.insert(gids, group:GetID()) end - + -- Group ID of controllable. local id=self:GetID() - + -- Distribution local distribution={} distribution[id]=gids - + local durationFlag=false if Duration then durationFlag=true @@ -1865,7 +1878,7 @@ function CONTROLLABLE:TaskEmbarking(GroupSet, Coordinate, Duration) y=Coordinate.z, } } - + self:E(DCStask) return DCStask @@ -1945,47 +1958,47 @@ end -- @param ... The variable arguments passed to the function when called! These arguments can be of any type! -- @return #CONTROLLABLE -- @usage --- --- local ZoneList = { --- ZONE:New( "ZONE1" ), --- ZONE:New( "ZONE2" ), --- ZONE:New( "ZONE3" ), --- ZONE:New( "ZONE4" ), --- ZONE:New( "ZONE5" ) +-- +-- local ZoneList = { +-- ZONE:New( "ZONE1" ), +-- ZONE:New( "ZONE2" ), +-- ZONE:New( "ZONE3" ), +-- ZONE:New( "ZONE4" ), +-- ZONE:New( "ZONE5" ) -- } --- +-- -- GroundGroup = GROUP:FindByName( "Vehicle" ) --- +-- -- --- @param Wrapper.Group#GROUP GroundGroup -- function RouteToZone( Vehicle, ZoneRoute ) --- +-- -- local Route = {} --- +-- -- Vehicle:E( { ZoneRoute = ZoneRoute } ) --- +-- -- Vehicle:MessageToAll( "Moving to zone " .. ZoneRoute:GetName(), 10 ) --- +-- -- -- Get the current coordinate of the Vehicle -- local FromCoord = Vehicle:GetCoordinate() --- +-- -- -- Select a random Zone and get the Coordinate of the new Zone. -- local RandomZone = ZoneList[ math.random( 1, #ZoneList ) ] -- Core.Zone#ZONE -- local ToCoord = RandomZone:GetCoordinate() --- +-- -- -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task -- Route[#Route+1] = FromCoord:WaypointGround( 72 ) -- Route[#Route+1] = ToCoord:WaypointGround( 60, "Vee" ) --- +-- -- local TaskRouteToZone = Vehicle:TaskFunction( "RouteToZone", RandomZone ) --- +-- -- Vehicle:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. --- +-- -- Vehicle:Route( Route, math.random( 10, 20 ) ) -- Move after a random seconds to the Route. See the Route method for details. --- +-- -- end --- +-- -- RouteToZone( GroundGroup, ZoneList[1] ) --- +-- function CONTROLLABLE:TaskFunction( FunctionString, ... ) local DCSTask @@ -2034,31 +2047,31 @@ do -- Patrol methods -- @param #CONTROLLABLE self -- @return #CONTROLLABLE function CONTROLLABLE:PatrolRoute() - + local PatrolGroup = self -- Wrapper.Group#GROUP - + if not self:IsInstanceOf( "GROUP" ) then PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP end - + self:F( { PatrolGroup = PatrolGroup:GetName() } ) - + if PatrolGroup:IsGround() or PatrolGroup:IsShip() then - + local Waypoints = PatrolGroup:GetTemplateRoutePoints() - + -- Calculate the new Route. local FromCoord = PatrolGroup:GetCoordinate() local From = FromCoord:WaypointGround( 120 ) - + table.insert( Waypoints, 1, From ) local TaskRoute = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRoute" ) - + self:F({Waypoints = Waypoints}) local Waypoint = Waypoints[#Waypoints] PatrolGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. - + PatrolGroup:Route( Waypoints ) -- Move after a random seconds to the Route. See the Route method for details. end end @@ -2071,31 +2084,31 @@ do -- Patrol methods -- @param Core.Point#COORDINATE ToWaypoint The waypoint where the group should move to. -- @return #CONTROLLABLE function CONTROLLABLE:PatrolRouteRandom( Speed, Formation, ToWaypoint ) - + local PatrolGroup = self -- Wrapper.Group#GROUP - + if not self:IsInstanceOf( "GROUP" ) then PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP end self:F( { PatrolGroup = PatrolGroup:GetName() } ) - + if PatrolGroup:IsGround() or PatrolGroup:IsShip() then - + local Waypoints = PatrolGroup:GetTemplateRoutePoints() - + -- Calculate the new Route. local FromCoord = PatrolGroup:GetCoordinate() local FromWaypoint = 1 if ToWaypoint then FromWaypoint = ToWaypoint end - + -- Loop until a waypoint has been found that is not the same as the current waypoint. -- Otherwise the object zon't move or drive in circles and the algorithm would not do exactly -- what it is supposed to do, which is making groups drive around. local ToWaypoint - repeat + repeat -- Select a random waypoint and check if it is not the same waypoint as where the object is about. ToWaypoint = math.random( 1, #Waypoints ) until( ToWaypoint ~= FromWaypoint ) @@ -2107,12 +2120,12 @@ do -- Patrol methods local Route = {} Route[#Route+1] = FromCoord:WaypointGround( 0 ) Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation ) - - + + local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRouteRandom", Speed, Formation, ToWaypoint ) - + PatrolGroup:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. - + PatrolGroup:Route( Route, 1 ) -- Move after a random seconds to the Route. See the Route method for details. end end @@ -2125,41 +2138,41 @@ do -- Patrol methods -- @param #string Formation (Optional) Formation the group should use. -- @return #CONTROLLABLE function CONTROLLABLE:PatrolZones( ZoneList, Speed, Formation ) - + if not type( ZoneList ) == "table" then ZoneList = { ZoneList } end - + local PatrolGroup = self -- Wrapper.Group#GROUP - + if not self:IsInstanceOf( "GROUP" ) then PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP end self:F( { PatrolGroup = PatrolGroup:GetName() } ) - + if PatrolGroup:IsGround() or PatrolGroup:IsShip() then - + local Waypoints = PatrolGroup:GetTemplateRoutePoints() local Waypoint = Waypoints[math.random( 1, #Waypoints )] -- Select random waypoint. - + -- Calculate the new Route. local FromCoord = PatrolGroup:GetCoordinate() - + -- Select a random Zone and get the Coordinate of the new Zone. local RandomZone = ZoneList[ math.random( 1, #ZoneList ) ] -- Core.Zone#ZONE local ToCoord = RandomZone:GetRandomCoordinate( 10 ) - + -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task local Route = {} Route[#Route+1] = FromCoord:WaypointGround( 20 ) Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation ) - - + + local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolZones", ZoneList, Speed, Formation ) - + PatrolGroup:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. - + PatrolGroup:Route( Route, 1 ) -- Move after a random seconds to the Route. See the Route method for details. end end @@ -2190,9 +2203,9 @@ do -- Route methods -- @return #CONTROLLABLE self function CONTROLLABLE:RouteToVec2( Point, Speed ) self:F2( { Point, Speed } ) - + local ControllablePoint = self:GetUnit( 1 ):GetVec2() - + local PointFrom = {} PointFrom.x = ControllablePoint.x PointFrom.y = ControllablePoint.y @@ -2207,8 +2220,8 @@ do -- Route methods ["vangle"] = 0, ["steer"] = 2, } - - + + local PointTo = {} PointTo.x = Point.x PointTo.y = Point.y @@ -2223,17 +2236,17 @@ do -- Route methods ["vangle"] = 0, ["steer"] = 2, } - - + + local Points = { PointFrom, PointTo } - + self:T3( Points ) - + self:Route( Points ) - + return self end - + --- (AIR + GROUND) Make the Controllable move to a given point. -- @param #CONTROLLABLE self -- @param DCS#Vec3 Point The destination point in Vec3 format. @@ -2241,9 +2254,9 @@ do -- Route methods -- @return #CONTROLLABLE self function CONTROLLABLE:RouteToVec3( Point, Speed ) self:F2( { Point, Speed } ) - + local ControllableVec3 = self:GetUnit( 1 ):GetVec3() - + local PointFrom = {} PointFrom.x = ControllableVec3.x PointFrom.y = ControllableVec3.z @@ -2260,8 +2273,8 @@ do -- Route methods ["vangle"] = 0, ["steer"] = 2, } - - + + local PointTo = {} PointTo.x = Point.x PointTo.y = Point.z @@ -2278,19 +2291,19 @@ do -- Route methods ["vangle"] = 0, ["steer"] = 2, } - - + + local Points = { PointFrom, PointTo } - + self:T3( Points ) - + self:Route( Points ) - + return self end - - - + + + --- Make the controllable to follow a given route. -- @param #CONTROLLABLE self -- @param #table Route A table of Route Points. @@ -2298,17 +2311,17 @@ do -- Route methods -- @return #CONTROLLABLE The CONTROLLABLE. function CONTROLLABLE:Route( Route, DelaySeconds ) self:F2( Route ) - + local DCSControllable = self:GetDCSObject() if DCSControllable then local RouteTask = self:TaskRoute( Route ) -- Create a RouteTask, that will route the CONTROLLABLE to the Route. self:SetTask( RouteTask, DelaySeconds or 1 ) -- Execute the RouteTask after the specified seconds (default is 1). return self end - + return nil end - + --- Make the controllable to push follow a given route. -- @param #CONTROLLABLE self -- @param #table Route A table of Route Points. @@ -2316,40 +2329,40 @@ do -- Route methods -- @return #CONTROLLABLE The CONTROLLABLE. function CONTROLLABLE:RoutePush( Route, DelaySeconds ) self:F2( Route ) - + local DCSControllable = self:GetDCSObject() if DCSControllable then local RouteTask = self:TaskRoute( Route ) -- Create a RouteTask, that will route the CONTROLLABLE to the Route. self:PushTask( RouteTask, DelaySeconds or 1 ) -- Execute the RouteTask after the specified seconds (default is 1). return self end - + return nil end - - + + --- Stops the movement of the vehicle on the route. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE function CONTROLLABLE:RouteStop() self:F(self:GetName() .. " RouteStop") - + local CommandStop = self:CommandStopRoute( true ) self:SetCommand( CommandStop ) - + end - + --- Resumes the movement of the vehicle on the route. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE function CONTROLLABLE:RouteResume() self:F( self:GetName() .. " RouteResume") - + local CommandResume = self:CommandStopRoute( false ) self:SetCommand( CommandResume ) - + end - + --- Make the GROUND Controllable to drive towards a specific point. -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. @@ -2358,17 +2371,17 @@ do -- Route methods -- @param #number DelaySeconds Wait for the specified seconds before executing the Route. -- @return #CONTROLLABLE The CONTROLLABLE. function CONTROLLABLE:RouteGroundTo( ToCoordinate, Speed, Formation, DelaySeconds ) - + local FromCoordinate = self:GetCoordinate() - + local FromWP = FromCoordinate:WaypointGround() local ToWP = ToCoordinate:WaypointGround( Speed, Formation ) - + self:Route( { FromWP, ToWP }, DelaySeconds ) - + return self end - + --- Make the GROUND Controllable to drive towards a specific point using (mostly) roads. -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. @@ -2377,21 +2390,21 @@ do -- Route methods -- @param #string OffRoadFormation (Optional) The formation at initial and final waypoint. Default is "Off Road". -- @return #CONTROLLABLE The CONTROLLABLE. function CONTROLLABLE:RouteGroundOnRoad( ToCoordinate, Speed, DelaySeconds, OffRoadFormation ) - + -- Defaults. Speed=Speed or 20 DelaySeconds=DelaySeconds or 1 OffRoadFormation=OffRoadFormation or "Off Road" - + -- Get the route task. local route=self:TaskGroundOnRoad(ToCoordinate, Speed, OffRoadFormation) - + -- Route controllable to destination. self:Route( route, DelaySeconds ) - + return self end - + --- Make the TRAIN Controllable to drive towards a specific point using railroads. -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. @@ -2399,22 +2412,22 @@ do -- Route methods -- @param #number DelaySeconds (Optional) Wait for the specified seconds before executing the Route. Default is one second. -- @return #CONTROLLABLE The CONTROLLABLE. function CONTROLLABLE:RouteGroundOnRailRoads( ToCoordinate, Speed, DelaySeconds) - + -- Defaults. Speed=Speed or 20 DelaySeconds=DelaySeconds or 1 - + -- Get the route task. local route=self:TaskGroundOnRailRoads(ToCoordinate, Speed) - + -- Route controllable to destination. self:Route( route, DelaySeconds ) - - return self - end - - + return self + end + + + --- Make a task for a GROUND Controllable to drive towards a specific point using (mostly) roads. -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. @@ -2426,35 +2439,35 @@ do -- Route methods -- @return #boolean If true, path on road is possible. If false, task will route the group directly to its destination. function CONTROLLABLE:TaskGroundOnRoad( ToCoordinate, Speed, OffRoadFormation, Shortcut, FromCoordinate ) self:F2({ToCoordinate=ToCoordinate, Speed=Speed, OffRoadFormation=OffRoadFormation}) - + -- Defaults. Speed=Speed or 20 OffRoadFormation=OffRoadFormation or "Off Road" - + -- Initial (current) coordinate. FromCoordinate = FromCoordinate or self:GetCoordinate() - + -- Get path and path length on road including the end points (From and To). local PathOnRoad, LengthOnRoad, GotPath =FromCoordinate:GetPathOnRoad(ToCoordinate, true) - + -- Get the length only(!) on the road. local _,LengthRoad=FromCoordinate:GetPathOnRoad(ToCoordinate, false) - -- Off road part of the rout: Total=OffRoad+OnRoad. + -- Off road part of the rout: Total=OffRoad+OnRoad. local LengthOffRoad local LongRoad - + -- Calculate the direct distance between the initial and final points. local LengthDirect=FromCoordinate:Get2DDistance(ToCoordinate) - + if GotPath and LengthRoad then - + -- Off road part of the rout: Total=OffRoad+OnRoad. LengthOffRoad=LengthOnRoad-LengthRoad -- Length on road is 10 times longer than direct route or path on road is very short (<5% of total path). LongRoad=LengthOnRoad and ((LengthOnRoad > LengthDirect*10) or (LengthRoad/LengthOnRoad*100<5)) - + -- Debug info. self:T(string.format("Length on road = %.3f km", LengthOnRoad/1000)) self:T(string.format("Length directly = %.3f km", LengthDirect/1000)) @@ -2462,13 +2475,13 @@ do -- Route methods self:T(string.format("Length only road = %.3f km", LengthRoad/1000)) self:T(string.format("Length off road = %.3f km", LengthOffRoad/1000)) self:T(string.format("Percent on road = %.1f", LengthRoad/LengthOnRoad*100)) - + end - + -- Route, ground waypoints along road. local route={} local canroad=false - + -- Check if a valid path on road could be found. if GotPath and LengthRoad 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. @@ -2477,14 +2490,14 @@ do -- Route methods -- Road is long ==> we take the short cut. table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) - + else -- Create waypoints. table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) table.insert(route, PathOnRoad[2]:WaypointGround(Speed, "On Road")) table.insert(route, PathOnRoad[#PathOnRoad-1]:WaypointGround(Speed, "On Road")) - + -- Add the final coordinate because the final might not be on the road. local dist=ToCoordinate:Get2DDistance(PathOnRoad[#PathOnRoad-1]) if dist>10 then @@ -2492,16 +2505,16 @@ do -- Route methods table.insert(route, ToCoordinate:GetRandomCoordinateInRadius(10,5):WaypointGround(5, OffRoadFormation)) table.insert(route, ToCoordinate:GetRandomCoordinateInRadius(10,5):WaypointGround(5, OffRoadFormation)) end - + end - + canroad=true else - + -- No path on road could be found (can happen!) ==> Route group directly from A to B. table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) - + end return route, canroad @@ -2514,31 +2527,31 @@ do -- Route methods -- @return Task function CONTROLLABLE:TaskGroundOnRailRoads(ToCoordinate, Speed) self:F2({ToCoordinate=ToCoordinate, Speed=Speed}) - + -- Defaults. Speed=Speed or 20 - + -- Current coordinate. local FromCoordinate = self:GetCoordinate() - + -- Get path and path length on railroad. local PathOnRail, LengthOnRail=FromCoordinate:GetPathOnRoad(ToCoordinate, false, true) - + -- Debug info. self:T(string.format("Length on railroad = %.3f km", LengthOnRail/1000)) - + -- Route, ground waypoints along road. local route={} - + -- Check if a valid path on railroad could be found. if PathOnRail then table.insert(route, PathOnRail[1]:WaypointGround(Speed, "On Railroad")) table.insert(route, PathOnRail[2]:WaypointGround(Speed, "On Railroad")) - + end - return route + return route end --- Make the AIR Controllable fly towards a specific point. @@ -2551,18 +2564,18 @@ do -- Route methods -- @param #number DelaySeconds Wait for the specified seconds before executing the Route. -- @return #CONTROLLABLE The CONTROLLABLE. function CONTROLLABLE:RouteAirTo( ToCoordinate, AltType, Type, Action, Speed, DelaySeconds ) - + local FromCoordinate = self:GetCoordinate() local FromWP = FromCoordinate:WaypointAir() - + local ToWP = ToCoordinate:WaypointAir( AltType, Type, Action, Speed ) - + self:Route( { FromWP, ToWP }, DelaySeconds ) - + return self end - - + + --- (AIR + GROUND) Route the controllable to a given zone. -- The controllable final destination point can be randomized. -- A speed can be given in km/h. @@ -2574,58 +2587,58 @@ do -- Route methods -- @param Base#FORMATION Formation The formation string. function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation ) self:F2( Zone ) - + local DCSControllable = self:GetDCSObject() - + if DCSControllable then - + local ControllablePoint = self:GetVec2() - + local PointFrom = {} PointFrom.x = ControllablePoint.x PointFrom.y = ControllablePoint.y PointFrom.type = "Turning Point" PointFrom.action = Formation or "Cone" PointFrom.speed = 20 / 3.6 - - + + local PointTo = {} local ZonePoint - + if Randomize then ZonePoint = Zone:GetRandomVec2() else ZonePoint = Zone:GetVec2() end - + PointTo.x = ZonePoint.x PointTo.y = ZonePoint.y PointTo.type = "Turning Point" - + if Formation then PointTo.action = Formation else PointTo.action = "Cone" end - + if Speed then PointTo.speed = Speed else PointTo.speed = 20 / 3.6 end - + local Points = { PointFrom, PointTo } - + self:T3( Points ) - + self:Route( Points ) - + return self end - + return nil end - + --- (GROUND) Route the controllable to a given Vec2. -- A speed can be given in km/h. -- A given formation can be given. @@ -2634,48 +2647,48 @@ do -- Route methods -- @param #number Speed The speed in m/s. Default is 5.555 m/s = 20 km/h. -- @param Base#FORMATION Formation The formation string. function CONTROLLABLE:TaskRouteToVec2( Vec2, Speed, Formation ) - + local DCSControllable = self:GetDCSObject() - + if DCSControllable then - + local ControllablePoint = self:GetVec2() - + local PointFrom = {} PointFrom.x = ControllablePoint.x PointFrom.y = ControllablePoint.y PointFrom.type = "Turning Point" PointFrom.action = Formation or "Cone" PointFrom.speed = 20 / 3.6 - - + + local PointTo = {} - + PointTo.x = Vec2.x PointTo.y = Vec2.y PointTo.type = "Turning Point" - + if Formation then PointTo.action = Formation else PointTo.action = "Cone" end - + if Speed then PointTo.speed = Speed else PointTo.speed = 20 / 3.6 end - + local Points = { PointFrom, PointTo } - + self:T3( Points ) - + self:Route( Points ) - + return self end - + return nil end @@ -2796,8 +2809,8 @@ function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRad local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil - - + + local Params = {} if DetectionVisual then Params[#Params+1] = DetectionVisual @@ -2817,10 +2830,10 @@ function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRad if DetectionDLINK then Params[#Params+1] = DetectionDLINK end - - + + self:T2( { DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK } ) - + return self:_GetController():getDetectedTargets( Params[1], Params[2], Params[3], Params[4], Params[5], Params[6] ) end @@ -2833,7 +2846,7 @@ function CONTROLLABLE:IsTargetDetected( DCSObject, DetectVisual, DetectOptical, self:F2( self.ControllableName ) local DCSControllable = self:GetDCSObject() - + if DCSControllable then local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil @@ -2847,7 +2860,7 @@ function CONTROLLABLE:IsTargetDetected( DCSObject, DetectVisual, DetectOptical, local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity = Controller:isTargetDetected( DCSObject, DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK ) - + return TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity end @@ -3226,7 +3239,7 @@ function CONTROLLABLE:OptionAlarmStateAuto() if self:IsGround() then Controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.AUTO) - elseif self:IsShip() then + elseif self:IsShip() then --Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.AUTO) Controller:setOption(9, 0) end @@ -3273,7 +3286,7 @@ function CONTROLLABLE:OptionAlarmStateRed() if self:IsGround() then Controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED) - elseif self:IsShip() then + elseif self:IsShip() then --Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.RED) Controller:setOption(9, 2) end @@ -3467,21 +3480,3 @@ function CONTROLLABLE:IsAirPlane() return nil end - - ---- Returns if the Controllable contains Helicopters. --- @param #CONTROLLABLE self --- @return #boolean true if Controllable contains Helicopters. -function CONTROLLABLE:IsHelicopter() - self:F2() - - local DCSObject = self:GetDCSObject() - - if DCSObject then - local Category = DCSObject:getDesc().category - return Category == Unit.Category.HELICOPTER - end - - return nil -end - From 9b96ff59783bfe81cf9f660b88b72aba4c2109e5 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 2 Oct 2019 19:47:33 +0200 Subject: [PATCH 375/485] AIRBASE - Added runwaydata and active runway functions --- Moose Development/Moose/Wrapper/Airbase.lua | 201 +++++++++++++++++++- 1 file changed, 197 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 89b2a38f2..6da64bf10 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -13,6 +13,9 @@ --- @type AIRBASE +-- @field #string ClassName Name of the class, i.e. "AIRBASE". +-- @field #table CategoryName Names of airbase categories. +-- @field #number activerwyno Active runway number (forced). -- @extends Wrapper.Positionable#POSITIONABLE --- Wrapper class to handle the DCS Airbase objects: @@ -54,6 +57,7 @@ AIRBASE = { [Airbase.Category.HELIPAD] = "Helipad", [Airbase.Category.SHIP] = "Ship", }, + activerwyno=nil, } --- Enumeration to identify the airbases in the Caucasus region. @@ -324,6 +328,14 @@ AIRBASE.TerminalType = { FighterAircraft=244, } +--- Runway data. +-- @type AIRBASE.Runway +-- @field #number heading Heading of the runway in degrees. +-- @field #string idx Runway ID: heading 070° ==> idx="07". +-- @field #number length Length of runway in meters. +-- @field Core.Point#COORDINATE position Position of runway start. +-- @field Core.Point#COORDINATE endpoint End point of runway. + -- Registration. --- Create a new AIRBASE from DCSAirbase. @@ -403,13 +415,17 @@ end --- Get all airbases of the current map. This includes ships and FARPS. -- @param DCS#Coalition coalition (Optional) Return only airbases belonging to the specified coalition. By default, all airbases of the map are returned. +-- @param #number category (Optional) Return only airbases of a certain category, e.g. Airbase.Category.FARP -- @return #table Table containing all airbase objects of the current map. -function AIRBASE.GetAllAirbases(coalition) +function AIRBASE.GetAllAirbases(coalition, category) local airbases={} - for _,airbase in pairs(_DATABASE.AIRBASES) do + for _,_airbase in pairs(_DATABASE.AIRBASES) do + local airbase=_airbase --#AIRBASE if (coalition~=nil and airbase:GetCoalition()==coalition) or coalition==nil then - table.insert(airbases, airbase) + if category==nil or category==airbase:GetAirbaseCategory() then + table.insert(airbases, airbase) + end end end @@ -530,7 +546,7 @@ function AIRBASE:GetParkingSpotsCoordinates(termtype) -- Put coordinates of free spots into table. local spots={} - for _,parkingspot in pairs(parkingdata) do + for _,parkingspot in ipairs(parkingdata) do -- Coordinates on runway are not returned unless explicitly requested. if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then @@ -990,3 +1006,180 @@ function AIRBASE._CheckTerminalType(Term_Type, termtype) return match end + +--- Get runways data. Only for airdromes! +-- @param #AIRBASE self +-- @param #number magvar (Optional) Magnetic variation in degrees. +-- @return #table Runway data. +function AIRBASE:GetRunwayData(magvar) + + -- Runway table. + local runways={} + + if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME then + return {} + end + + -- Get spawn points on runway. + local runwaycoords=self:GetParkingSpotsCoordinates(AIRBASE.TerminalType.Runway) + + -- Magnetic declination. + magvar=magvar or UTILS.GetMagneticDeclination() + + local N=#runwaycoords + local dN=2 + local ex=false + + local name=self:GetName() + if name==AIRBASE.Nevada.Jean_Airport or + name==AIRBASE.Nevada.Creech_AFB or + name==AIRBASE.PersianGulf.Abu_Dhabi_International_Airport or + name==AIRBASE.PersianGulf.Dubai_Intl or + name==AIRBASE.PersianGulf.Shiraz_International_Airport or + name==AIRBASE.PersianGulf.Kish_International_Airport then + + N=#runwaycoords/2 + dN=1 + ex=true + end + + + for i=1,N,dN do + + local j=i+1 + if ex then + --j=N+i + j=#runwaycoords-i+1 + end + + -- Coordinates of the two runway points. + local c1=runwaycoords[i] --Core.Point#COORDINATES + local c2=runwaycoords[j] --Core.Point#COORDINATES + + -- Heading of runway. + local hdg=c1:HeadingTo(c2) + + -- Runway ID: heading=070° ==> idx="07" + local idx=string.format("%02d", UTILS.Round((hdg-magvar)/10, 0)) + + -- Runway table. + local runway={} --#AIRBASE.Runway + runway.heading=hdg + runway.idx=idx + runway.length=c1:Get2DDistance(c2) + runway.position=c1 + runway.endpoint=c2 + + -- Debug info. + self:T(string.format("Airbase %s: Adding runway id=%s, heading=%03d, length=%d m", self:GetName(), runway.idx, runway.heading, runway.length)) + + -- Debug mark + runway.position:MarkToAll(string.format("Runway %s Heading=%03d", runway.idx, runway.heading)) + + -- Add runway. + table.insert(runways, runway) + + end + + -- Get inverse runways + local inverse={} + for _,_runway in pairs(runways) do + local r=_runway --#AIRBASE.Runway + + local runway={} --#AIRBASE.Runway + runway.heading=r.heading-180 + if runway.heading<0 then + runway.heading=runway.heading+360 + end + runway.idx=string.format("%02d", math.max(0, UTILS.Round((runway.heading-magvar)/10, 0))) + runway.length=r.length + runway.position=r.endpoint + runway.endpoint=r.position + + -- Debug info. + self:T(string.format("Airbase %s: Adding runway id=%s, heading=%03d, length=%d m", self:GetName(), runway.idx, runway.heading, runway.length)) + + -- Debug mark + runway.position:MarkToAll(string.format("Runway %s Heading=%03d", runway.idx, runway.heading)) + + -- Add runway. + table.insert(inverse, runway) + end + + for _,runway in pairs(inverse) do + -- Add runway. + table.insert(runways, runway) + end + + return runways +end + +--- Set the active runway in case it cannot be determined by the wind direction. +-- @param #AIRBASE self +-- @param #number iactive Number of the active runway in the runway data table. +function AIRBASE:SetActiveRunway(iactive) + self.activerwyno=iactive +end + +--- Get the active runway based on current wind direction. +-- @param #AIRBASE self +-- @param #number magvar (Optional) Magnetic variation in degrees. +-- @return #AIRBASE.Runway Active runway data table. +function AIRBASE:GetActiveRunway(magvar) + + -- Get runways data (initialize if necessary). + local runways=self:GetRunwayData(magvar) + + -- Return user forced active runway if it was set. + if self.activerwyno then + return runways[self.activerwyno] + end + + -- Get wind vector. + local Vwind=self:GetCoordinate():GetWindWithTurbulenceVec3() + local norm=UTILS.VecNorm(Vwind) + + -- Active runway number. + local iact=1 + + -- Check if wind is blowing (norm>0). + if norm>0 then + + -- Normalize wind (not necessary). + Vwind.x=Vwind.x/norm + Vwind.y=0 + Vwind.z=Vwind.z/norm + + -- Loop over runways. + local dotmin=nil + for i,_runway in pairs(runways) do + local runway=_runway --#AIRBASE.Runway + + -- Angle in rad. + local alpha=math.rad(runway.heading) + + -- Runway vector. + local Vrunway={x=math.cos(alpha), y=0, z=math.sin(alpha)} + + -- Dot product: parallel component of the two vectors. + local dot=UTILS.VecDot(Vwind, Vrunway) + + -- Debug. + env.info(string.format("runway=%03d° dot=%.3f", runway.heading, dot)) + + -- New min? + if dotmin==nil or dot Date: Wed, 2 Oct 2019 20:43:51 +0200 Subject: [PATCH 376/485] ATIS v0.3.0 --- Moose Development/Moose/Modules.lua | 1 + Moose Development/Moose/Ops/ATIS.lua | 6 ++++-- Moose Development/Moose/Wrapper/Airbase.lua | 17 +++++++++++------ Moose Setup/Moose.files | 1 + 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index da8a13e96..22f23bb75 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -68,6 +68,7 @@ __Moose.Include( 'Scripts/Moose/Functional/Fox.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/Ops/ATIS.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' ) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 564cfa8dc..b9da4738e 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -113,8 +113,10 @@ -- -- ## Active Runway -- --- By default, the currently active runway is determined automatically by analysing the wind direction. However, there are special cases, where this does not yield the correct result. --- Also, there are airports with more than one runway facing in the same direction (usually denoted left and right). In this case, there is obviously no *unique* result depending on the wind vector. +-- By default, the currently active runway is determined automatically by analysing the wind direction. Therefore, you should obviously set the wind speed to be greater zero in your mission. +-- +-- Note however, there are a few special cases, where automatic detection does not yield the correct or desired result. +-- For example, there are airports with more than one runway facing in the same direction (usually denoted left and right). In this case, there is obviously no *unique* result depending on the wind vector. -- -- If the automatic runway detection fails, the active runway can be specified manually in the script via the @{#ATIS.SetActiveRunway}(*runway*) function. -- The parameter *runway* is a string which can be used to specify the runway heading and, if applicable, whether the left or right runway is in use. diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 6da64bf10..832b94d76 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -1010,8 +1010,9 @@ end --- Get runways data. Only for airdromes! -- @param #AIRBASE self -- @param #number magvar (Optional) Magnetic variation in degrees. +-- @param #boolean mark (Optional) Place markers with runway data on F10 map. -- @return #table Runway data. -function AIRBASE:GetRunwayData(magvar) +function AIRBASE:GetRunwayData(magvar, mark) -- Runway table. local runways={} @@ -1074,7 +1075,9 @@ function AIRBASE:GetRunwayData(magvar) self:T(string.format("Airbase %s: Adding runway id=%s, heading=%03d, length=%d m", self:GetName(), runway.idx, runway.heading, runway.length)) -- Debug mark - runway.position:MarkToAll(string.format("Runway %s Heading=%03d", runway.idx, runway.heading)) + if mark then + runway.position:MarkToAll(string.format("Runway %s: true heading=%03d, length=%d m", runway.idx, runway.heading, runway.length)) + end -- Add runway. table.insert(runways, runway) @@ -1100,14 +1103,16 @@ function AIRBASE:GetRunwayData(magvar) self:T(string.format("Airbase %s: Adding runway id=%s, heading=%03d, length=%d m", self:GetName(), runway.idx, runway.heading, runway.length)) -- Debug mark - runway.position:MarkToAll(string.format("Runway %s Heading=%03d", runway.idx, runway.heading)) - + if mark then + runway.position:MarkToAll(string.format("Runway %s: true heading=%03d, length=%d m", runway.idx, runway.heading, runway.length)) + end + -- Add runway. table.insert(inverse, runway) end + -- Add inverse runway. for _,runway in pairs(inverse) do - -- Add runway. table.insert(runways, runway) end @@ -1165,7 +1170,7 @@ function AIRBASE:GetActiveRunway(magvar) local dot=UTILS.VecDot(Vwind, Vrunway) -- Debug. - env.info(string.format("runway=%03d° dot=%.3f", runway.heading, dot)) + --env.info(string.format("runway=%03d° dot=%.3f", runway.heading, dot)) -- New min? if dotmin==nil or dot Date: Thu, 3 Oct 2019 12:13:38 +0200 Subject: [PATCH 377/485] ATIS v0.3.1 - Fixed bug in NATO alphabet (entry 14 missing) --- Moose Development/Moose/Ops/ATIS.lua | 38 +++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index b9da4738e..a7ebe4fe8 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -227,26 +227,26 @@ ATIS.Alphabet = { [11] = "India", [12] = "Juliett", [13] = "Kilo", - [15] = "Lima", - [16] = "Mike", - [17] = "November", - [18] = "Oscar", - [19] = "Papa", - [20] = "Quebec", - [21] = "Romeo", - [22] = "Sierra", - [23] = "Tango", - [24] = "Uniform", - [25] = "Victor", - [26] = "Whiskey", - [27] = "Xray", - [28] = "Yankee", - [29] = "Zulu", + [14] = "Lima", + [15] = "Mike", + [16] = "November", + [17] = "Oscar", + [18] = "Papa", + [19] = "Quebec", + [20] = "Romeo", + [21] = "Sierra", + [22] = "Tango", + [23] = "Uniform", + [24] = "Victor", + [25] = "Whiskey", + [26] = "Xray", + [27] = "Yankee", + [28] = "Zulu", } --- ATIS class version. -- @field #string version -ATIS.version="0.3.0" +ATIS.version="0.3.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -642,7 +642,11 @@ function ATIS:onafterBroadcast(From, Event, To) local NATO=ATIS.Alphabet[tonumber(zulu[1])+1] -- Debug. - self:T3({nato=NATO}) + self:T3(string.format("clock=%s", tostring(clock))) + self:T3(string.format("zulu1=%s", tostring(zulu[1]))) + self:T3(string.format("zulu2=%s", tostring(zulu[2]))) + self:T3(string.format("ZULU =%s", tostring(ZULU))) + self:T3(string.format("NATO =%s", tostring(NATO))) ------------------- --- Temperature --- From c0e48288de683581a4974bf883fd14cbe4a0c352 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 8 Oct 2019 00:21:41 +0200 Subject: [PATCH 378/485] ATIS v0.3.3 - Improved soud file duration. - Added Nav point data option. --- Moose Development/Moose/Core/RadioQueue.lua | 3 + Moose Development/Moose/Ops/ATIS.lua | 477 +++++++++++++++++--- 2 files changed, 409 insertions(+), 71 deletions(-) diff --git a/Moose Development/Moose/Core/RadioQueue.lua b/Moose Development/Moose/Core/RadioQueue.lua index baa294dc9..4dd101b95 100644 --- a/Moose Development/Moose/Core/RadioQueue.lua +++ b/Moose Development/Moose/Core/RadioQueue.lua @@ -321,6 +321,8 @@ function RADIOQUEUE:Broadcast(transmission) -- Set command for radio transmission. sender:SetCommand(commandTransmit) + + --MESSAGE:New(string.format("transmissing file %s duration=%.2f sec, subtitle=%s", filename, transmission.duration, transmission.subtitle or ""), 5, "RADIOQUEUE"):ToAll() else @@ -347,6 +349,7 @@ function RADIOQUEUE:Broadcast(transmission) if vec3 then self:T("Sending") self:T( { filename = filename, vec3 = vec3, modulation = self.modulation, frequency = self.frequency, power = self.power } ) + --MESSAGE:New(string.format("transmissing file %s duration=%.2f sec, subtitle=%s", filename, transmission.duration, transmission.subtitle or ""), 5, "RADIOQUEUE (trigger)"):ToAll() trigger.action.radioTransmission(filename, vec3, self.modulation, false, self.frequency, self.power) end diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index a7ebe4fe8..f3c465a1f 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -21,13 +21,13 @@ -- ## Youtube Videos: -- -- * [ATIS v0.1 Caucasus - Batumi (WIP)](https://youtu.be/MdH9FmbNabo) --- * [ATIS Airport Names Sound Check](https://youtu.be/qIE_OUQNAc0) -- * [ATIS v0.2 Nevada - Nellis AFB (WIP)](https://youtu.be/8CT_9AoPrTk) -- * [ATIS v0.3 Persion Gulf - Abu Dhabi/Dubai International](https://youtu.be/NjkKvPz6ovM) +-- * [ATIS Airport Names Sound Check](https://youtu.be/qIE_OUQNAc0) -- -- === -- --- ## Missions: Example missions will be added later. +-- ## Missions: Example missions can be found [here](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20ATIS) -- -- === -- @@ -66,6 +66,13 @@ -- @field #boolean TDegF If true, give temperature in degrees Fahrenheit. Default is in degrees Celsius independent of chosen unit system. -- @field #number zuludiff Time difference local vs. zulu in hours. -- @field #number magvar Magnetic declination/variation at the airport in degrees. +-- @field #table ils Table of ILS frequencies (can be runway specific). +-- @field #table ndbinner Table of inner NDB frequencies (can be runway specific). +-- @field #table ndbouter Table of outer NDB frequencies (can be runway specific). +-- @field #number tacan TACAN channel. +-- @field #number vor VOR frequency. +-- @field #number rsbn RSBN channel. +-- @field #table prmg PRMG channels (can be runway specific). -- @extends Core.Fsm#FSM --- Be informed! @@ -209,6 +216,13 @@ ATIS = { TDegF = nil, zuludiff = nil, magvar = nil, + ils = {}, + ndbinner = {}, + ndbouter = {}, + vor = nil, + tacan = nil, + rsbn = nil, + prmg = {}, } --- NATO alphabet. @@ -244,19 +258,161 @@ ATIS.Alphabet = { [28] = "Zulu", } +--- Nav point data. +-- @type ATIS.NavPoint +-- @field #number frequency Nav point frequency. +-- @field #string runway Runway, e.g. "21". + +--- Sound file data. +-- @type ATIS.Soundfile +-- @field #string filename Name of the file +-- @field #number duration Duration in seconds. + +--- Sound files. +-- @type ATIS.Sound +-- @field #ATIS.Soundfile ActiveRunway +-- @field #ATIS.Soundfile Airport +-- @field #ATIS.Soundfile Altimeter +-- @field #ATIS.Soundfile At +-- @field #ATIS.Soundfile CloudBase +-- @field #ATIS.Soundfile CloudCeiling +-- @field #ATIS.Soundfile CloudsBroken +-- @field #ATIS.Soundfile CloudsFew +-- @field #ATIS.Soundfile CloudsNo +-- @field #ATIS.Soundfile CloudsNotAvailable +-- @field #ATIS.Soundfile CloudsOvercast +-- @field #ATIS.Soundfile CloudsScattered +-- @field #ATIS.Soundfile Decimal +-- @field #ATIS.Soundfile DegreesCelsius +-- @field #ATIS.Soundfile DegreesFahrenheit +-- @field #ATIS.Soundfile Dust +-- @field #ATIS.Soundfile Feet +-- @field #ATIS.Soundfile Fog +-- @field #ATIS.Soundfile Gusting +-- @field #ATIS.Soundfile HectoPascal +-- @field #ATIS.Soundfile Hundred +-- @field #ATIS.Soundfile InchesOfMercury +-- @field #ATIS.Soundfile Information +-- @field #ATIS.Soundfile Kilometers +-- @field #ATIS.Soundfile Knots +-- @field #ATIS.Soundfile Left +-- @field #ATIS.Soundfile MegaHertz +-- @field #ATIS.Soundfile Meters +-- @field #ATIS.Soundfile MetersPerSecond +-- @field #ATIS.Soundfile MillimetersOfMercury +-- @field #ATIS.Soundfile N0 +-- @field #ATIS.Soundfile N1 +-- @field #ATIS.Soundfile N2 +-- @field #ATIS.Soundfile N3 +-- @field #ATIS.Soundfile N4 +-- @field #ATIS.Soundfile N5 +-- @field #ATIS.Soundfile N6 +-- @field #ATIS.Soundfile N7 +-- @field #ATIS.Soundfile N8 +-- @field #ATIS.Soundfile N9 +-- @field #ATIS.Soundfile NauticalMiles +-- @field #ATIS.Soundfile None +-- @field #ATIS.Soundfile QFE +-- @field #ATIS.Soundfile QNH +-- @field #ATIS.Soundfile Rain +-- @field #ATIS.Soundfile Right +-- @field #ATIS.Soundfile Temperature +-- @field #ATIS.Soundfile Thousand +-- @field #ATIS.Soundfile ThunderStorm +-- @field #ATIS.Soundfile TimeLocal +-- @field #ATIS.Soundfile TimeZulu +-- @field #ATIS.Soundfile TowerFrequency +-- @field #ATIS.Soundfile Visibilty +-- @field #ATIS.Soundfile WeatherPhenomena +-- @field #ATIS.Soundfile WindFrom +-- @field #ATIS.Soundfile ILSFrequency +-- @field #ATIS.Soundfile InnerNDBFrequency +-- @field #ATIS.Soundfile OuterNDBFrequency +-- @field #ATIS.Soundfile PRGMChannel +-- @field #ATIS.Soundfiel RSBNChannel +-- @field #ATIS.Soundfile RunwayLength +-- @field #ATIS.Soundfile TACANChannel +-- @field #ATIS.Soundfile VORFrequency +ATIS.Sound = { + ActiveRunway={filename="ActiveRunway.ogg", duration=0.99}, + Airport={filename="Airport.ogg", duration=0.66}, + Altimeter={filename="Altimeter.ogg", duration=0.68}, + At={filename="At.ogg", duration=0.41}, + CloudBase={filename="CloudBase.ogg", duration=0.82}, + CloudCeiling={filename="CloudCeiling.ogg", duration=0.61}, + CloudsBroken={filename="CloudsBroken.ogg", duration=1.07}, + CloudsFew={filename="CloudsFew.ogg", duration=0.99}, + CloudsNo={filename="CloudsNo.ogg", duration=1.01}, + CloudsNotAvailable={filename="CloudsNotAvailable.ogg", duration=2.35}, + CloudsOvercast={filename="CloudsOvercast.ogg", duration=0.83}, + CloudsScattered={filename="CloudsScattered.ogg", duration=1.18}, + Decimal={filename="Decimal.ogg", duration=0.54}, + DegreesCelsius={filename="DegreesCelsius.ogg", duration=1.27}, + DegreesFahrenheit={filename="DegreesFahrenheit.ogg", duration=1.23}, + Dust={filename="Dust.ogg", duration=0.54}, + Feet={filename="Feet.ogg", duration=0.45}, + Fog={filename="Fog.ogg", duration=0.47}, + Gusting={filename="Gusting.ogg", duration=0.55}, + HectoPascal={filename="HectoPascal.ogg", duration=1.15}, + Hundred={filename="Hundred.ogg", duration=0.47}, + InchesOfMercury={filename="InchesOfMercury.ogg", duration=1.16}, + Information={filename="Information.ogg", duration=0.85}, + Kilometers={filename="Kilometers.ogg", duration=0.78}, + Knots={filename="Knots.ogg", duration=0.59}, + Left={filename="Left.ogg", duration=0.54}, + MegaHertz={filename="MegaHertz.ogg", duration=0.87}, + Meters={filename="Meters.ogg", duration=0.59}, + MetersPerSecond={filename="MetersPerSecond.ogg", duration=1.14}, + MillimetersOfMercury={filename="MillimetersOfMercury.ogg", duration=1.53}, + Minus={filename="Minus.ogg", duration=0.64}, + N0={filename="N-0.ogg", duration=0.55}, + N1={filename="N-1.ogg", duration=0.41}, + N2={filename="N-2.ogg", duration=0.37}, + N3={filename="N-3.ogg", duration=0.41}, + N4={filename="N-4.ogg", duration=0.37}, + N5={filename="N-5.ogg", duration=0.43}, + N6={filename="N-6.ogg", duration=0.55}, + N7={filename="N-7.ogg", duration=0.43}, + N8={filename="N-8.ogg", duration=0.38}, + N9={filename="N-9.ogg", duration=0.55}, + NauticalMiles={filename="NauticalMiles.ogg", duration=1.04}, + None={filename="None.ogg", duration=0.43}, + QFE={filename="QFE.ogg", duration=0.63}, + QNH={filename="QNH.ogg", duration=0.71}, + Rain={filename="Rain.ogg", duration=0.41}, + Right={filename="Right.ogg", duration=0.44}, + Temperature={filename="Temperature.ogg", duration=0.64}, + Thousand={filename="Thousand.ogg", duration=0.55}, + ThunderStorm={filename="ThunderStorm.ogg", duration=0.81}, + TimeLocal={filename="TimeLocal.ogg", duration=0.90}, + TimeZulu={filename="TimeZulu.ogg", duration=0.86}, + TowerFrequency={filename="TowerFrequency.ogg", duration=1.19}, + Visibilty={filename="Visibility.ogg", duration=0.79}, + WeatherPhenomena={filename="WeatherPhenomena.ogg", duration=1.07}, + WindFrom={filename="WindFrom.ogg", duration=0.60}, + ILSFrequency={filename="ILSFrequency.ogg", duration=1.30}, + InnerNDBFrequency={filename="InnerNDBFrequency.ogg", duration=1.56}, + OuterNDBFrequency={filename="OuterNDBFrequency.ogg", duration=1.59}, + RunwayLength={filename="RunwayLength.ogg", duration=0.91}, + VORFrequency={filename="VORFrequency.ogg", duration=1.38}, + TACANChannel={filename="TACANChannel.ogg", duration=0.88}, + PRGMChannel={filename="PRGMChannel.ogg", duration=1.12}, + RSBNChannel={filename="RSBNChannel.ogg", duration=1.14}, +} + --- ATIS class version. -- @field #string version -ATIS.version="0.3.1" +ATIS.version="0.3.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Metric units. +-- DONE: Metric units. -- TODO: Correct fog for elevation. --- TODO: Set UTC correction. +-- DONE: Set UTC correction. -- TODO: Use local time. --- TODO: Set magnetic variation. +-- DONE: Set magnetic variation. -- TODO: Add stop/pause FMS functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -461,6 +617,68 @@ function ATIS:SetZuluTimeDifference(delta) return self end +--- Add ILS station. +-- @param #ATIS self +-- @param #number frequency ILS frequency. +-- @param #string runway Runway. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddILS(frequency, runway) + local ils={} --#ATIS.NavPoint + ils.frequency=tonumber(frequency) + ils.runway=runway + table.insert(self.ils, ils) + return self +end + +--- Add VOR station. +-- @param #ATIS self +-- @param #number frequency VOR frequency. +-- @param #string runway Runway. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddVOR(frequency, runway) + local vor={} --#ATIS.NavPoint + vor.frequency=tonumber(frequency) + vor.runway=runway + table.insert(self.vor, vor) + return self +end + +--- Add outer NDB. +-- @param #ATIS self +-- @param #number frequency NDB frequency. +-- @param #string runway Runway. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddNDBouter(frequency, runway) + local ndb={} --#ATIS.NavPoint + ndb.frequency=tonumber(frequency) + ndb.runway=runway + table.insert(self.ndbouter, ndb) + return self +end + +--- Add inner NDB. +-- @param #ATIS self +-- @param #number frequency NDB frequency. +-- @param #string runway Runway. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddNDBouter(frequency, runway) + local ndb={} --#ATIS.NavPoint + ndb.frequency=tonumber(frequency) + ndb.runway=runway + table.insert(self.ndbinner, ndb) + return self +end + +--- Set TACAN channel. +-- @param #ATIS self +-- @param #number tacan TACAN channel. +-- @return #ATIS self +function ATIS:SetTACAN(tacan) + self.tacan=tacan + return self +end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -482,16 +700,16 @@ function ATIS:onafterStart(From, Event, To) self.radioqueue:SetSenderUnitName(self.relayunitname) -- Init numbers. - self.radioqueue:SetDigit(0, "N-0.ogg", 0.55, self.soundpath) - self.radioqueue:SetDigit(1, "N-1.ogg", 0.40, self.soundpath) - self.radioqueue:SetDigit(2, "N-2.ogg", 0.35, self.soundpath) - self.radioqueue:SetDigit(3, "N-3.ogg", 0.40, self.soundpath) - self.radioqueue:SetDigit(4, "N-4.ogg", 0.36, self.soundpath) - self.radioqueue:SetDigit(5, "N-5.ogg", 0.42, self.soundpath) - self.radioqueue:SetDigit(6, "N-6.ogg", 0.53, self.soundpath) - self.radioqueue:SetDigit(7, "N-7.ogg", 0.42, self.soundpath) - self.radioqueue:SetDigit(8, "N-8.ogg", 0.37, self.soundpath) - self.radioqueue:SetDigit(9, "N-9.ogg", 0.38, self.soundpath) + self.radioqueue:SetDigit(0, ATIS.Sound.N0.filename, ATIS.Sound.N0.duration, self.soundpath) + self.radioqueue:SetDigit(1, ATIS.Sound.N1.filename, ATIS.Sound.N1.duration, self.soundpath) + self.radioqueue:SetDigit(2, ATIS.Sound.N2.filename, ATIS.Sound.N2.duration, self.soundpath) + self.radioqueue:SetDigit(3, ATIS.Sound.N3.filename, ATIS.Sound.N3.duration, self.soundpath) + self.radioqueue:SetDigit(4, ATIS.Sound.N4.filename, ATIS.Sound.N4.duration, self.soundpath) + self.radioqueue:SetDigit(5, ATIS.Sound.N5.filename, ATIS.Sound.N5.duration, self.soundpath) + self.radioqueue:SetDigit(6, ATIS.Sound.N6.filename, ATIS.Sound.N6.duration, self.soundpath) + self.radioqueue:SetDigit(7, ATIS.Sound.N7.filename, ATIS.Sound.N7.duration, self.soundpath) + self.radioqueue:SetDigit(8, ATIS.Sound.N8.filename, ATIS.Sound.N8.duration, self.soundpath) + self.radioqueue:SetDigit(9, ATIS.Sound.N9.filename, ATIS.Sound.N9.duration, self.soundpath) -- Start radio queue. self.radioqueue:Start(1, 0.1) @@ -512,7 +730,7 @@ function ATIS:onafterStatus(From, Event, To) local text=string.format("State %s", fsmstate) self:I(self.lid..text) - self:__Status(30) + self:__Status(60) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -734,39 +952,34 @@ function ATIS:onafterBroadcast(From, Event, To) end -- No cloud info for dynamic weather. - local CLOUDSogg="CloudsNotAvailable.ogg" + local CloudCover={} --#ATIS.Soundfile + CloudCover=ATIS.Sound.CloudsNotAvailable local CLOUDSsub="Cloud coverage information not available" - local CLOUDSdur=2.40 -- Only valid for static weather. if static then if clouddens>=9 then -- Overcast 9,10 - CLOUDSogg="CloudsOvercast.ogg" + CloudCover=ATIS.Sound.CloudsOvercast CLOUDSsub="Overcast" - CLOUDSdur=0.85 elseif clouddens>=7 then -- Broken 7,8 - CLOUDSogg="CloudsBroken.ogg" + CloudCover=ATIS.Sound.CloudsBroken CLOUDSsub="Broken clouds" - CLOUDSdur=1.10 elseif clouddens>=4 then -- Scattered 4,5,6 - CLOUDSogg="CloudsScattered.ogg" + CloudCover=ATIS.Sound.CloudsScattered CLOUDSsub="Scattered clouds" - CLOUDSdur=1.20 elseif clouddens>=1 then -- Few 1,2,3 - CLOUDSogg="CloudsFew.ogg" + CloudCover=ATIS.Sound.CloudsFew CLOUDSsub="Few clouds" - CLOUDSdur=1.00 else -- No clouds CLOUDBASE=nil CLOUDCEIL=nil - CLOUDSogg="CloudsNo.ogg" + CloudCover=ATIS.Sound.CloudsNo CLOUDSsub="No clouds" - CLOUDSdur=1.00 end end @@ -774,7 +987,7 @@ function ATIS:onafterBroadcast(From, Event, To) --- Transmission --- -------------------- - local subduration=self.subduration + -- Subtitle local subtitle="" --Airbase name @@ -782,17 +995,17 @@ function ATIS:onafterBroadcast(From, Event, To) if self.airbasename:find("AFB")==nil and self.airbasename:find("Airport")==nil and self.airbasename:find("Airstrip")==nil and self.airbasename:find("airfield")==nil and self.airbasename:find("AB")==nil then subtitle=subtitle.." Airport" end - self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, subduration) + self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, self.subduration) -- Information tag subtitle=string.format("Information %s", NATO) - self.radioqueue:NewTransmission("Information.ogg", 0.85, self.soundpath, nil, 0.5, subtitle, subduration) + self:Transmission(ATIS.Sound.Information, 0.5, subtitle) self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) -- Zulu Time subtitle=string.format("%s Zulu Time", ZULU) self.radioqueue:Number2Transmission(ZULU, nil, 0.5) - self.radioqueue:NewTransmission("TimeZulu.ogg", 0.89, self.soundpath, nil, 0.2, subtitle, subduration) + self:Transmission(ATIS.Sound.TimeZulu, 0.2, subtitle) -- Visibility if self.metric then @@ -800,16 +1013,16 @@ function ATIS:onafterBroadcast(From, Event, To) else subtitle=string.format("Visibility %s NM", VISIBILITY) end - self.radioqueue:NewTransmission("Visibility.ogg", 0.8, self.soundpath, nil, 1.0, subtitle, subduration) + self:Transmission(ATIS.Sound.Visibilty, 1.0, subtitle) self.radioqueue:Number2Transmission(VISIBILITY) if self.metric then - self.radioqueue:NewTransmission("Kilometers.ogg", 0.78, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.Kilometers, 0.2) else - self.radioqueue:NewTransmission("NauticalMiles.ogg", 1.05, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.NauticalMiles, 0.2) end -- Cloud base - self.radioqueue:NewTransmission(CLOUDSogg, CLOUDSdur, self.soundpath, nil, 1.0, CLOUDSsub, subduration) + self:Transmission(CloudCover, 1.0, CLOUDSsub) if CLOUDBASE and static then -- Base if self.metric then @@ -817,29 +1030,29 @@ function ATIS:onafterBroadcast(From, Event, To) else subtitle=string.format("Cloudbase %s, ceiling %s ft", CLOUDBASE, CLOUDCEIL) end - self.radioqueue:NewTransmission("CloudBase.ogg", 0.81, self.soundpath, nil, 1.0, subtitle, subduration) + self:Transmission(ATIS.Sound.CloudBase, 1.0, subtitle) if tonumber(CLOUDBASE1000)>0 then self.radioqueue:Number2Transmission(CLOUDBASE1000) - self.radioqueue:NewTransmission("Thousand.ogg", 0.55, self.soundpath, nil, 0.1) + self:Transmission(ATIS.Sound.Thousand, 0.1) end if tonumber(CLOUDBASE0100)>0 then self.radioqueue:Number2Transmission(CLOUDBASE0100) - self.radioqueue:NewTransmission("Hundred.ogg", 0.47, self.soundpath, nil, 0.1) + self:Transmission(ATIS.Sound.Hundred, 0.1) end -- Ceiling - self.radioqueue:NewTransmission("CloudCeiling.ogg", 0.62, self.soundpath, nil, 0.5) + self:Transmission(ATIS.Sound.CloudCeiling, 0.5) if tonumber(CLOUDCEIL1000)>0 then self.radioqueue:Number2Transmission(CLOUDCEIL1000) - self.radioqueue:NewTransmission("Thousand.ogg", 0.55, self.soundpath, nil, 0.1) + self:Transmission(ATIS.Sound.Thousand, 0.1) end if tonumber(CLOUDCEIL0100)>0 then self.radioqueue:Number2Transmission(CLOUDCEIL0100) - self.radioqueue:NewTransmission("Hundred.ogg", 0.47, self.soundpath, nil, 0.1) + self:Transmission(ATIS.Sound.Hundred, 0.1) end if self.metric then - self.radioqueue:NewTransmission("Meters.ogg", 0.59, self.soundpath, nil, 0.1) + self:Transmission(ATIS.Sound.Meters, 0.1) else - self.radioqueue:NewTransmission("Feet.ogg", 0.45, self.soundpath, nil, 0.1) + self:Transmission(ATIS.Sound.Feet, 0.1) end end @@ -872,17 +1085,18 @@ function ATIS:onafterBroadcast(From, Event, To) end -- Actual output if wp then - self.radioqueue:NewTransmission("WeatherPhenomena.ogg", 1.07, self.soundpath, nil, 1.0, string.format("Weather phenomena:%s", wpsub), subduration) + subtitle=string.format("Weather phenomena:%s", wpsub) + self:Transmission(ATIS.Sound.WeatherPhenomena, 1.0, subtitle) if precepitation==1 then - self.radioqueue:NewTransmission("Rain.ogg", 0.41, self.soundpath, nil, 0.5) + self:Transmission(ATIS.Sound.Rain, 0.5) elseif precepitation==2 then - self.radioqueue:NewTransmission("ThunderStorm.ogg", 0.81, self.soundpath, nil, 0.5) + self:Transmission(ATIS.Sound.ThunderStorm, 0.5) end if fog then - self.radioqueue:NewTransmission("Fog.ogg", 0.81, self.soundpath, nil, 0.5) + self:Transmission(ATIS.Sound.Fog, 0.5) end if dust then - self.radioqueue:NewTransmission("Dust.ogg", 0.81, self.soundpath, nil, 0.5) + self:Transmission(ATIS.Sound.Dust, 0.5) end end @@ -896,22 +1110,22 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2]) end end - self.radioqueue:NewTransmission("Altimeter.ogg", 0.7, self.soundpath, nil, 1.0, subtitle, subduration) - self.radioqueue:NewTransmission("QNH.ogg", 0.70, self.soundpath, nil, 0.5) + self:Transmission(ATIS.Sound.Altimeter, 1.0, subtitle) + self:Transmission(ATIS.Sound.QNH, 0.5) self.radioqueue:Number2Transmission(QNH[1]) - self.radioqueue:NewTransmission("Decimal.ogg", 0.58, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.Decimal, 0.2) self.radioqueue:Number2Transmission(QNH[2]) - self.radioqueue:NewTransmission("QFE.ogg", 0.62, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.QFE, 0.2) self.radioqueue:Number2Transmission(QFE[1]) - self.radioqueue:NewTransmission("Decimal.ogg", 0.58, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.Decimal, 0.2) self.radioqueue:Number2Transmission(QFE[2]) if self.PmmHg then - self.radioqueue:NewTransmission("MillimetersOfMercury.ogg", 1.53, self.soundpath, nil, 0.1) + self:Transmission(ATIS.Sound.MillimetersOfMercury, 0.1) else if self.metric then - self.radioqueue:NewTransmission("HectoPascal.ogg", 1.15, self.soundpath, nil, 0.1) + self:Transmission(ATIS.Sound.HectoPascal, 0.1) else - self.radioqueue:NewTransmission("InchesOfMercury.ogg", 1.16, self.soundpath, nil, 0.1) + self:Transmission(ATIS.Sound.InchesOfMercury, 0.1) end end @@ -921,12 +1135,12 @@ function ATIS:onafterBroadcast(From, Event, To) else subtitle=string.format("Temperature %s °C", TEMPERATURE) end - self.radioqueue:NewTransmission("Temperature.ogg", 0.55, self.soundpath, nil, 1.0, subtitle, subduration) + self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) self.radioqueue:Number2Transmission(TEMPERATURE) if self.TDegF then - self.radioqueue:NewTransmission("DegreesFahrenheit.ogg", 1.23, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) else - self.radioqueue:NewTransmission("DegreesCelsius.ogg", 1.28, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) end -- Wind @@ -938,17 +1152,17 @@ function ATIS:onafterBroadcast(From, Event, To) if turbulence>0 then subtitle=subtitle..", gusting" end - self.radioqueue:NewTransmission("WindFrom.ogg", 0.60, self.soundpath, nil, 1.0, subtitle, subduration) + self:Transmission(ATIS.Sound.WindFrom, 1.0, subtitle) self.radioqueue:Number2Transmission(WINDFROM) - self.radioqueue:NewTransmission("At.ogg", 0.40, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.At, 0.2) self.radioqueue:Number2Transmission(WINDSPEED) if self.metric then - self.radioqueue:NewTransmission("MetersPerSecond.ogg", 1.14, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.MetersPerSecond, 0.2) else - self.radioqueue:NewTransmission("Knots.ogg", 0.60, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.Knots, 0.2) end if turbulence>0 then - self.radioqueue:NewTransmission("Gusting.ogg", 0.55, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.Gusting, 0.2) end -- Active runway. @@ -958,14 +1172,17 @@ function ATIS:onafterBroadcast(From, Event, To) elseif rright then subtitle=subtitle.." Right" end - self.radioqueue:NewTransmission("ActiveRunway.ogg", 1.05, self.soundpath, nil, 1.0, subtitle, subduration) + self:Transmission(ATIS.Sound.Knots, 1.0, subtitle) self.radioqueue:Number2Transmission(runway) if rleft then - self.radioqueue:NewTransmission("Left.ogg", 0.53, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.Left, 0.2) elseif rright then + self:Transmission(ATIS.Sound.Right, 0.2) self.radioqueue:NewTransmission("Right.ogg", 0.43, self.soundpath, nil, 0.2) end + --TODO: runway length + -- Tower frequency. if self.towerfrequency then local freqs="" @@ -975,23 +1192,141 @@ function ATIS:onafterBroadcast(From, Event, To) freqs=freqs..", " end end - self.radioqueue:NewTransmission("TowerFrequency.ogg", 1.19, self.soundpath, nil, 1.0, string.format("Tower frequency %s", freqs), subduration) + subtitle=string.format("Tower frequency %s", freqs) + self:Transmission(ATIS.Sound.TowerFrequency, 1.0, subtitle) for _,freq in pairs(self.towerfrequency) do local f=string.format("%.3f", freq) f=UTILS.Split(f, ".") self.radioqueue:Number2Transmission(f[1], nil, 0.5) - self.radioqueue:NewTransmission("Decimal.ogg", 0.58, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.Decimal, 0.2) self.radioqueue:Number2Transmission(f[2]) - self.radioqueue:NewTransmission("MegaHertz.ogg", 0.86, self.soundpath, nil, 0.2) + self:Transmission(ATIS.Sound.MegaHertz, 0.2) end end + -- ILS + local ils=self:GetNavPoint(self.ils, runway) + if ils then + subtitle=string.format("ILS frequency %.2f", ils.frequency) + self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) + local f=string.format("%.2f", vor.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + + -- Outer NDB + local ndb=self:GetNavPoint(self.ndbouter, runway) + if ndb then + subtitle=string.format("Outer NDB frequency %.2f", ndb.frequency) + self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) + local f=string.format("%.2f", ndb.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + + -- Inner NDB + local ndb=self:GetNavPoint(self.ndbinner, runway) + if ndb then + subtitle=string.format("Inner NDB frequency %.2f", ndb.frequency) + self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) + local f=string.format("%.2f", ndb.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + + -- VOR + if self.vor then + subtitle=string.format("VOR frequency %.2f", self.vor) + self:Transmission(ATIS.Sound.VORFrequency, 1.0, subtitle) + local f=string.format("%.2f", self.vor) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + + -- TACAN + if self.tacan then + subtitle=string.format("TACAN channel %dX", self.tacan) + self:Transmission(ATIS.Sound.TACANChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(self.tacan, nil, 0.2) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/Xray.ogg", NATO), 0.75, self.soundpath) + end + + -- RSBN + if self.prmg then + subtitle=string.format("RSBN channel %d", self.rsbn) + self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(self.rsbn, nil, 0.2) + end + + -- PRMG + local ndb=self:GetNavPoint(self.prmg, runway) + if ndb then + subtitle=string.format("PRMG %d", ndb.frequency) + self:Transmission(ATIS.Sound.PRGMChannel, 1.0, subtitle) + local f=string.format("%.2f", vor.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get Nav point data. +-- @param #ATIS self +-- @param #table navpoints Nav points data table. +-- @param #string runway (Active) runway, e.g. "31" +-- @return #ATIS.NavPoint Nav point data table. +function ATIS:GetNavPoint(navpoints, runway) + for _,_nav in pairs(navpoints or {}) do + local nav=_nav --#ATIS.NavPoint + if nav.runway==nil or nav.runway==runway then + return nav + end + end + return nil +end + + +--- Transmission via RADIOQUEUE. +-- @param #ATIS self +-- @param #string runway Runway, e.g. "31" +-- @return #ATIS.NavPoint Nav point data table. +function ATIS:GetVOR(runway) + + for _,_vor in pairs(self.vor or {}) do + local vor=_vor --#ATIS.NavPoint + if vor.runway==nil or vor.runway==runway then + return vor + end + end + return nil +end + + +--- Transmission via RADIOQUEUE. +-- @param #ATIS self +-- @param #ATIS.Soundfile sound ATIS sound object. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @param #string subtitle Subtitle of the transmission. +-- @param #string path Path to sound file. Default self.soundpath. +function ATIS:Transmission(sound, interval, subtitle, path) + self.radioqueue:NewTransmission(sound.filename, sound.duration, path or self.soundpath, nil, interval, subtitle, self.subduration) +end + + --- Get weather of this mission from env.mission.weather variable. -- @param #ATIS self -- @return #table Clouds table which has entries "thickness", "density", "base", "iprecptns". From dc9f730d7d192feeb6cf00ad49fe1e84c2643f2b Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 9 Oct 2019 21:13:08 +0200 Subject: [PATCH 379/485] ATIS v0.4.0 ATIS UTILS AIRBASE --- Moose Development/Moose/Ops/ATIS.lua | 427 ++++++++++++++++---- Moose Development/Moose/Utilities/Utils.lua | 17 + Moose Development/Moose/Wrapper/Airbase.lua | 4 +- 3 files changed, 357 insertions(+), 91 deletions(-) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index f3c465a1f..e4027f45c 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -14,7 +14,8 @@ -- * Tower frequencies, -- * More than 180 voice overs, -- * Airbase names pronounced in locale accent (russian, US, french, arabic), --- * Option to present information in imperial or metric units. +-- * Option to present information in imperial or metric units, +-- * Frequencies/channels of nav aids (ILS, VOR, NDB, TACAN, RPMG, RSBN). -- -- === -- @@ -73,6 +74,10 @@ -- @field #number vor VOR frequency. -- @field #number rsbn RSBN channel. -- @field #table prmg PRMG channels (can be runway specific). +-- @field #boolean rwylength If true, give info on runway length. +-- @field #table runwaymag Table of magnetic runway headings. +-- @field #number runwaym2t Optional correction for magnetic to true runway heading conversion (and vice versa) in degrees. +-- @field #boolean windtrue Report true (from) heading of wind. Default is magnetic. -- @extends Core.Fsm#FSM --- Be informed! @@ -139,13 +144,75 @@ -- atisAbuDhabi:SetActiveRunway("L") -- -- The first two digits of the runway are determined by converting the *true* runway heading into its magnetic heading. The magnetic declination (or variation) is assumed to be constant on the given map. --- The magnatic declinatin can also be specified for the specific airport using the @{#ATIS.SetMagneticDeclination}(*magvar*). -- -- ## Tower Frequencies -- -- The tower frequency (or frequencies) can also be included in the ATIS information. However, there is no way to get these automatically. Therefore, it is necessary to manually specify them in the script via the -- @{#ATIS.SetTowerFrequencies}(*frequencies*) function. The parameter *frequencies* can be a plain number if only one frequency is necessary or it can be a table of frequencies. -- +-- ## Nav Aids +-- +-- Frequencies or channels of navigation aids can be specified by the user and are then provided as additional information. Unfortunately, it is **not possible** to aquire this information via the DCS API +-- we have access to. +-- +-- As they say, all road lead to Rome but (for me) the easiest way to obtain the available nav aids data of an airport, is to start a mission and click on an airport symbol. +-- +-- For example, the *AIRDROME DATA* for **Batumi** reads: +-- +-- * **TACAN** *16X* - set via @{#ATIS.SetTACAN} +-- * **VOR** *N/A* - set via @{#ATIS.SetVOR} +-- * **RSBN** *N/A* - set via @{#ATIS.SetRSBN} +-- * **ATC** *260.000*, *131.000*, *40.400*, *4.250* - set via @{#ATIS.SetTowerFrequencies} +-- * **Runways** *31* and *13* - automatic but can be set manually via @{#ATIS.SetRunwayHeadingsMagnetic} +-- * **ILS** *110.30* for runway *13* - set via @{#ATIS.AddILS} +-- * **PRMG** *N/A* - set via @{#ATIS.AddPRMG} +-- * **OUTER NDB** *N/A* - set via @{#ATIS.AddNDBinner} +-- * **INNER NDB** *N/A* - set via @{#ATIS.AddNDBinner} +-- +-- ![Banner Image](..\Presentations\ATIS\NavAid_Batumi.png) +-- +-- And the *AIRDROME DATA* for **Kobuleti** reads: +-- +-- * **TACAN** *67X* - set via @{#ATIS.SetTACAN} +-- * **VOR** *N/A* - set via @{#ATIS.SetVOR} +-- * **RSBN** *N/A* - set via @{#ATIS.SetRSBN} +-- * **ATC** *262.000*, *133.000*, *40.800*, *4.350* - set via @{#ATIS.SetTowerFrequencies} +-- * **Runways** *25* and *07* - automatic but can be set manually via @{#ATIS.SetRunwayHeadingsMagnetic} +-- * **ILS** *111.50* for runway *07* - set via @{#ATIS.AddILS} +-- * **PRMG** *N/A* - set via @{#ATIS.AddPRMG} +-- * **OUTER NDB** *870.00* - set via @{#ATIS.AddNDBinner} +-- * **INNER NDB** *490.00* - set via @{#ATIS.AddNDBinner} +-- +-- ![Banner Image](..\Presentations\ATIS\NavAid_Kobuleti.png) +-- +-- ### TACAN +-- +-- The [TACAN](https://en.wikipedia.org/wiki/Tactical_air_navigation_system) channel can be set via the @{#ATIS.SetTACAN}(*channel*) function, where *channel* is the TACAN channel. Band is always assumed to be X-ray. +-- +-- ### VOR +-- +-- The [VOR](https://en.wikipedia.org/wiki/VHF_omnidirectional_range) frequency can be set via the @{#ATIS.SetVOR}(*frequency*) function, where *frequency* is the VOR frequency. +-- +-- ### ILS +-- +-- The ILS frequency can be set via the @{#ATIS.AddILS}(*frequency*, *runway*) function, where *frequency* is the ILS frequency and *runway* the two letter string of the corresponding runway, e.g. "31". +-- If the parameter *runway* is omitted (nil) then the frequency is supposed to be valid for all runways of the airport. +-- +-- ### NDB +-- +-- Inner and outer [NDBs](https://en.wikipedia.org/wiki/Non-directional_beacon) can be set via the @{#ATIS.AddNDBinner}(*frequency*, *runway*) and @{#ATIS.AddNDBouter}(*frequency*, *runway*) functions, respectively. +-- +-- In both cases, the parameter *frequency* is the NDB frequency and *runway* the two letter string of the corresponding runway, e.g. "31". +-- If the parameter *runway* is omitted (nil) then the frequency is supposed to be valid for all runways of the airport. +-- +-- ## RSBN +-- +-- The RSBN channel can be set via the @{#ATIS.SetRSBN}(*channel*) function. +-- +-- ## PRMG +-- +-- The PRMG channel can be set via the @{#ATIS.AddPRMG}(*channel*, *runway*) function for each *runway*. +-- -- ## Unit System -- -- By default, information is given in imperial units, i.e. wind speed in knots, pressure in inches of mercury, visibility in Nautical miles, etc. @@ -223,6 +290,10 @@ ATIS = { tacan = nil, rsbn = nil, prmg = {}, + rwylength = nil, + runwaymag = {}, + runwaym2t = nil, + windtrue = nil, } --- NATO alphabet. @@ -258,6 +329,19 @@ ATIS.Alphabet = { [28] = "Zulu", } +--- Runway correction for converting true to magnetic heading. +-- @type ATIS.RunwayM2T +-- @field #number Caucasus 0° (East). +-- @field #number Nevada +12° (East). +-- @field #number Normandy -10° (West). +-- @field #number PersianGulf +2° (East). +ATIS.RunwayM2T={ + Caucasus=0, + Nevada=12, + Normany=-10, + PersianGulf=2, +} + --- Nav point data. -- @type ATIS.NavPoint -- @field #number frequency Nav point frequency. @@ -329,7 +413,7 @@ ATIS.Alphabet = { -- @field #ATIS.Soundfile InnerNDBFrequency -- @field #ATIS.Soundfile OuterNDBFrequency -- @field #ATIS.Soundfile PRGMChannel --- @field #ATIS.Soundfiel RSBNChannel +-- @field #ATIS.Soundfile RSBNChannel -- @field #ATIS.Soundfile RunwayLength -- @field #ATIS.Soundfile TACANChannel -- @field #ATIS.Soundfile VORFrequency @@ -402,7 +486,7 @@ ATIS.Sound = { --- ATIS class version. -- @field #string version -ATIS.version="0.3.3" +ATIS.version="0.4.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -450,6 +534,8 @@ function ATIS:New(airbasename, frequency, modulation) -- Defaults: self:SetSoundfilesPath() self:SetSubtitleDuration() + self:SetMagneticDeclination() + self:SetRunwayCorrectionMagnetic2True() -- Start State. self:SetStartState("Stopped") @@ -542,6 +628,41 @@ function ATIS:SetActiveRunway(runway) return self end +--- Give information on runway length. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetRunwayLength() + self.rwylength=true + return self +end + +--- Set magnetic runway headings as depicted on the runway, e.g. 13 for 130. +-- @param #ATIS self +-- @param #table headings Magnetic headings. Inverse (-180°) headings are added automatically. +-- @return #ATIS self +function ATIS:SetRunwayHeadingsMagnetic(headings) + if type(headings)=="table" then + -- nothing to do + else + headings={headings} + end + + for _,heading in pairs(headings) do + heading=tonumber(heading) + table.insert(self.runwaymag, heading) + + local head2=heading-18 + if head2<0 then + head2=head2+36 + end + + self:I(self.lid..string.format("Adding magnetic runway heading %d", head2)) + table.insert(self.runwaymag, head2) + end + + return self +end + --- Set duration how long subtitles are displayed. -- @param #ATIS self -- @param #number duration Duration in seconds. Default 10 seconds. @@ -593,11 +714,31 @@ end -- * Normandy -10 (West), year ~ 1944 -- * Persian Gulf +2 (East), year ~ 2011 -- +-- To get *true* from *magnetic* heading one has to add easterly or substract westerly variation, e.g +-- +-- A magnetic heading of 180° corresponds to a true heading of +-- +-- * 186° on the Caucaus map +-- * 192° on the Nevada map +-- * 170° on the Normany map +-- * 182° on the Persian Gulf map +-- +-- Likewise, to convert *magnetic* into *true* heading, one has to substract easterly and add westerly variation. +-- -- @param #ATIS self --- @param #number magvar Magnetic variation in degrees. +-- @param #number magvar Magnetic variation in degrees. Positive for easterly and negative for westerly variation. Default is magnatic declinaton of the used map, c.f. @{Utilities.UTils#UTILS.GetMagneticDeclination}. -- @return #ATIS self function ATIS:SetMagneticDeclination(magvar) - self.magvar=magvar + self.magvar=magvar or UTILS.GetMagneticDeclination() + return self +end + +--- Explicitly set correction of magnetic to true heading for runways. +-- @param #ATIS self +-- @param #number correction Correction of magnetic to true heading for runways in degrees. +-- @return #ATIS self +function ATIS:SetRunwayCorrectionMagnetic2True(correction) + self.runwaym2t=correction or ATIS.RunwayM2T[UTILS.GetDCSMap()] return self end @@ -625,21 +766,17 @@ end function ATIS:AddILS(frequency, runway) local ils={} --#ATIS.NavPoint ils.frequency=tonumber(frequency) - ils.runway=runway + ils.runway=runway and tostring(runway) or nil table.insert(self.ils, ils) return self end ---- Add VOR station. +--- Set VOR station. -- @param #ATIS self -- @param #number frequency VOR frequency. --- @param #string runway Runway. Default all (*nil*). -- @return #ATIS self -function ATIS:AddVOR(frequency, runway) - local vor={} --#ATIS.NavPoint - vor.frequency=tonumber(frequency) - vor.runway=runway - table.insert(self.vor, vor) +function ATIS:SetVOR(frequency) + self.vor=frequency return self end @@ -651,7 +788,7 @@ end function ATIS:AddNDBouter(frequency, runway) local ndb={} --#ATIS.NavPoint ndb.frequency=tonumber(frequency) - ndb.runway=runway + ndb.runway=runway and tostring(runway) or nil table.insert(self.ndbouter, ndb) return self end @@ -661,23 +798,58 @@ end -- @param #number frequency NDB frequency. -- @param #string runway Runway. Default all (*nil*). -- @return #ATIS self -function ATIS:AddNDBouter(frequency, runway) +function ATIS:AddNDBinner(frequency, runway) local ndb={} --#ATIS.NavPoint ndb.frequency=tonumber(frequency) - ndb.runway=runway + ndb.runway=runway and tostring(runway) or nil table.insert(self.ndbinner, ndb) return self end --- Set TACAN channel. -- @param #ATIS self --- @param #number tacan TACAN channel. +-- @param #number channel TACAN channel. -- @return #ATIS self -function ATIS:SetTACAN(tacan) - self.tacan=tacan +function ATIS:SetTACAN(channel) + self.tacan=channel return self end +--- Set RSBN channel. +-- @param #ATIS self +-- @param #number channel RSBN channel. +-- @return #ATIS self +function ATIS:SetRSBN(channel) + self.rsbn=channel + return self +end + +--- Add PRMG channel. +-- @param #ATIS self +-- @param #number channel PRMG channel. +-- @param #string runway Runway. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddPRMG(channel, runway) + local ndb={} --#ATIS.NavPoint + ndb.frequency=tonumber(channel) + ndb.runway=runway and tostring(runway) or nil + table.insert(self.prmg, ndb) + return self +end + + +--- Place marks with runway data on the F10 map. +-- @param #ATIS self +-- @param #boolean markall If true, mark all runways of the map. By default only the current ATIS runways are marked. +function ATIS:MarkRunways(markall) + local airbases=AIRBASE.GetAllAirbases() + for _,_airbase in pairs(airbases) do + local airbase=_airbase --Wrapper.Airbase#AIRBASE + if (not markall and airbase:GetName()==self.airbasename) or markall==true then + airbase:GetRunwayData(self.runwaym2t, true) + end + end +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status @@ -793,13 +965,31 @@ function ATIS:onafterBroadcast(From, Event, To) QNH=UTILS.Split(string.format("%.1f", qnh), ".") end end + + ------------ + --- Wind --- + ------------ + + -- Get wind direction and speed in m/s. + local windFrom, windSpeed=coord:GetWind(height) + + + local WINDFROM=string.format("%03d", windFrom-self.magvar) + local WINDSPEED=string.format("%d", UTILS.MpsToKnots(windSpeed)) + + if self.metric then + WINDSPEED=string.format("%d", windSpeed) + end -------------- --- Runway --- -------------- - -- Get runway based on wind direction. - local runway=self.airbase:GetActiveRunway(self.magvar).idx + -- Active runway data based on wind direction. + local runact=self.airbase:GetActiveRunway(self.runwaym2t) + + -- Active runway "31". + local runway=self:GetMagneticRunway(windFrom) or runact.idx -- Left or right in case there are two runways with the same heading. local rleft=false @@ -815,21 +1005,6 @@ function ATIS:onafterBroadcast(From, Event, To) rright=self.activerunway:lower():find("r") end - ------------ - --- Wind --- - ------------ - - -- Get wind direction and speed in m/s. - local windFrom, windSpeed=coord:GetWind(height) - - - local WINDFROM=string.format("%03d", windFrom) - local WINDSPEED=string.format("%d", UTILS.MpsToKnots(windSpeed)) - - if self.metric then - WINDSPEED=string.format("%d", windSpeed) - end - ------------ --- Time --- ------------ @@ -870,15 +1045,16 @@ function ATIS:onafterBroadcast(From, Event, To) --- Temperature --- ------------------- - -- Temperature in °C. + -- Temperature in °C (or °F). local temperature=coord:GetTemperature(height) - local TEMPERATURE=string.format("%d", temperature) - + -- Convert to °F. if self.TDegF then - TEMPERATURE=string.format("%d", UTILS.CelciusToFarenheit(temperature)) + temperature=UTILS.CelciusToFarenheit(temperature) end + local TEMPERATURE=string.format("%d", temperature) + --------------- --- Weather --- --------------- @@ -1115,7 +1291,7 @@ function ATIS:onafterBroadcast(From, Event, To) self.radioqueue:Number2Transmission(QNH[1]) self:Transmission(ATIS.Sound.Decimal, 0.2) self.radioqueue:Number2Transmission(QNH[2]) - self:Transmission(ATIS.Sound.QFE, 0.2) + self:Transmission(ATIS.Sound.QFE, 0.75) self.radioqueue:Number2Transmission(QFE[1]) self:Transmission(ATIS.Sound.Decimal, 0.2) self.radioqueue:Number2Transmission(QFE[2]) @@ -1136,6 +1312,9 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=string.format("Temperature %s °C", TEMPERATURE) end self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) + if temperature<0 then + self:Transmission(ATIS.Sound.Minus, 0.2) + end self.radioqueue:Number2Transmission(TEMPERATURE) if self.TDegF then self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) @@ -1172,16 +1351,50 @@ function ATIS:onafterBroadcast(From, Event, To) elseif rright then subtitle=subtitle.." Right" end - self:Transmission(ATIS.Sound.Knots, 1.0, subtitle) + self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) self.radioqueue:Number2Transmission(runway) if rleft then self:Transmission(ATIS.Sound.Left, 0.2) elseif rright then self:Transmission(ATIS.Sound.Right, 0.2) - self.radioqueue:NewTransmission("Right.ogg", 0.43, self.soundpath, nil, 0.2) end - --TODO: runway length + -- Runway length. + if self.rwylength then + + local length=runact.length + if not self.metric then + length=UTILS.MetersToFeet(length) + end + + -- Length in thousands and hundrets of ft/meters. + local L1000, L0100=self:_GetThousandsAndHundreds(length) + + -- Subtitle. + local subtitle=string.format("Runway length %d", length) + if self.metric then + subtitle=subtitle.." meters" + else + subtitle=subtitle.." feet" + end + + -- Transmitt. + self:Transmission(ATIS.Sound.RunwayLength, 1.0, subtitle) + if tonumber(L1000)>0 then + self.radioqueue:Number2Transmission(L1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(L0100)>0 then + self.radioqueue:Number2Transmission(L0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end + + end -- Tower frequency. if self.towerfrequency then @@ -1197,9 +1410,11 @@ function ATIS:onafterBroadcast(From, Event, To) for _,freq in pairs(self.towerfrequency) do local f=string.format("%.3f", freq) f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end self:Transmission(ATIS.Sound.MegaHertz, 0.2) end end @@ -1207,75 +1422,83 @@ function ATIS:onafterBroadcast(From, Event, To) -- ILS local ils=self:GetNavPoint(self.ils, runway) if ils then - subtitle=string.format("ILS frequency %.2f", ils.frequency) + subtitle=string.format("ILS frequency %.2f MHz", ils.frequency) self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) - local f=string.format("%.2f", vor.frequency) + local f=string.format("%.2f", ils.frequency) f=UTILS.Split(f, ".") self.radioqueue:Number2Transmission(f[1], nil, 0.5) - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) end -- Outer NDB local ndb=self:GetNavPoint(self.ndbouter, runway) if ndb then - subtitle=string.format("Outer NDB frequency %.2f", ndb.frequency) + subtitle=string.format("Outer NDB frequency %.2f MHz", ndb.frequency) self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) local f=string.format("%.2f", ndb.frequency) f=UTILS.Split(f, ".") self.radioqueue:Number2Transmission(f[1], nil, 0.5) - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) end -- Inner NDB local ndb=self:GetNavPoint(self.ndbinner, runway) if ndb then - subtitle=string.format("Inner NDB frequency %.2f", ndb.frequency) + subtitle=string.format("Inner NDB frequency %.2f MHz", ndb.frequency) self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) local f=string.format("%.2f", ndb.frequency) f=UTILS.Split(f, ".") self.radioqueue:Number2Transmission(f[1], nil, 0.5) - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) end -- VOR if self.vor then - subtitle=string.format("VOR frequency %.2f", self.vor) + subtitle=string.format("VOR frequency %.2f MHz", self.vor) self:Transmission(ATIS.Sound.VORFrequency, 1.0, subtitle) local f=string.format("%.2f", self.vor) f=UTILS.Split(f, ".") self.radioqueue:Number2Transmission(f[1], nil, 0.5) - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) end -- TACAN if self.tacan then subtitle=string.format("TACAN channel %dX", self.tacan) self:Transmission(ATIS.Sound.TACANChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(self.tacan, nil, 0.2) - self.radioqueue:NewTransmission(string.format("NATO Alphabet/Xray.ogg", NATO), 0.75, self.soundpath) + self.radioqueue:Number2Transmission(tostring(self.tacan), nil, 0.2) + self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2) end -- RSBN - if self.prmg then + if self.rsbn then subtitle=string.format("RSBN channel %d", self.rsbn) self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(self.rsbn, nil, 0.2) + self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) end -- PRMG local ndb=self:GetNavPoint(self.prmg, runway) if ndb then - subtitle=string.format("PRMG %d", ndb.frequency) + subtitle=string.format("PRMG channel %d", ndb.frequency) self:Transmission(ATIS.Sound.PRGMChannel, 1.0, subtitle) - local f=string.format("%.2f", vor.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) end end @@ -1284,6 +1507,31 @@ end -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get runway from user supplied magnetic heading. +-- @param #ATIS self +-- @param #number windfrom Wind direction (from) in degrees. +-- @return #string Runway magnetic heading divided by ten (and rounded). Eg, "13" for 130°. +function ATIS:GetMagneticRunway(windfrom) + + local diffmin=nil + local runway=nil + for _,heading in pairs(self.runwaymag) do + + local diff=UTILS.HdgDiff(windfrom, tonumber(heading)*10) + if diffmin==nil or diff Date: Fri, 11 Oct 2019 01:23:26 +0200 Subject: [PATCH 380/485] Update ATIS.lua --- Moose Development/Moose/Ops/ATIS.lua | 75 ++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index e4027f45c..b1f9df0d6 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -15,7 +15,7 @@ -- * More than 180 voice overs, -- * Airbase names pronounced in locale accent (russian, US, french, arabic), -- * Option to present information in imperial or metric units, --- * Frequencies/channels of nav aids (ILS, VOR, NDB, TACAN, RPMG, RSBN). +-- * Frequencies/channels of nav aids (ILS, VOR, NDB, TACAN, PRMG, RSBN). -- -- === -- @@ -121,6 +121,8 @@ -- -- With a unit set in the mission editor with name "Radio Relay Batumi". -- +-- **Note** that you should use a different relay unit for each ATIS! +-- -- By default, subtitles are displayed for 10 seconds. This can be changed using @{#ATIS.SetSubtitleDuration}(*duration*) with *duration* being the duration in seconds. -- -- ## Active Runway @@ -144,6 +146,7 @@ -- atisAbuDhabi:SetActiveRunway("L") -- -- The first two digits of the runway are determined by converting the *true* runway heading into its magnetic heading. The magnetic declination (or variation) is assumed to be constant on the given map. +-- An explicit correction factor can be set via @{#ATIS.SetRunwayCorrectionMagnetic2True}. -- -- ## Tower Frequencies -- @@ -187,20 +190,20 @@ -- -- ### TACAN -- --- The [TACAN](https://en.wikipedia.org/wiki/Tactical_air_navigation_system) channel can be set via the @{#ATIS.SetTACAN}(*channel*) function, where *channel* is the TACAN channel. Band is always assumed to be X-ray. +-- The TACtical Air Navigation system [(TACAN)](https://en.wikipedia.org/wiki/Tactical_air_navigation_system) channel can be set via the @{#ATIS.SetTACAN}(*channel*) function, where *channel* is the TACAN channel. Band is always assumed to be X-ray. -- -- ### VOR -- --- The [VOR](https://en.wikipedia.org/wiki/VHF_omnidirectional_range) frequency can be set via the @{#ATIS.SetVOR}(*frequency*) function, where *frequency* is the VOR frequency. +-- The Very high frequency Omni-directional Range [(VOR)](https://en.wikipedia.org/wiki/VHF_omnidirectional_range) frequency can be set via the @{#ATIS.SetVOR}(*frequency*) function, where *frequency* is the VOR frequency. -- -- ### ILS -- --- The ILS frequency can be set via the @{#ATIS.AddILS}(*frequency*, *runway*) function, where *frequency* is the ILS frequency and *runway* the two letter string of the corresponding runway, e.g. "31". +-- The Instrument Landing System [(ILS)](https://en.wikipedia.org/wiki/Instrument_landing_system) frequency can be set via the @{#ATIS.AddILS}(*frequency*, *runway*) function, where *frequency* is the ILS frequency and *runway* the two letter string of the corresponding runway, e.g. "31". -- If the parameter *runway* is omitted (nil) then the frequency is supposed to be valid for all runways of the airport. -- -- ### NDB -- --- Inner and outer [NDBs](https://en.wikipedia.org/wiki/Non-directional_beacon) can be set via the @{#ATIS.AddNDBinner}(*frequency*, *runway*) and @{#ATIS.AddNDBouter}(*frequency*, *runway*) functions, respectively. +-- Inner and outer Non-Directional (radio) Beacons [NDBs](https://en.wikipedia.org/wiki/Non-directional_beacon) can be set via the @{#ATIS.AddNDBinner}(*frequency*, *runway*) and @{#ATIS.AddNDBouter}(*frequency*, *runway*) functions, respectively. -- -- In both cases, the parameter *frequency* is the NDB frequency and *runway* the two letter string of the corresponding runway, e.g. "31". -- If the parameter *runway* is omitted (nil) then the frequency is supposed to be valid for all runways of the airport. @@ -245,20 +248,25 @@ -- atisBatumi:Start() -- -- ## Nevada: Nellis AFB --- --- -- ATIS Nellis AFB on 270.100 MHz AM. --- atisNellis=ATIS:New(AIRBASE.Nevada.Nellis_AFB, 270.100) +-- +-- -- ATIS Nellis AFB on 270.10 MHz AM. +-- atisNellis=ATIS:New(AIRBASE.Nevada.Nellis_AFB, 270.1) -- atisNellis:SetRadioRelayUnitName("Radio Relay Nellis") -- atisNellis:SetActiveRunway("21L") -- atisNellis:SetTowerFrequencies({327.000, 132.550}) +-- atisNellis:SetTACAN(12) +-- atisNellis:AddILS(109.1, "21") -- atisNellis:Start() -- -- ## Persian Gulf: Abu Dhabi International Airport -- +-- -- ATIS Abu Dhabi International on 125.1 MHz AM. -- atisAbuDhabi=ATIS:New(AIRBASE.PersianGulf.Abu_Dhabi_International_Airport, 125.1) -- atisAbuDhabi:SetRadioRelayUnitName("Radio Relay Abu Dhabi International Airport") -- atisAbuDhabi:SetMetricUnits() -- atisAbuDhabi:SetActiveRunway("L") +-- atisAbuDhabi:SetTowerFrequencies({250.5, 119.2}) +-- atisAbuDhabi:SetVOR(114.25) -- atisAbuDhabi:Start() -- -- @@ -400,6 +408,8 @@ ATIS.RunwayM2T={ -- @field #ATIS.Soundfile QNH -- @field #ATIS.Soundfile Rain -- @field #ATIS.Soundfile Right +-- @field #ATIS.Soundfile Snow +-- @field #ATIS.Soundfile SnowStorm -- @field #ATIS.Soundfile Temperature -- @field #ATIS.Soundfile Thousand -- @field #ATIS.Soundfile ThunderStorm @@ -412,7 +422,7 @@ ATIS.RunwayM2T={ -- @field #ATIS.Soundfile ILSFrequency -- @field #ATIS.Soundfile InnerNDBFrequency -- @field #ATIS.Soundfile OuterNDBFrequency --- @field #ATIS.Soundfile PRGMChannel +-- @field #ATIS.Soundfile PRMGChannel -- @field #ATIS.Soundfile RSBNChannel -- @field #ATIS.Soundfile RunwayLength -- @field #ATIS.Soundfile TACANChannel @@ -465,6 +475,8 @@ ATIS.Sound = { QNH={filename="QNH.ogg", duration=0.71}, Rain={filename="Rain.ogg", duration=0.41}, Right={filename="Right.ogg", duration=0.44}, + Snow={filename="Snow.ogg", duration=0.48}, + SnowStorm={filename="SnowStorm.ogg", duration=0.82}, Temperature={filename="Temperature.ogg", duration=0.64}, Thousand={filename="Thousand.ogg", duration=0.55}, ThunderStorm={filename="ThunderStorm.ogg", duration=0.81}, @@ -480,7 +492,7 @@ ATIS.Sound = { RunwayLength={filename="RunwayLength.ogg", duration=0.91}, VORFrequency={filename="VORFrequency.ogg", duration=1.38}, TACANChannel={filename="TACANChannel.ogg", duration=0.88}, - PRGMChannel={filename="PRGMChannel.ogg", duration=1.12}, + PRMGChannel={filename="PRMGChannel.ogg", duration=1.18}, RSBNChannel={filename="RSBNChannel.ogg", duration=1.14}, } @@ -742,6 +754,14 @@ function ATIS:SetRunwayCorrectionMagnetic2True(correction) return self end +--- Set wind direction (from) to be reported as *true* heading. Default is magnetic. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetReportWindTrue() + self.windtrue=true + return self +end + --- Set time local difference with respect to Zulu time. -- Default is per map: -- @@ -973,8 +993,13 @@ function ATIS:onafterBroadcast(From, Event, To) -- Get wind direction and speed in m/s. local windFrom, windSpeed=coord:GetWind(height) + -- Wind in magnetic or true. + local magvar=self.magvar + if self.windtrue then + magvar=0 + end - local WINDFROM=string.format("%03d", windFrom-self.magvar) + local WINDFROM=string.format("%03d", windFrom-magvar) local WINDSPEED=string.format("%d", UTILS.MpsToKnots(windSpeed)) if self.metric then @@ -1053,7 +1078,7 @@ function ATIS:onafterBroadcast(From, Event, To) temperature=UTILS.CelciusToFarenheit(temperature) end - local TEMPERATURE=string.format("%d", temperature) + local TEMPERATURE=string.format("%d", math.abs(temperature)) --------------- --- Weather --- @@ -1107,7 +1132,7 @@ function ATIS:onafterBroadcast(From, Event, To) local cloudceil=clouds.base+clouds.thickness local clouddens=clouds.density - -- Precepitation: 0=None, 1=Rain, 2=Thunderstorm. + -- Precepitation: 0=None, 1=Rain, 2=Thunderstorm, 3=Snow, 4=Snowstorm. local precepitation=tonumber(clouds.iprecptns) local CLOUDBASE=string.format("%d", UTILS.MetersToFeet(cloudbase)) @@ -1244,6 +1269,12 @@ function ATIS:onafterBroadcast(From, Event, To) end wpsub=wpsub.." thunderstorm" wp=true + elseif precepitation==3 then + wpsub=wpsub.." snow" + wp=true + elseif precepitation==4 then + wpsub=wpsub.." snowstorm" + wp=true end if fog then if wp then @@ -1267,6 +1298,10 @@ function ATIS:onafterBroadcast(From, Event, To) self:Transmission(ATIS.Sound.Rain, 0.5) elseif precepitation==2 then self:Transmission(ATIS.Sound.ThunderStorm, 0.5) + elseif precepitation==3 then + self:Transmission(ATIS.Sound.Snow, 0.5) + elseif precepitation==4 then + self:Transmission(ATIS.Sound.SnowStorm, 0.5) end if fog then self:Transmission(ATIS.Sound.Fog, 0.5) @@ -1307,9 +1342,17 @@ function ATIS:onafterBroadcast(From, Event, To) -- Temperature if self.TDegF then - subtitle=string.format("Temperature %s °F", TEMPERATURE) + if temperature<0 then + subtitle=string.format("Temperature -%s °F", TEMPERATURE) + else + subtitle=string.format("Temperature %s °F", TEMPERATURE) + end else - subtitle=string.format("Temperature %s °C", TEMPERATURE) + if temperature<0 then + subtitle=string.format("Temperature -%s °C", TEMPERATURE) + else + subtitle=string.format("Temperature %s °C", TEMPERATURE) + end end self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) if temperature<0 then @@ -1497,7 +1540,7 @@ function ATIS:onafterBroadcast(From, Event, To) local ndb=self:GetNavPoint(self.prmg, runway) if ndb then subtitle=string.format("PRMG channel %d", ndb.frequency) - self:Transmission(ATIS.Sound.PRGMChannel, 1.0, subtitle) + self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) end From 1cea10f03ad4b3ab497c9ccb6f45b1d62cad8dd0 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 14 Oct 2019 14:34:53 +0200 Subject: [PATCH 381/485] Update Point.lua Fixed bug in GetClosestAirbase() function. --- Moose Development/Moose/Core/Point.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 7c600e697..fa8d58d65 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1269,7 +1269,7 @@ do -- COORDINATE for _,_airbase in pairs(airbases) do local airbase=_airbase --Wrapper.Airbase#AIRBASE if airbase then - local category=airbase:GetCategory() + local category=airbase:GetAirbaseCategory() if Category and Category==category or Category==nil then -- Distance to airbase. From f5f1c9043b52466d3d9414388ce5f45d82fe37f0 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 18 Oct 2019 21:15:53 +0200 Subject: [PATCH 382/485] ATIS v0.4.1 ATIS v0.4.1 - Fixed schedulers being deallocated by lua garbage collector. RADIOQUEUE - Added alias optional name. - Minor. --- Moose Development/Moose/Core/RadioQueue.lua | 128 ++++-- Moose Development/Moose/Ops/ATIS.lua | 440 ++++++++++---------- 2 files changed, 314 insertions(+), 254 deletions(-) diff --git a/Moose Development/Moose/Core/RadioQueue.lua b/Moose Development/Moose/Core/RadioQueue.lua index 4dd101b95..dabaf50f3 100644 --- a/Moose Development/Moose/Core/RadioQueue.lua +++ b/Moose Development/Moose/Core/RadioQueue.lua @@ -17,33 +17,45 @@ -- -- @type RADIOQUEUE -- @field #string ClassName Name of the class "RADIOQUEUE". +-- @field #boolean Debug Debug mode. More info. -- @field #string lid ID for dcs.log. -- @field #number frequency The radio frequency in Hz. -- @field #number modulation The radio modulation. Either radio.modulation.AM or radio.modulation.FM. -- @field Core.Scheduler#SCHEDULER scheduler The scheduler. -- @field #string RQid The radio queue scheduler ID. --- @field #table queue The queue of transmissions. +-- @field #table queue The queue of transmissions. +-- @field #string alias Name of the radio. +-- @field #number dt Time interval in seconds for checking the radio queue. +-- @field #number delay Time delay before starting the radio queue. -- @field #number Tlast Time (abs) when the last transmission finished. -- @field Core.Point#COORDINATE sendercoord Coordinate from where transmissions are broadcasted. -- @field #number sendername Name of the sending unit or static. --- @field Wrapper.Positionable#POSITIONABLE positionable The positionable to broadcast the message. +-- @field #boolean senderinit Set frequency was initialized. -- @field #number power Power of radio station in Watts. Default 100 W. -- @field #table numbers Table of number transmission parameters. +-- @field #boolean checking Scheduler is checking the radio queue. +-- @field #boolean schedonce Call ScheduleOnce instead of normal scheduler. -- @extends Core.Base#BASE RADIOQUEUE = { - ClassName = "RADIOQUEUE", - lid=nil, - frequency=nil, - modulation=nil, - scheduler=nil, - RQid=nil, - queue={}, - Tlast=nil, - sendercoord=nil, - sendername=nil, - positionable=nil, - power=100, - numbers={}, + ClassName = "RADIOQUEUE", + Debug = nil, + lid = nil, + frequency = nil, + modulation = nil, + scheduler = nil, + RQid = nil, + queue = {}, + alias = nil, + dt = nil, + delay = nil, + Tlast = nil, + sendercoord = nil, + sendername = nil, + senderinit = nil, + power = 100, + numbers = {}, + checking = nil, + schedonce = nil, } --- Radio queue transmission data. @@ -63,13 +75,16 @@ RADIOQUEUE = { -- @param #RADIOQUEUE self -- @param #number frequency The radio frequency in MHz. -- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM. +-- @param #string alias (Optional) Name of the radio queue. -- @return #RADIOQUEUE self The RADIOQUEUE object. -function RADIOQUEUE:New(frequency, modulation) +function RADIOQUEUE:New(frequency, modulation, alias) -- Inherit base local self=BASE:Inherit(self, BASE:New()) -- #RADIOQUEUE - self.lid="RADIOQUEUE | " + self.alias=alias or "My Radio" + + self.lid=string.format("RADIOQUEUE %s | ", self.alias) if frequency==nil then self:E(self.lid.."ERROR: No frequency specified as first parameter!") @@ -82,7 +97,7 @@ function RADIOQUEUE:New(frequency, modulation) -- Modulation. self.modulation=modulation or radio.modulation.AM - -- Scheduler + -- Scheduler. self.scheduler=SCHEDULER:New() self.scheduler:NoTrace() @@ -96,13 +111,19 @@ end -- @return #RADIOQUEUE self The RADIOQUEUE object. function RADIOQUEUE:Start(delay, dt) - delay=delay or 1 + self.delay=delay or 1 - dt=dt or 0.01 + self.dt=dt or 0.01 - self:I(self.lid..string.format("Starting RADIOQUEUE on Frequency %.2f MHz [modulation=%d] in %.1f seconds (dt=%.3f sec)", self.frequency/1000000, self.modulation, delay, dt)) + self:I(self.lid..string.format("Starting RADIOQUEUE %s on Frequency %.2f MHz [modulation=%d] in %.1f seconds (dt=%.3f sec)", self.alias, self.frequency/1000000, self.modulation, delay, dt)) - self.RQid=self.scheduler:Schedule(self, self._CheckRadioQueue, {}, delay, dt) + + if self.schedonce then + self:_CheckRadioQueueDelayed(self.delta) + else + --self.RQid=self.scheduler:Schedule(self, self._CheckRadioQueue, {}, delay, dt) + self.RQid=self.scheduler:Schedule(nil, RADIOQUEUE._CheckRadioQueue, {self}, delay, dt) + end return self end @@ -178,7 +199,10 @@ function RADIOQUEUE:AddTransmission(transmission) -- Add to queue. table.insert(self.queue, transmission) - --TODO: Start scheduler. + -- Start checking. + if self.schedonce and not self.checking then + self:_CheckRadioQueueDelayed() + end return self end @@ -298,13 +322,22 @@ function RADIOQUEUE:Broadcast(transmission) -- 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=self.frequency, -- Frequency in Hz. - modulation=self.modulation, - }} + + if not self.senderinit then + + -- Command to set the Frequency for the transmission. + local commandFrequency={ + id="SetFrequency", + params={ + frequency=self.frequency, -- Frequency in Hz. + modulation=self.modulation, + }} + + -- Set commend for frequency + sender:SetCommand(commandFrequency) + + self.senderinit=true + end -- Command to tranmit the call. local commandTransmit={ @@ -314,15 +347,14 @@ function RADIOQUEUE:Broadcast(transmission) duration=transmission.subduration, subtitle=transmission.subtitle or "", loop=false, - }} - - -- Set commend for frequency - sender:SetCommand(commandFrequency) + }} -- Set command for radio transmission. sender:SetCommand(commandTransmit) - --MESSAGE:New(string.format("transmissing file %s duration=%.2f sec, subtitle=%s", filename, transmission.duration, transmission.subtitle or ""), 5, "RADIOQUEUE"):ToAll() + -- Debug message. + local text=string.format("file=%s, freq=%.2f MHz, duration=%.2f sec, subtitle=%s", filename, self.frequency/1000000, transmission.duration, transmission.subtitle or "") + MESSAGE:New(text, 2, "RADIOQUEUE "..self.alias):ToAllIf(self.Debug) else @@ -349,20 +381,35 @@ function RADIOQUEUE:Broadcast(transmission) if vec3 then self:T("Sending") self:T( { filename = filename, vec3 = vec3, modulation = self.modulation, frequency = self.frequency, power = self.power } ) - --MESSAGE:New(string.format("transmissing file %s duration=%.2f sec, subtitle=%s", filename, transmission.duration, transmission.subtitle or ""), 5, "RADIOQUEUE (trigger)"):ToAll() + + -- Trigger transmission. trigger.action.radioTransmission(filename, vec3, self.modulation, false, self.frequency, self.power) + + -- Debug message. + local text=string.format("file=%s, freq=%.2f MHz, duration=%.2f sec, subtitle=%s", filename, self.frequency/1000000, transmission.duration, transmission.subtitle or "") + MESSAGE:New(string.format(text, filename, transmission.duration, transmission.subtitle or ""), 5, "RADIOQUEUE "..self.alias):ToAllIf(self.Debug) end end end +--- Start checking the radio queue. +-- @param #RADIOQUEUE self +-- @param #number delay Delay in seconds before checking. +function RADIOQUEUE:_CheckRadioQueueDelayed(delay) + self.checking=true + self:ScheduleOnce(delay or self.dt, RADIOQUEUE._CheckRadioQueue, self) +end + --- Check radio queue for transmissions to be broadcasted. -- @param #RADIOQUEUE self function RADIOQUEUE:_CheckRadioQueue() + --env.info("FF check radio queue "..self.alias) -- Check if queue is empty. if #self.queue==0 then - --TODO: stop scheduler. + -- Queue is now empty. Nothing to else to do. + self.checking=false return end @@ -445,7 +492,12 @@ function RADIOQUEUE:_CheckRadioQueue() if remove then table.remove(self.queue, remove) end - + + -- Check queue. + if self.schedonce then + self:_CheckRadioQueueDelayed() + end + end --- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index b1f9df0d6..59aa48a24 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -1,5 +1,5 @@ --- **Ops** - (R2.5) - Automatic Terminal Information Service (ATIS). --- +-- -- === -- -- **Main Features:** @@ -29,9 +29,9 @@ -- === -- -- ## Missions: Example missions can be found [here](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20ATIS) --- +-- -- === --- +-- -- ## Sound files: Check out the pinned messages in the Moose discord #ops-atis channel. -- -- === @@ -64,7 +64,7 @@ -- @field #number subduration Duration how long subtitles are displayed in seconds. -- @field #boolean metric If true, use metric units. If false, use imperial (default). -- @field #boolean PmmHg If true, give pressure in millimeters of Mercury. Default is inHg for imperial and hecto Pascal (=mili Bars) for metric units. --- @field #boolean TDegF If true, give temperature in degrees Fahrenheit. Default is in degrees Celsius independent of chosen unit system. +-- @field #boolean TDegF If true, give temperature in degrees Fahrenheit. Default is in degrees Celsius independent of chosen unit system. -- @field #number zuludiff Time difference local vs. zulu in hours. -- @field #number magvar Magnetic declination/variation at the airport in degrees. -- @field #table ils Table of ILS frequencies (can be runway specific). @@ -80,88 +80,88 @@ -- @field #boolean windtrue Report true (from) heading of wind. Default is magnetic. -- @extends Core.Fsm#FSM ---- Be informed! +--- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde -- -- === -- -- ![Banner Image](..\Presentations\ATIS\ATIS_Main.png) -- -- # The ATIS Concept --- +-- -- Automatic terminal information service, or ATIS, is a continuous broadcast of recorded aeronautical information in busier terminal areas, i.e. airports and their immediate surroundings. -- ATIS broadcasts contain essential information, such as current weather information, active runways, and any other information required by the pilots. --- +-- -- # DCS Limitations --- +-- -- Unfortunately, the DCS API only allow to get the temperature, pressure as well as wind direction and speed. Therefore, some other information such as cloud coverage, base and ceiling are not available -- when dynamic weather is used. --- +-- -- # Scripting --- +-- -- The lua script to create an ATIS at an airport is pretty easy: --- +-- -- -- ATIS at Batumi Airport on 143.00 MHz AM. -- atisBatumi=ATIS:New("Batumi", 143.00) -- atisBatumi:Start() --- +-- -- The @{#ATIS.New}(*airbasename*, *frequency*) creates a new ATIS object. The parameter *airbasename* is the name of the airbase or airport. Note that this has to be spelled exactly as in the DCS mission editor. -- The parameter *frequency* is the frequency the ATIS broadcasts in MHz. --- --- Broadcasting is started via the @{#ATIS.Start}() function. The start can be delayed by useing @{#ATIS.__Start}(*delay*), where *delay* is the delay in seconds. --- +-- +-- Broadcasting is started via the @{#ATIS.Start}() function. The start can be delayed by useing @{#ATIS.__Start}(*delay*), where *delay* is the delay in seconds. +-- -- ## Subtitles --- --- Currently, DCS allows for displaying subtitles of radio transmissions only from airborne units, i.e. airplanes and helicopters. Therefore, if you want to have subtitles, it is necessary to place an +-- +-- Currently, DCS allows for displaying subtitles of radio transmissions only from airborne units, i.e. airplanes and helicopters. Therefore, if you want to have subtitles, it is necessary to place an -- additonal aircraft on the ATIS airport and set it to uncontrolled. This unit can then function as a radio relay to transmit messages with subtitles. These subtitles will only be displayed, if the -- player has tuned in the correct ATIS frequency. --- +-- -- Radio transmissions via an airborne unit can be set via the @{#ATIS.SetRadioRelayUnitName}(*unitname*) function, where the parameter *unitname* is the name of the unit passed as string, e.g. --- +-- -- atisBatumi:SetRadioRelayUnitName("Radio Relay Batumi") --- +-- -- With a unit set in the mission editor with name "Radio Relay Batumi". --- +-- -- **Note** that you should use a different relay unit for each ATIS! --- +-- -- By default, subtitles are displayed for 10 seconds. This can be changed using @{#ATIS.SetSubtitleDuration}(*duration*) with *duration* being the duration in seconds. --- +-- -- ## Active Runway --- +-- -- By default, the currently active runway is determined automatically by analysing the wind direction. Therefore, you should obviously set the wind speed to be greater zero in your mission. --- +-- -- Note however, there are a few special cases, where automatic detection does not yield the correct or desired result. -- For example, there are airports with more than one runway facing in the same direction (usually denoted left and right). In this case, there is obviously no *unique* result depending on the wind vector. --- +-- -- If the automatic runway detection fails, the active runway can be specified manually in the script via the @{#ATIS.SetActiveRunway}(*runway*) function. -- The parameter *runway* is a string which can be used to specify the runway heading and, if applicable, whether the left or right runway is in use. --- +-- -- For example, setting runway 21L would be --- +-- -- atisNellis:SetActiveRunway("21L") --- +-- -- The script will examine the string and search for the characters "L" (left) and "R" (right). --- +-- -- If only left or right should be set and the direction determined by the wind vector, the runway heading can be left out, e.g. --- +-- -- atisAbuDhabi:SetActiveRunway("L") --- +-- -- The first two digits of the runway are determined by converting the *true* runway heading into its magnetic heading. The magnetic declination (or variation) is assumed to be constant on the given map. -- An explicit correction factor can be set via @{#ATIS.SetRunwayCorrectionMagnetic2True}. --- +-- -- ## Tower Frequencies --- +-- -- The tower frequency (or frequencies) can also be included in the ATIS information. However, there is no way to get these automatically. Therefore, it is necessary to manually specify them in the script via the -- @{#ATIS.SetTowerFrequencies}(*frequencies*) function. The parameter *frequencies* can be a plain number if only one frequency is necessary or it can be a table of frequencies. --- +-- -- ## Nav Aids --- +-- -- Frequencies or channels of navigation aids can be specified by the user and are then provided as additional information. Unfortunately, it is **not possible** to aquire this information via the DCS API -- we have access to. --- +-- -- As they say, all road lead to Rome but (for me) the easiest way to obtain the available nav aids data of an airport, is to start a mission and click on an airport symbol. --- +-- -- For example, the *AIRDROME DATA* for **Batumi** reads: --- +-- -- * **TACAN** *16X* - set via @{#ATIS.SetTACAN} -- * **VOR** *N/A* - set via @{#ATIS.SetVOR} -- * **RSBN** *N/A* - set via @{#ATIS.SetRSBN} @@ -171,11 +171,11 @@ -- * **PRMG** *N/A* - set via @{#ATIS.AddPRMG} -- * **OUTER NDB** *N/A* - set via @{#ATIS.AddNDBinner} -- * **INNER NDB** *N/A* - set via @{#ATIS.AddNDBinner} --- +-- -- ![Banner Image](..\Presentations\ATIS\NavAid_Batumi.png) --- +-- -- And the *AIRDROME DATA* for **Kobuleti** reads: --- +-- -- * **TACAN** *67X* - set via @{#ATIS.SetTACAN} -- * **VOR** *N/A* - set via @{#ATIS.SetVOR} -- * **RSBN** *N/A* - set via @{#ATIS.SetRSBN} @@ -183,70 +183,70 @@ -- * **Runways** *25* and *07* - automatic but can be set manually via @{#ATIS.SetRunwayHeadingsMagnetic} -- * **ILS** *111.50* for runway *07* - set via @{#ATIS.AddILS} -- * **PRMG** *N/A* - set via @{#ATIS.AddPRMG} --- * **OUTER NDB** *870.00* - set via @{#ATIS.AddNDBinner} +-- * **OUTER NDB** *870.00* - set via @{#ATIS.AddNDBouter} -- * **INNER NDB** *490.00* - set via @{#ATIS.AddNDBinner} --- +-- -- ![Banner Image](..\Presentations\ATIS\NavAid_Kobuleti.png) --- +-- -- ### TACAN --- +-- -- The TACtical Air Navigation system [(TACAN)](https://en.wikipedia.org/wiki/Tactical_air_navigation_system) channel can be set via the @{#ATIS.SetTACAN}(*channel*) function, where *channel* is the TACAN channel. Band is always assumed to be X-ray. --- +-- -- ### VOR --- +-- -- The Very high frequency Omni-directional Range [(VOR)](https://en.wikipedia.org/wiki/VHF_omnidirectional_range) frequency can be set via the @{#ATIS.SetVOR}(*frequency*) function, where *frequency* is the VOR frequency. --- +-- -- ### ILS --- +-- -- The Instrument Landing System [(ILS)](https://en.wikipedia.org/wiki/Instrument_landing_system) frequency can be set via the @{#ATIS.AddILS}(*frequency*, *runway*) function, where *frequency* is the ILS frequency and *runway* the two letter string of the corresponding runway, e.g. "31". -- If the parameter *runway* is omitted (nil) then the frequency is supposed to be valid for all runways of the airport. --- +-- -- ### NDB --- +-- -- Inner and outer Non-Directional (radio) Beacons [NDBs](https://en.wikipedia.org/wiki/Non-directional_beacon) can be set via the @{#ATIS.AddNDBinner}(*frequency*, *runway*) and @{#ATIS.AddNDBouter}(*frequency*, *runway*) functions, respectively. --- +-- -- In both cases, the parameter *frequency* is the NDB frequency and *runway* the two letter string of the corresponding runway, e.g. "31". -- If the parameter *runway* is omitted (nil) then the frequency is supposed to be valid for all runways of the airport. --- +-- -- ## RSBN --- +-- -- The RSBN channel can be set via the @{#ATIS.SetRSBN}(*channel*) function. --- +-- -- ## PRMG --- +-- -- The PRMG channel can be set via the @{#ATIS.AddPRMG}(*channel*, *runway*) function for each *runway*. --- +-- -- ## Unit System --- +-- -- By default, information is given in imperial units, i.e. wind speed in knots, pressure in inches of mercury, visibility in Nautical miles, etc. --- +-- -- If you prefer metric units, you can enable this via the @{#ATIS.SetMetricUnits}() function, --- +-- -- atisBatumi:SetMetricUnits() --- +-- -- With this, wind speed is given in meters per second, pressure in hecto Pascal (mbar), visibility in kilometers etc. --- +-- -- # Sound Files --- --- More than 180 individual sound files have been created using a text-to-speech program. All ATIS information is given with en-US accent. --- +-- +-- More than 180 individual sound files have been created using a text-to-speech program. All ATIS information is given with en-US accent. +-- -- Check out the pinned messages in the Moose discord #ops-atis channel. --- +-- -- To include the files, open the mission (.miz) file with, e.g., 7-zip. Then just drag-n-drop the file into the miz. --- +-- -- ![Banner Image](..\Presentations\ATIS\ATIS_SoundFolder.png) --- +-- -- **Note** that the default folder name is *ATIS Soundfiles/*. If you want to change it, you can use the @{#ATIS.SetSoundfilesPath}(*path*), where *path* is the path of the directory. This must end with a slash "/"! --- +-- -- # Examples --- +-- -- ## Caucasus: Batumi --- +-- -- -- ATIS Batumi Airport on 143.00 MHz AM. -- atisBatumi=ATIS:New(AIRBASE.Caucasus.Batumi, 143.00) -- atisBatumi:SetRadioRelayUnitName("Radio Relay Batumi") -- atisBatumi:Start() --- +-- -- ## Nevada: Nellis AFB -- -- -- ATIS Nellis AFB on 270.10 MHz AM. @@ -257,7 +257,7 @@ -- atisNellis:SetTACAN(12) -- atisNellis:AddILS(109.1, "21") -- atisNellis:Start() --- +-- -- ## Persian Gulf: Abu Dhabi International Airport -- -- -- ATIS Abu Dhabi International on 125.1 MHz AM. @@ -268,7 +268,7 @@ -- atisAbuDhabi:SetTowerFrequencies({250.5, 119.2}) -- atisAbuDhabi:SetVOR(114.25) -- atisAbuDhabi:Start() --- +-- -- -- @field #ATIS ATIS = { @@ -496,9 +496,14 @@ ATIS.Sound = { RSBNChannel={filename="RSBNChannel.ogg", duration=1.14}, } + +--- ATIS table containing all defined ATISes. +-- @field #table _ATIS +_ATIS={} + --- ATIS class version. -- @field #string version -ATIS.version="0.4.0" +ATIS.version="0.4.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -523,26 +528,29 @@ ATIS.version="0.4.0" -- @return #ATIS self function ATIS:New(airbasename, frequency, modulation) - -- Inherit everything from WAREHOUSE class. + -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #ATIS - + self.airbasename=airbasename self.airbase=AIRBASE:FindByName(airbasename) - + if self.airbase==nil then self:E("ERROR: Airbase %s for ATIS could not be found!", tostring(airbasename)) end - + -- Default freq and modulation. self.frequency=frequency or 143.00 self.modulation=modulation or 0 - + -- Get map. self.theatre=env.mission.theatre -- Set some string id for output to DCS.log file. self.lid=string.format("ATIS %s | ", self.airbasename) + -- This is just to hinder the garbage collector deallocating the ATIS object. + _ATIS[#_ATIS+1]=self + -- Defaults: self:SetSoundfilesPath() self:SetSubtitleDuration() @@ -558,7 +566,7 @@ function ATIS:New(airbasename, frequency, modulation) self:AddTransition("*", "Status", "*") -- Update status. self:AddTransition("*", "Broadcast", "*") -- Broadcast ATIS message. self:AddTransition("*", "CheckQueue", "*") -- Check if radio queue is empty. - + ------------------------ --- Pseudo Functions --- ------------------------ @@ -623,7 +631,7 @@ end function ATIS:SetTowerFrequencies(freqs) if type(freqs)=="table" then -- nothing to do - else + else freqs={freqs} end self.towerfrequency=freqs @@ -655,23 +663,23 @@ end function ATIS:SetRunwayHeadingsMagnetic(headings) if type(headings)=="table" then -- nothing to do - else + else headings={headings} end - + for _,heading in pairs(headings) do heading=tonumber(heading) table.insert(self.runwaymag, heading) - + local head2=heading-18 if head2<0 then head2=head2+36 end - + self:I(self.lid..string.format("Adding magnetic runway heading %d", head2)) table.insert(self.runwaymag, head2) end - + return self end @@ -701,7 +709,7 @@ function ATIS:SetImperialUnits() end --- Set pressure unit to millimeters of mercury (mmHg). --- Default is inHg for imperial and hPa (=mBar) for metric units. +-- Default is inHg for imperial and hPa (=mBar) for metric units. -- @param #ATIS self -- @return #ATIS self function ATIS:SetPressureMillimetersMercury() @@ -718,25 +726,25 @@ function ATIS:SetTemperatureFahrenheit() end --- Set magnetic declination/variation at the airport. --- +-- -- Default is per map: --- +-- -- * Caucasus +6 (East), year ~ 2011 -- * NTTR +12 (East), year ~ 2011 -- * Normandy -10 (West), year ~ 1944 -- * Persian Gulf +2 (East), year ~ 2011 --- +-- -- To get *true* from *magnetic* heading one has to add easterly or substract westerly variation, e.g --- --- A magnetic heading of 180° corresponds to a true heading of --- +-- +-- A magnetic heading of 180° corresponds to a true heading of +-- -- * 186° on the Caucaus map -- * 192° on the Nevada map -- * 170° on the Normany map -- * 182° on the Persian Gulf map --- +-- -- Likewise, to convert *magnetic* into *true* heading, one has to substract easterly and add westerly variation. --- +-- -- @param #ATIS self -- @param #number magvar Magnetic variation in degrees. Positive for easterly and negative for westerly variation. Default is magnatic declinaton of the used map, c.f. @{Utilities.UTils#UTILS.GetMagneticDeclination}. -- @return #ATIS self @@ -764,12 +772,12 @@ end --- Set time local difference with respect to Zulu time. -- Default is per map: --- +-- -- * Caucasus +4 -- * Nevada -7 -- * Normandy +1 -- * Persian Gulf +4 --- +-- -- @param #ATIS self -- @param #number delta Time difference in hours. -- @return #ATIS self @@ -881,16 +889,16 @@ function ATIS:onafterStart(From, Event, To) -- Info. self:I(self.lid..string.format("Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation)) - + -- Start radio queue. - self.radioqueue=RADIOQUEUE:New(self.frequency, self.modulation) - + self.radioqueue=RADIOQUEUE:New(self.frequency, self.modulation, string.format("ATIS %s", self.airbasename)) + -- Send coordinate is airbase coord. self.radioqueue:SetSenderCoordinate(self.airbase:GetCoordinate()) - + -- Set relay unit if we have one. self.radioqueue:SetSenderUnitName(self.relayunitname) - + -- Init numbers. self.radioqueue:SetDigit(0, ATIS.Sound.N0.filename, ATIS.Sound.N0.duration, self.soundpath) self.radioqueue:SetDigit(1, ATIS.Sound.N1.filename, ATIS.Sound.N1.duration, self.soundpath) @@ -902,7 +910,7 @@ function ATIS:onafterStart(From, Event, To) self.radioqueue:SetDigit(7, ATIS.Sound.N7.filename, ATIS.Sound.N7.duration, self.soundpath) self.radioqueue:SetDigit(8, ATIS.Sound.N8.filename, ATIS.Sound.N8.duration, self.soundpath) self.radioqueue:SetDigit(9, ATIS.Sound.N9.filename, ATIS.Sound.N9.duration, self.soundpath) - + -- Start radio queue. self.radioqueue:Start(1, 0.1) @@ -917,12 +925,12 @@ function ATIS:onafterStatus(From, Event, To) -- Get FSM state. local fsmstate=self:GetState() - + -- Info text. local text=string.format("State %s", fsmstate) self:I(self.lid..text) - - self:__Status(60) + + self:__Status(-60) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -939,9 +947,9 @@ function ATIS:onafterCheckQueue(From, Event, To) else self:T2(self.lid..string.format("Radio queue %d transmissions queued.", #self.radioqueue.queue)) end - + -- Check back in 5 seconds. - self:__CheckQueue(5) + self:__CheckQueue(-5) end --- Broadcast ATIS radio message. @@ -950,29 +958,29 @@ function ATIS:onafterBroadcast(From, Event, To) -- Get current coordinate. local coord=self.airbase:GetCoordinate() - + -- Get elevation. local height=coord:GetLandHeight()+10 - + ---------------- --- Pressure --- ---------------- - + -- Pressure in hPa. local qfe=coord:GetPressure(height) local qnh=coord:GetPressure(0) - + -- Convert to inHg. if self.PmmHg then qfe=UTILS.hPa2mmHg(qfe) qnh=UTILS.hPa2mmHg(qnh) - else + else if not self.metric then qfe=UTILS.hPa2inHg(qfe) qnh=UTILS.hPa2inHg(qnh) end end - + local QFE=UTILS.Split(string.format("%.2f", qfe), ".") local QNH=UTILS.Split(string.format("%.2f", qnh), ".") @@ -985,41 +993,41 @@ function ATIS:onafterBroadcast(From, Event, To) QNH=UTILS.Split(string.format("%.1f", qnh), ".") end end - + ------------ --- Wind --- ------------ - + -- Get wind direction and speed in m/s. local windFrom, windSpeed=coord:GetWind(height) - + -- Wind in magnetic or true. local magvar=self.magvar if self.windtrue then magvar=0 end - + local WINDFROM=string.format("%03d", windFrom-magvar) local WINDSPEED=string.format("%d", UTILS.MpsToKnots(windSpeed)) - + if self.metric then WINDSPEED=string.format("%d", windSpeed) end - + -------------- --- Runway --- -------------- - + -- Active runway data based on wind direction. local runact=self.airbase:GetActiveRunway(self.runwaym2t) - + -- Active runway "31". local runway=self:GetMagneticRunway(windFrom) or runact.idx - + -- Left or right in case there are two runways with the same heading. local rleft=false local rright=false - + -- Check if user explicitly specified a runway. if self.activerunway then local runwayno=self.activerunway:gsub("%D+", "") @@ -1029,12 +1037,12 @@ function ATIS:onafterBroadcast(From, Event, To) rleft=self.activerunway:lower():find("l") rright=self.activerunway:lower():find("r") end - + ------------ --- Time --- ------------ local time=timer.getAbsTime() - + -- Conversion to Zulu time. if self.zuludiff then -- User specified. @@ -1050,48 +1058,48 @@ function ATIS:onafterBroadcast(From, Event, To) time=time-1*60*60 -- Calais UTC+1 hour end end - + local clock=UTILS.SecondsToClock(time) local zulu=UTILS.Split(clock, ":") local ZULU=string.format("%s%s", zulu[1], zulu[2]) - - + + -- NATO time stamp. 0=Alfa, 1=Bravo, 2=Charlie, etc. local NATO=ATIS.Alphabet[tonumber(zulu[1])+1] - + -- Debug. self:T3(string.format("clock=%s", tostring(clock))) self:T3(string.format("zulu1=%s", tostring(zulu[1]))) self:T3(string.format("zulu2=%s", tostring(zulu[2]))) self:T3(string.format("ZULU =%s", tostring(ZULU))) self:T3(string.format("NATO =%s", tostring(NATO))) - + ------------------- --- Temperature --- ------------------- - + -- Temperature in °C (or °F). local temperature=coord:GetTemperature(height) - + -- Convert to °F. if self.TDegF then temperature=UTILS.CelciusToFarenheit(temperature) end - + local TEMPERATURE=string.format("%d", math.abs(temperature)) - + --------------- --- Weather --- --------------- - + -- Get mission weather info. Most of this is static. local clouds, visibility, turbulence, fog, dust, static=self:GetMissionWeather() - + -- Check that fog is actually "thick" enough to reach the airport. If an airport is in the mountains, fog might not affect it as it is measured from sea level. if fog and fog.thicknessUTILS.FeetToMeters(1500) then dust=nil @@ -1100,63 +1108,63 @@ function ATIS:onafterBroadcast(From, Event, To) ------------------ --- Visibility --- ------------------ - + -- Get min visibility. local visibilitymin=visibility - + if fog then if fog.visibility=9 then @@ -1183,31 +1191,31 @@ function ATIS:onafterBroadcast(From, Event, To) CLOUDSsub="No clouds" end end - + -------------------- --- Transmission --- -------------------- - + -- Subtitle local subtitle="" - + --Airbase name subtitle=string.format("%s", self.airbasename) if self.airbasename:find("AFB")==nil and self.airbasename:find("Airport")==nil and self.airbasename:find("Airstrip")==nil and self.airbasename:find("airfield")==nil and self.airbasename:find("AB")==nil then subtitle=subtitle.." Airport" end self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, self.subduration) - + -- Information tag subtitle=string.format("Information %s", NATO) self:Transmission(ATIS.Sound.Information, 0.5, subtitle) self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) - + -- Zulu Time subtitle=string.format("%s Zulu Time", ZULU) self.radioqueue:Number2Transmission(ZULU, nil, 0.5) self:Transmission(ATIS.Sound.TimeZulu, 0.2, subtitle) - + -- Visibility if self.metric then subtitle=string.format("Visibility %s km", VISIBILITY) @@ -1219,9 +1227,9 @@ function ATIS:onafterBroadcast(From, Event, To) if self.metric then self:Transmission(ATIS.Sound.Kilometers, 0.2) else - self:Transmission(ATIS.Sound.NauticalMiles, 0.2) + self:Transmission(ATIS.Sound.NauticalMiles, 0.2) end - + -- Cloud base self:Transmission(CloudCover, 1.0, CLOUDSsub) if CLOUDBASE and static then @@ -1235,7 +1243,7 @@ function ATIS:onafterBroadcast(From, Event, To) if tonumber(CLOUDBASE1000)>0 then self.radioqueue:Number2Transmission(CLOUDBASE1000) self:Transmission(ATIS.Sound.Thousand, 0.1) - end + end if tonumber(CLOUDBASE0100)>0 then self.radioqueue:Number2Transmission(CLOUDBASE0100) self:Transmission(ATIS.Sound.Hundred, 0.1) @@ -1245,7 +1253,7 @@ function ATIS:onafterBroadcast(From, Event, To) if tonumber(CLOUDCEIL1000)>0 then self.radioqueue:Number2Transmission(CLOUDCEIL1000) self:Transmission(ATIS.Sound.Thousand, 0.1) - end + end if tonumber(CLOUDCEIL0100)>0 then self.radioqueue:Number2Transmission(CLOUDCEIL0100) self:Transmission(ATIS.Sound.Hundred, 0.1) @@ -1256,7 +1264,7 @@ function ATIS:onafterBroadcast(From, Event, To) self:Transmission(ATIS.Sound.Feet, 0.1) end end - + -- Weather phenomena local wp=false local wpsub="" @@ -1274,21 +1282,21 @@ function ATIS:onafterBroadcast(From, Event, To) wp=true elseif precepitation==4 then wpsub=wpsub.." snowstorm" - wp=true + wp=true end if fog then if wp then wpsub=wpsub.."," - end + end wpsub=wpsub.." fog" - wp=true + wp=true end if dust then if wp then wpsub=wpsub.."," - end + end wpsub=wpsub.." dust" - wp=true + wp=true end -- Actual output if wp then @@ -1307,14 +1315,14 @@ function ATIS:onafterBroadcast(From, Event, To) self:Transmission(ATIS.Sound.Fog, 0.5) end if dust then - self:Transmission(ATIS.Sound.Dust, 0.5) - end - end - + self:Transmission(ATIS.Sound.Dust, 0.5) + end + end + -- Altimeter QNH/QFE. if self.PmmHg then subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2]) - else + else if self.metric then subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2]) else @@ -1339,7 +1347,7 @@ function ATIS:onafterBroadcast(From, Event, To) self:Transmission(ATIS.Sound.InchesOfMercury, 0.1) end end - + -- Temperature if self.TDegF then if temperature<0 then @@ -1357,14 +1365,14 @@ function ATIS:onafterBroadcast(From, Event, To) self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) if temperature<0 then self:Transmission(ATIS.Sound.Minus, 0.2) - end + end self.radioqueue:Number2Transmission(TEMPERATURE) if self.TDegF then self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) else self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) end - + -- Wind if self.metric then subtitle=string.format("Wind from %s at %s m/s", WINDFROM, WINDSPEED) @@ -1386,7 +1394,7 @@ function ATIS:onafterBroadcast(From, Event, To) if turbulence>0 then self:Transmission(ATIS.Sound.Gusting, 0.2) end - + -- Active runway. local subtitle=string.format("Active runway %s", runway) if rleft then @@ -1401,19 +1409,19 @@ function ATIS:onafterBroadcast(From, Event, To) elseif rright then self:Transmission(ATIS.Sound.Right, 0.2) end - + -- Runway length. if self.rwylength then - + local length=runact.length if not self.metric then length=UTILS.MetersToFeet(length) end - + -- Length in thousands and hundrets of ft/meters. local L1000, L0100=self:_GetThousandsAndHundreds(length) - - -- Subtitle. + + -- Subtitle. local subtitle=string.format("Runway length %d", length) if self.metric then subtitle=subtitle.." meters" @@ -1426,7 +1434,7 @@ function ATIS:onafterBroadcast(From, Event, To) if tonumber(L1000)>0 then self.radioqueue:Number2Transmission(L1000) self:Transmission(ATIS.Sound.Thousand, 0.1) - end + end if tonumber(L0100)>0 then self.radioqueue:Number2Transmission(L0100) self:Transmission(ATIS.Sound.Hundred, 0.1) @@ -1436,9 +1444,9 @@ function ATIS:onafterBroadcast(From, Event, To) else self:Transmission(ATIS.Sound.Feet, 0.1) end - + end - + -- Tower frequency. if self.towerfrequency then local freqs="" @@ -1452,42 +1460,42 @@ function ATIS:onafterBroadcast(From, Event, To) self:Transmission(ATIS.Sound.TowerFrequency, 1.0, subtitle) for _,freq in pairs(self.towerfrequency) do local f=string.format("%.3f", freq) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) - end + end end - + -- ILS local ils=self:GetNavPoint(self.ils, runway) if ils then subtitle=string.format("ILS frequency %.2f MHz", ils.frequency) self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) local f=string.format("%.2f", ils.frequency) - f=UTILS.Split(f, ".") + f=UTILS.Split(f, ".") self.radioqueue:Number2Transmission(f[1], nil, 0.5) if tonumber(f[2])>0 then self:Transmission(ATIS.Sound.Decimal, 0.2) self.radioqueue:Number2Transmission(f[2]) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) - end - + end + -- Outer NDB local ndb=self:GetNavPoint(self.ndbouter, runway) if ndb then subtitle=string.format("Outer NDB frequency %.2f MHz", ndb.frequency) self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) local f=string.format("%.2f", ndb.frequency) - f=UTILS.Split(f, ".") + f=UTILS.Split(f, ".") self.radioqueue:Number2Transmission(f[1], nil, 0.5) if tonumber(f[2])>0 then self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + self.radioqueue:Number2Transmission(f[2]) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) end @@ -1498,7 +1506,7 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=string.format("Inner NDB frequency %.2f MHz", ndb.frequency) self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) local f=string.format("%.2f", ndb.frequency) - f=UTILS.Split(f, ".") + f=UTILS.Split(f, ".") self.radioqueue:Number2Transmission(f[1], nil, 0.5) if tonumber(f[2])>0 then self:Transmission(ATIS.Sound.Decimal, 0.2) @@ -1506,13 +1514,13 @@ function ATIS:onafterBroadcast(From, Event, To) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) end - + -- VOR if self.vor then subtitle=string.format("VOR frequency %.2f MHz", self.vor) self:Transmission(ATIS.Sound.VORFrequency, 1.0, subtitle) local f=string.format("%.2f", self.vor) - f=UTILS.Split(f, ".") + f=UTILS.Split(f, ".") self.radioqueue:Number2Transmission(f[1], nil, 0.5) if tonumber(f[2])>0 then self:Transmission(ATIS.Sound.Decimal, 0.2) @@ -1520,7 +1528,7 @@ function ATIS:onafterBroadcast(From, Event, To) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) end - + -- TACAN if self.tacan then subtitle=string.format("TACAN channel %dX", self.tacan) @@ -1528,14 +1536,14 @@ function ATIS:onafterBroadcast(From, Event, To) self.radioqueue:Number2Transmission(tostring(self.tacan), nil, 0.2) self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2) end - + -- RSBN if self.rsbn then subtitle=string.format("RSBN channel %d", self.rsbn) self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) + self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) end - + -- PRMG local ndb=self:GetNavPoint(self.prmg, runway) if ndb then @@ -1543,7 +1551,7 @@ function ATIS:onafterBroadcast(From, Event, To) self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1559,13 +1567,13 @@ function ATIS:GetMagneticRunway(windfrom) local diffmin=nil local runway=nil for _,heading in pairs(self.runwaymag) do - + local diff=UTILS.HdgDiff(windfrom, tonumber(heading)*10) if diffmin==nil or diff Date: Mon, 21 Oct 2019 00:29:46 +0200 Subject: [PATCH 383/485] ATIS --- Moose Development/Moose/Core/RadioQueue.lua | 14 +- Moose Development/Moose/Ops/ATIS.lua | 296 ++++++++++++++------ 2 files changed, 230 insertions(+), 80 deletions(-) diff --git a/Moose Development/Moose/Core/RadioQueue.lua b/Moose Development/Moose/Core/RadioQueue.lua index dabaf50f3..7ee3c02ae 100644 --- a/Moose Development/Moose/Core/RadioQueue.lua +++ b/Moose Development/Moose/Core/RadioQueue.lua @@ -52,7 +52,7 @@ RADIOQUEUE = { sendercoord = nil, sendername = nil, senderinit = nil, - power = 100, + power = nil, numbers = {}, checking = nil, schedonce = nil, @@ -97,6 +97,9 @@ function RADIOQUEUE:New(frequency, modulation, alias) -- Modulation. self.modulation=modulation or radio.modulation.AM + -- Set radio power. + self:SetRadioPower() + -- Scheduler. self.scheduler=SCHEDULER:New() self.scheduler:NoTrace() @@ -156,6 +159,15 @@ function RADIOQUEUE:SetSenderUnitName(name) return self end +--- Set radio power. Note that this only applies if no relay unit is used. +-- @param #RADIOQUEUE self +-- @param #number power Radio power in Watts. Default 100 W. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetRadioPower(power) + self.power=power or 100 + return self +end + --- Set parameters of a digit. -- @param #RADIOQUEUE self -- @param #number digit The digit 0-9. diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 59aa48a24..181a77e97 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -15,7 +15,8 @@ -- * More than 180 voice overs, -- * Airbase names pronounced in locale accent (russian, US, french, arabic), -- * Option to present information in imperial or metric units, --- * Frequencies/channels of nav aids (ILS, VOR, NDB, TACAN, PRMG, RSBN). +-- * Runway length and airfield elevation (optional), +-- * Frequencies/channels of nav aids (ILS, VOR, NDB, TACAN, PRMG, RSBN) (optional). -- -- === -- @@ -36,7 +37,7 @@ -- -- === -- --- Automatic terminal information service, or ATIS, is a continuous broadcast of recorded aeronautical information in busier terminal areas, i.e. airports and their immediate surroundings. +-- Automatic terminal information service, or ATIS, is a continuous broadcast of recorded aeronautical information in busier terminal areas, *i.e.* airports and their immediate surroundings. -- ATIS broadcasts contain essential information, such as current weather information, active runways, and any other information required by the pilots. -- -- === @@ -56,6 +57,7 @@ -- @field Wrapper.Airbase#AIRBASE airbase The airbase object. -- @field #number frequency Radio frequency in MHz. -- @field #number modulation Radio modulation 0=AM or 1=FM. +-- @field #number power Radio power in Watts. Default 100 W. -- @field Core.RadioQueue#RADIOQUEUE radioqueue Radio queue for broadcasing messages. -- @field #string soundpath Path to sound files. -- @field #string relayunitname Name of the radio relay unit. @@ -75,6 +77,7 @@ -- @field #number rsbn RSBN channel. -- @field #table prmg PRMG channels (can be runway specific). -- @field #boolean rwylength If true, give info on runway length. +-- @field #boolean elevation If true, give info on airfield elevation. -- @field #table runwaymag Table of magnetic runway headings. -- @field #number runwaym2t Optional correction for magnetic to true runway heading conversion (and vice versa) in degrees. -- @field #boolean windtrue Report true (from) heading of wind. Default is magnetic. @@ -88,7 +91,7 @@ -- -- # The ATIS Concept -- --- Automatic terminal information service, or ATIS, is a continuous broadcast of recorded aeronautical information in busier terminal areas, i.e. airports and their immediate surroundings. +-- Automatic terminal information service, or ATIS, is a continuous broadcast of recorded aeronautical information in busier terminal areas, *i.e.* airports and their immediate surroundings. -- ATIS broadcasts contain essential information, such as current weather information, active runways, and any other information required by the pilots. -- -- # DCS Limitations @@ -111,11 +114,11 @@ -- -- ## Subtitles -- --- Currently, DCS allows for displaying subtitles of radio transmissions only from airborne units, i.e. airplanes and helicopters. Therefore, if you want to have subtitles, it is necessary to place an +-- Currently, DCS allows for displaying subtitles of radio transmissions only from airborne units, *i.e.* airplanes and helicopters. Therefore, if you want to have subtitles, it is necessary to place an -- additonal aircraft on the ATIS airport and set it to uncontrolled. This unit can then function as a radio relay to transmit messages with subtitles. These subtitles will only be displayed, if the -- player has tuned in the correct ATIS frequency. -- --- Radio transmissions via an airborne unit can be set via the @{#ATIS.SetRadioRelayUnitName}(*unitname*) function, where the parameter *unitname* is the name of the unit passed as string, e.g. +-- Radio transmissions via an airborne unit can be set via the @{#ATIS.SetRadioRelayUnitName}(*unitname*) function, where the parameter *unitname* is the name of the unit passed as string, *e.g.* -- -- atisBatumi:SetRadioRelayUnitName("Radio Relay Batumi") -- @@ -141,7 +144,7 @@ -- -- The script will examine the string and search for the characters "L" (left) and "R" (right). -- --- If only left or right should be set and the direction determined by the wind vector, the runway heading can be left out, e.g. +-- If only left or right should be set and the direction determined by the wind vector, the runway heading can be left out, *e.g.* -- -- atisAbuDhabi:SetActiveRunway("L") -- @@ -169,7 +172,7 @@ -- * **Runways** *31* and *13* - automatic but can be set manually via @{#ATIS.SetRunwayHeadingsMagnetic} -- * **ILS** *110.30* for runway *13* - set via @{#ATIS.AddILS} -- * **PRMG** *N/A* - set via @{#ATIS.AddPRMG} --- * **OUTER NDB** *N/A* - set via @{#ATIS.AddNDBinner} +-- * **OUTER NDB** *N/A* - set via @{#ATIS.AddNDBouter} -- * **INNER NDB** *N/A* - set via @{#ATIS.AddNDBinner} -- -- ![Banner Image](..\Presentations\ATIS\NavAid_Batumi.png) @@ -198,14 +201,14 @@ -- -- ### ILS -- --- The Instrument Landing System [(ILS)](https://en.wikipedia.org/wiki/Instrument_landing_system) frequency can be set via the @{#ATIS.AddILS}(*frequency*, *runway*) function, where *frequency* is the ILS frequency and *runway* the two letter string of the corresponding runway, e.g. "31". +-- The Instrument Landing System [(ILS)](https://en.wikipedia.org/wiki/Instrument_landing_system) frequency can be set via the @{#ATIS.AddILS}(*frequency*, *runway*) function, where *frequency* is the ILS frequency and *runway* the two letter string of the corresponding runway, *e.g.* "31". -- If the parameter *runway* is omitted (nil) then the frequency is supposed to be valid for all runways of the airport. -- -- ### NDB -- -- Inner and outer Non-Directional (radio) Beacons [NDBs](https://en.wikipedia.org/wiki/Non-directional_beacon) can be set via the @{#ATIS.AddNDBinner}(*frequency*, *runway*) and @{#ATIS.AddNDBouter}(*frequency*, *runway*) functions, respectively. -- --- In both cases, the parameter *frequency* is the NDB frequency and *runway* the two letter string of the corresponding runway, e.g. "31". +-- In both cases, the parameter *frequency* is the NDB frequency and *runway* the two letter string of the corresponding runway, *e.g.* "31". -- If the parameter *runway* is omitted (nil) then the frequency is supposed to be valid for all runways of the airport. -- -- ## RSBN @@ -218,7 +221,7 @@ -- -- ## Unit System -- --- By default, information is given in imperial units, i.e. wind speed in knots, pressure in inches of mercury, visibility in Nautical miles, etc. +-- By default, information is given in imperial units, *i.e.* wind speed in knots, pressure in inches of mercury, visibility in Nautical miles, etc. -- -- If you prefer metric units, you can enable this via the @{#ATIS.SetMetricUnits}() function, -- @@ -232,7 +235,7 @@ -- -- Check out the pinned messages in the Moose discord #ops-atis channel. -- --- To include the files, open the mission (.miz) file with, e.g., 7-zip. Then just drag-n-drop the file into the miz. +-- To include the files, open the mission (.miz) file with, *e.g.*, 7-zip. Then just drag-n-drop the file into the miz. -- -- ![Banner Image](..\Presentations\ATIS\ATIS_SoundFolder.png) -- @@ -280,6 +283,7 @@ ATIS = { airbase = nil, frequency = nil, modulation = nil, + power = nil, radioqueue = nil, soundpath = nil, relayunitname = nil, @@ -299,6 +303,7 @@ ATIS = { rsbn = nil, prmg = {}, rwylength = nil, + elevation = nil, runwaymag = {}, runwaym2t = nil, windtrue = nil, @@ -353,7 +358,8 @@ ATIS.RunwayM2T={ --- Nav point data. -- @type ATIS.NavPoint -- @field #number frequency Nav point frequency. --- @field #string runway Runway, e.g. "21". +-- @field #string runway Runway, *e.g.* "21". +-- @field #boolean leftright If true, runway has left "L" and right "R" runways. --- Sound file data. -- @type ATIS.Soundfile @@ -378,6 +384,8 @@ ATIS.RunwayM2T={ -- @field #ATIS.Soundfile DegreesCelsius -- @field #ATIS.Soundfile DegreesFahrenheit -- @field #ATIS.Soundfile Dust +-- @field #ATIS.Soundfile Elevation +-- @field #ATIS.Soundfile EndOfInformation -- @field #ATIS.Soundfile Feet -- @field #ATIS.Soundfile Fog -- @field #ATIS.Soundfile Gusting @@ -444,6 +452,8 @@ ATIS.Sound = { DegreesCelsius={filename="DegreesCelsius.ogg", duration=1.27}, DegreesFahrenheit={filename="DegreesFahrenheit.ogg", duration=1.23}, Dust={filename="Dust.ogg", duration=0.54}, + Elevation={filename="Elevation.ogg", duration=0.50}, + EndOfInformation={filename="EndOfInformation.ogg", duration=1.15}, Feet={filename="Feet.ogg", duration=0.45}, Fog={filename="Fog.ogg", duration=0.47}, Gusting={filename="Gusting.ogg", duration=0.55}, @@ -503,18 +513,18 @@ _ATIS={} --- ATIS class version. -- @field #string version -ATIS.version="0.4.1" +ATIS.version="0.4.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- DONE: Metric units. --- TODO: Correct fog for elevation. --- DONE: Set UTC correction. --- TODO: Use local time. --- DONE: Set magnetic variation. -- TODO: Add stop/pause FMS functions. +-- TODO: Correct fog for elevation. +-- TODO: Use local time. +-- DONE: Metric units. +-- DONE: Set UTC correction. +-- DONE: Set magnetic variation. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -556,6 +566,7 @@ function ATIS:New(airbasename, frequency, modulation) self:SetSubtitleDuration() self:SetMagneticDeclination() self:SetRunwayCorrectionMagnetic2True() + self:SetRadioPower() -- Start State. self:SetStartState("Stopped") @@ -626,7 +637,7 @@ end --- Set tower frequencies. -- @param #ATIS self --- @param #table freqs Table of frequencies in MHz. A single frequency can be given as a plain number (i.e. must not be table). +-- @param #table freqs Table of frequencies in MHz. A single frequency can be given as a plain number (*i.e.* must not be table). -- @return #ATIS self function ATIS:SetTowerFrequencies(freqs) if type(freqs)=="table" then @@ -641,7 +652,7 @@ end --- Set active runway. This can be used if the automatic runway determination via the wind direction gives incorrect results. -- For example, use this if there are two runways with the same directions. -- @param #ATIS self --- @param #string runway Active runway, e.g. "31L". +-- @param #string runway Active runway, *e.g.* "31L". -- @return #ATIS self function ATIS:SetActiveRunway(runway) self.activerunway=tostring(runway) @@ -656,11 +667,30 @@ function ATIS:SetRunwayLength() return self end ---- Set magnetic runway headings as depicted on the runway, e.g. 13 for 130. +--- Give information on airfield elevation -- @param #ATIS self --- @param #table headings Magnetic headings. Inverse (-180°) headings are added automatically. +-- @return #ATIS self +function ATIS:SetElevation() + self.elevation=true + return self +end + +--- Set radio power. Note that this only applies if no relay unit is used. +-- @param #ATIS self +-- @param #number power Radio power in Watts. Default 100 W. +-- @return #ATIS self +function ATIS:SetRadioPower(power) + self.power=power or 100 + return self +end + +--- Set magnetic runway headings as depicted on the runway, *e.g.* "13" for 130° or "25L" for the left runway with magnetic heading 250°. +-- @param #ATIS self +-- @param #table headings Magnetic headings. Inverse (-180°) headings are added automatically. You only need to specify one heading per runway direction. "L"eft and "R" right can also be appended. -- @return #ATIS self function ATIS:SetRunwayHeadingsMagnetic(headings) + + -- First make sure, we have a table. if type(headings)=="table" then -- nothing to do else @@ -668,15 +698,35 @@ function ATIS:SetRunwayHeadingsMagnetic(headings) end for _,heading in pairs(headings) do - heading=tonumber(heading) + + if type(heading)=="number" then + heading=string.format("%02d", heading) + end + + -- Add runway heading to table. + self:I(self.lid..string.format("Adding user specified magnetic runway heading %s", heading)) table.insert(self.runwaymag, heading) - local head2=heading-18 + local h=self:GetRunwayWithoutLR(heading) + + local head2=tonumber(h)-18 if head2<0 then head2=head2+36 end + + -- Convert to string. + head2=string.format("%02d", head2) + + -- Append "L" or "R" if necessary. + local left=self:GetRunwayLR(heading) + if left==true then + head2=head2.."L" + elseif left==false then + head2=head2.."R" + end - self:I(self.lid..string.format("Adding magnetic runway heading %d", head2)) + -- Add inverse runway heading to table. + self:I(self.lid..string.format("Adding user specified magnetic runway heading %s (inverse)", head2)) table.insert(self.runwaymag, head2) end @@ -688,7 +738,7 @@ end -- @param #number duration Duration in seconds. Default 10 seconds. -- @return #ATIS self function ATIS:SetSubtitleDuration(duration) - self.subduration=tonumber(duration) or 10 + self.subduration=tonumber(duration or 10) return self end @@ -786,10 +836,10 @@ function ATIS:SetZuluTimeDifference(delta) return self end ---- Add ILS station. +--- Add ILS station. Note that this can be runway specific. -- @param #ATIS self --- @param #number frequency ILS frequency. --- @param #string runway Runway. Default all (*nil*). +-- @param #number frequency ILS frequency in MHz. +-- @param #string runway (Optional) Runway for which the given ILS frequency applies. Default all (*nil*). -- @return #ATIS self function ATIS:AddILS(frequency, runway) local ils={} --#ATIS.NavPoint @@ -808,10 +858,10 @@ function ATIS:SetVOR(frequency) return self end ---- Add outer NDB. +--- Add outer NDB. Note that this can be runway specific. -- @param #ATIS self --- @param #number frequency NDB frequency. --- @param #string runway Runway. Default all (*nil*). +-- @param #number frequency NDB frequency in MHz. +-- @param #string runway (Optional) Runway for which the given NDB frequency applies. Default all (*nil*). -- @return #ATIS self function ATIS:AddNDBouter(frequency, runway) local ndb={} --#ATIS.NavPoint @@ -821,10 +871,10 @@ function ATIS:AddNDBouter(frequency, runway) return self end ---- Add inner NDB. +--- Add inner NDB. Note that this can be runway specific. -- @param #ATIS self --- @param #number frequency NDB frequency. --- @param #string runway Runway. Default all (*nil*). +-- @param #number frequency NDB frequency in MHz. +-- @param #string runway (Optional) Runway for which the given NDB frequency applies. Default all (*nil*). -- @return #ATIS self function ATIS:AddNDBinner(frequency, runway) local ndb={} --#ATIS.NavPoint @@ -852,10 +902,10 @@ function ATIS:SetRSBN(channel) return self end ---- Add PRMG channel. +--- Add PRMG channel. Note that this can be runway specific. -- @param #ATIS self -- @param #number channel PRMG channel. --- @param #string runway Runway. Default all (*nil*). +-- @param #string runway (Optional) Runway for which the given PRMG channel applies. Default all (*nil*). -- @return #ATIS self function ATIS:AddPRMG(channel, runway) local ndb={} --#ATIS.NavPoint @@ -898,6 +948,9 @@ function ATIS:onafterStart(From, Event, To) -- Set relay unit if we have one. self.radioqueue:SetSenderUnitName(self.relayunitname) + + -- Set radio power. + self.radioqueue:SetRadioPower(self.power) -- Init numbers. self.radioqueue:SetDigit(0, ATIS.Sound.N0.filename, ATIS.Sound.N0.duration, self.soundpath) @@ -1018,24 +1071,26 @@ function ATIS:onafterBroadcast(From, Event, To) --- Runway --- -------------- - -- Active runway data based on wind direction. + -- Get active runway data based on wind direction. local runact=self.airbase:GetActiveRunway(self.runwaym2t) -- Active runway "31". local runway=self:GetMagneticRunway(windFrom) or runact.idx -- Left or right in case there are two runways with the same heading. - local rleft=false - local rright=false + local rwyLeft=nil -- Check if user explicitly specified a runway. if self.activerunway then - local runwayno=self.activerunway:gsub("%D+", "") + + -- Get explicit runway heading if specified. + local runwayno=self:GetRunwayWithoutLR(self.activerunway) if runwayno~="" then runway=runwayno end - rleft=self.activerunway:lower():find("l") - rright=self.activerunway:lower():find("r") + + -- Was "L"eft or "R"ight given? + rwyLeft=self:GetRunwayLR(self.activerunway) end ------------ @@ -1397,16 +1452,16 @@ function ATIS:onafterBroadcast(From, Event, To) -- Active runway. local subtitle=string.format("Active runway %s", runway) - if rleft then + if rwyLeft==true then subtitle=subtitle.." Left" - elseif rright then + elseif rwyLeft==false then subtitle=subtitle.." Right" end self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) self.radioqueue:Number2Transmission(runway) - if rleft then + if rwyLeft==true then self:Transmission(ATIS.Sound.Left, 0.2) - elseif rright then + elseif rwyLeft==false then self:Transmission(ATIS.Sound.Right, 0.2) end @@ -1446,6 +1501,43 @@ function ATIS:onafterBroadcast(From, Event, To) end end + + -- Airfield elevation + if self.elevation then + + local elevation=self.airbase:GetHeight() + if not self.metric then + elevation=UTILS.MetersToFeet(elevation) + end + + -- Length in thousands and hundrets of ft/meters. + local L1000, L0100=self:_GetThousandsAndHundreds(elevation) + + -- Subtitle. + local subtitle=string.format("Elevation %d", elevation) + if self.metric then + subtitle=subtitle.." meters" + else + subtitle=subtitle.." feet" + end + + -- Transmitt. + self:Transmission(ATIS.Sound.Elevation, 1.0, subtitle) + if tonumber(L1000)>0 then + self.radioqueue:Number2Transmission(L1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(L0100)>0 then + self.radioqueue:Number2Transmission(L0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end + + end -- Tower frequency. if self.towerfrequency then @@ -1471,7 +1563,7 @@ function ATIS:onafterBroadcast(From, Event, To) end -- ILS - local ils=self:GetNavPoint(self.ils, runway) + local ils=self:GetNavPoint(self.ils, runway, rwyLeft) if ils then subtitle=string.format("ILS frequency %.2f MHz", ils.frequency) self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) @@ -1486,7 +1578,7 @@ function ATIS:onafterBroadcast(From, Event, To) end -- Outer NDB - local ndb=self:GetNavPoint(self.ndbouter, runway) + local ndb=self:GetNavPoint(self.ndbouter, runway, rwyLeft) if ndb then subtitle=string.format("Outer NDB frequency %.2f MHz", ndb.frequency) self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) @@ -1501,7 +1593,7 @@ function ATIS:onafterBroadcast(From, Event, To) end -- Inner NDB - local ndb=self:GetNavPoint(self.ndbinner, runway) + local ndb=self:GetNavPoint(self.ndbinner, runway, rwyLeft) if ndb then subtitle=string.format("Inner NDB frequency %.2f MHz", ndb.frequency) self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) @@ -1545,12 +1637,17 @@ function ATIS:onafterBroadcast(From, Event, To) end -- PRMG - local ndb=self:GetNavPoint(self.prmg, runway) + local ndb=self:GetNavPoint(self.prmg, runway, rwyLeft) if ndb then subtitle=string.format("PRMG channel %d", ndb.frequency) self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) end + + -- End of Information Alpha, Bravo, ... + subtitle=string.format("End of information %s", NATO) + self:Transmission(ATIS.Sound.EndOfInformation, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) end @@ -1567,41 +1664,82 @@ function ATIS:GetMagneticRunway(windfrom) local diffmin=nil local runway=nil for _,heading in pairs(self.runwaymag) do + + local hdg=self:GetRunwayWithoutLR(heading) - local diff=UTILS.HdgDiff(windfrom, tonumber(heading)*10) + local diff=UTILS.HdgDiff(windfrom, tonumber(hdg)*10) if diffmin==nil or diff data is valid for all runways. + return nav + else + + local navy=tonumber(self:GetRunwayWithoutLR(nav.runway))*10 + local rwyy=tonumber(self:GetRunwayWithoutLR(runway))*10 + + local navL=self:GetRunwayLR(nav.runway) + + if UTILS.HdgDiff(navy,rwyy)<=15 then --We allow an error of +-15° here. + if navL==nil or navL==left then + return nav + end + end + + end + end + + return nil +end + +--- Get runway heading without left or right info. +-- @param #ATIS self +-- @param #string runway Runway heading, *e.g.* "31L". +-- @return #string Runway heading without left or right, *e.g.* "31". +function ATIS:GetRunwayWithoutLR(runway) + local rwywo=runway:gsub("%D+", "") + --self:I(string.format("FF runway=%s ==> rwywo=%s", runway, rwywo)) + return rwywo +end + +--- Get info if left or right runway is active. +-- @param #ATIS self +-- @param #string runway Runway heading, *e.g.* "31L". +-- @return #boolean If *true*, left runway is active. If *false*, right runway. If *nil*, neither applies. +function ATIS:GetRunwayLR(runway) + + -- Get left/right if specified. + local rwyL=runway:lower():find("l") + local rwyR=runway:lower():find("r") + + if rwyL then + return true + elseif rwyR then + return false else return nil end -end - ---- Get Nav point data. --- @param #ATIS self --- @param #table navpoints Nav points data table. --- @param #string runway (Active) runway, e.g. "31" --- @return #ATIS.NavPoint Nav point data table. -function ATIS:GetNavPoint(navpoints, runway) - for _,_nav in pairs(navpoints or {}) do - local nav=_nav --#ATIS.NavPoint - if nav.runway==nil then - return nav - else - local nr=tonumber(nav.runway) - local r=tonumber(runway) - if math.abs(nr-r)<=2 then --We allow an error of +-20° here. - return nav - end - end - end - return nil + end --- Transmission via RADIOQUEUE. @@ -1698,9 +1836,9 @@ end --- Get thousands of a number. -- @param #ATIS self --- @param #number n Number, e.g. 4359. --- @return #string Thousands of n, e.g. "4" for 4359. --- @return #string Hundreds of n, e.g. "4" for 4359 because its rounded. +-- @param #number n Number, *e.g.* 4359. +-- @return #string Thousands of n, *e.g.* "4" for 4359. +-- @return #string Hundreds of n, *e.g.* "4" for 4359 because its rounded. function ATIS:_GetThousandsAndHundreds(n) local N=UTILS.Round(n/1000, 1) From fe2ef11f8d4dfd1d1d323010a4fccf45629dee53 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 21 Oct 2019 22:29:45 +0200 Subject: [PATCH 384/485] ATIS v0.5.0 --- Moose Development/Moose/Ops/ATIS.lua | 10 +++++----- Moose Development/Moose/Utilities/Utils.lua | 21 +++++++++++++++++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 181a77e97..38fcb3401 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -452,7 +452,7 @@ ATIS.Sound = { DegreesCelsius={filename="DegreesCelsius.ogg", duration=1.27}, DegreesFahrenheit={filename="DegreesFahrenheit.ogg", duration=1.23}, Dust={filename="Dust.ogg", duration=0.54}, - Elevation={filename="Elevation.ogg", duration=0.50}, + Elevation={filename="Elevation.ogg", duration=0.78}, EndOfInformation={filename="EndOfInformation.ogg", duration=1.15}, Feet={filename="Feet.ogg", duration=0.45}, Fog={filename="Fog.ogg", duration=0.47}, @@ -513,7 +513,7 @@ _ATIS={} --- ATIS class version. -- @field #string version -ATIS.version="0.4.2" +ATIS.version="0.5.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1699,13 +1699,13 @@ function ATIS:GetNavPoint(navpoints, runway, left) local rwyy=tonumber(self:GetRunwayWithoutLR(runway))*10 local navL=self:GetRunwayLR(nav.runway) + local hdgD=UTILS.HdgDiff(navy,rwyy) - if UTILS.HdgDiff(navy,rwyy)<=15 then --We allow an error of +-15° here. - if navL==nil or navL==left then + if hdgD<=15 then --We allow an error of +-15° here. + if navL==nil or (navL==true and left==true) or (navL==false and left==false) then return nav end end - end end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index f1e77e12c..683252718 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -857,7 +857,18 @@ end -- @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))) + + local cosalpha=UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b)) + + local alpha=0 + if cosalpha>=0.9999999999 then --acos(1) is not defined. + alpha=0 + elseif cosalpha<=-0.999999999 then --acos(-1) is not defined. + alpha=math.pi + else + alpha=math.acos(cosalpha) + end + return math.deg(alpha) end @@ -879,14 +890,16 @@ end function UTILS.HdgDiff(h1, h2) -- Angle in rad. - local alpha=math.rad(h1) - local beta=math.rad(h2) + local alpha= math.rad(tonumber(h1)) + local beta = math.rad(tonumber(h2)) -- Runway vector. local v1={x=math.cos(alpha), y=0, z=math.sin(alpha)} local v2={x=math.cos(beta), y=0, z=math.sin(beta)} + + local delta=UTILS.VecAngle(v1, v2) - return math.abs(UTILS.VecAngle(v1, v2)) + return math.abs(delta) end From 01dd3c93d2aaa591549e7ceef886b1c679a2a93a Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 22 Oct 2019 14:35:10 +0200 Subject: [PATCH 385/485] AIRBOSS v1.0.9 - Case II/III marshal stacks are not collapsed. - Fixed little bug that Charlie time cannot be calculated if no future recovery window is defined. --- Moose Development/Moose/Ops/Airboss.lua | 207 +++++++++++++++++++----- 1 file changed, 167 insertions(+), 40 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index e8a88ec21..5a3c3a6e5 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -52,9 +52,9 @@ -- -- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-airboss channel. -- There you also find an example mission and the necessary voice over sound files. Check out the **pinned messages**. --- +-- -- ## Example Missions --- +-- -- Example missions can be found [here](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Airboss). -- They contain the latest development Moose.lua file. -- @@ -1688,13 +1688,13 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.0.8" +AIRBOSS.version="1.0.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Handle tanker and AWACS. Put them into pattern. +-- DONE: Handle tanker and AWACS. Put them into pattern. -- TODO: Handle cases where AI crashes on carrier deck ==> Clean up deck. -- TODO: Player eject and crash debrief "gradings". -- TODO: PWO during case 2/3. @@ -1955,19 +1955,23 @@ function AIRBOSS:New(carriername, alias) -- Smoke zones. if false then - local case=2 + local case=3 self.holdingoffset=30 self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red, 5) self:_GetZoneLineup():SmokeZone(SMOKECOLOR.Green, 5) self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) - self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 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:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Blue, 45) self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) self:_GetZoneHolding(case, 1):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneHolding(case, 2):SmokeZone(SMOKECOLOR.White, 45) self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Orange, 45) - self:_GetZoneCommence(case):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneCommence(case, 1):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneCommence(case, 2):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneAbeamLandingSpot():SmokeZone(SMOKECOLOR.Red, 5) + self:_GetZoneLandingSpot():SmokeZone(SMOKECOLOR.Red, 5) end -- Carrier parameter debug tests. @@ -2247,6 +2251,29 @@ function AIRBOSS:New(carriername, alias) -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + --- Triggers the FSM event "LSOgrade". Called when the LSO grades a player + -- @function [parent=#AIRBOSS] LSOgrade + -- @param #AIRBOSS self + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- Triggers the FSM event "LSOgrade". Delayed called when the LSO grades a player. + -- @function [parent=#AIRBOSS] __LSOgrade + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- On after "LSOgrade" user function. Called when the carrier passes a waypoint of its route. + -- @function [parent=#AIRBOSS] OnAfterLSOgrade + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- Triggers the FSM event "LSOGrade". Called when the LSO grades a player -- @function [parent=#AIRBOSS] LSOGrade -- @param #AIRBOSS self @@ -3283,11 +3310,11 @@ function AIRBOSS:onafterStart(From, Event, To) -- Schedule radio queue checks. --self.RQLid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQLSO, "LSO"}, 1, 0.1) --self.RQMid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQMarshal, "MARSHAL"}, 1, 0.1) - + --self:I("FF: starting timer.scheduleFunction") --timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQLSO, name="LSO"}, timer.getTime()+1) --timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQMarshal, name="MARSHAL"}, timer.getTime()+1) - + -- Initial carrier position and orientation. self.Cposition=self:GetCoordinate() self.Corientation=self.carrier:GetOrientationX() @@ -3332,12 +3359,19 @@ end -- @param #string To To state. function AIRBOSS:onafterStatus(From, Event, To) + if true then + --env.info("FF Status ==> return") + --return + end + -- Get current time. local time=timer.getTime() -- Update marshal and pattern queue every 30 seconds. if time-self.Tqueue>self.dTqueue then + --collectgarbage() + -- Get time. local clock=UTILS.SecondsToClock(timer.getAbsTime()) local eta=UTILS.SecondsToClock(self:_GetETAatNextWP()) @@ -4070,7 +4104,7 @@ function AIRBOSS:onafterStop(From, Event, To) self:UnHandleEvent(EVENTS.Ejection) self:UnHandleEvent(EVENTS.PlayerLeaveUnit) self:UnHandleEvent(EVENTS.MissionEnd) - + self.CallScheduler:Clear() end @@ -5683,14 +5717,16 @@ function AIRBOSS:_GetNextMarshalFight() end -- Check if conditions are right. - if stack==1 and flight.holding~=nil and Tmarshal>=TmarshalMin then - if flight.ai then - -- Return AI flight. - return flight - else - -- Check for human player if they are already commencing. - if flight.step~=AIRBOSS.PatternStep.COMMENCING then + if flight.holding~=nil and Tmarshal>=TmarshalMin then + if flight.case==1 and stack==1 or flight.case>1 then + if flight.ai then + -- Return AI flight. return flight + else + -- Check for human player if they are already commencing. + if flight.step~=AIRBOSS.PatternStep.COMMENCING then + return flight + end end end end @@ -6681,7 +6717,8 @@ function AIRBOSS:_GetCharlieTime(flightgroup) -- 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 + -- Set ~7 min if no future recovery window is defined. Otherwise radio call function crashes. + Trecovery=7*60 end -- Loop over flights currently in the marshal queue. @@ -6701,7 +6738,7 @@ function AIRBOSS:_GetCharlieTime(flightgroup) -- 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. + -- Flight is on its way to the marshal stack. -- Coordinate of the holding zone. local holdingzone=self:_GetZoneHolding(flight.case, 1):GetCoordinate() @@ -6831,8 +6868,8 @@ function AIRBOSS:_CollapseMarshalStack(flight, nopattern) for _,_flight in pairs(self.Qmarshal) do local mflight=_flight --#AIRBOSS.PlayerData - -- 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 + -- Only collapse stack of which the flight left. CASE II/III stacks are not collapsed. + if (case==1 and mflight.case==1) then --or (case>1 and mflight.case>1) then -- Get current flag/stack value. local mstack=mflight.flag @@ -6936,6 +6973,96 @@ function AIRBOSS:_GetFreeStack(ai, case, empty) -- Recovery case. case=case or self.case + + if case==1 then + return self:_GetFreeStack_Old(ai, case, empty) + end + + -- 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 + + local nmax=1 + + -- 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 + + if n>nmax then + nmax=n + end + + if n>0 then + if flight.ai or flight.case>1 then + stack[n]=0 -- AI get one stack on their own. Also CASE II/III get one stack each. + 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 + + local nfree=nil + if stack[nmax]==0 then + -- Max occupied stack is completely full! + if case==1 then + if nmax>=nmaxstacks then + -- Already all Case I stacks are occupied ==> wait outside 10 NM zone. + nfree=nil + else + -- Return next free stack. + nfree=nmax+1 + end + else + -- Case II/III return next stack + nfree=nmax+1 + end + + elseif stack[nmax]==self.NmaxStack then + -- Max occupied stack is completely empty! This should happen only when there is no other flight in the marshal queue. + self:E(self.lid..string.format("ERROR: Max occupied stack is empty. Should not happen! Nmax=%d, stack[nmax]=%d", nmax, stack[nmax])) + nfree=nmax + else + -- Max occupied stack is partly full. + if ai or empty or case>1 then + nfree=nmax+1 + else + nfree=nmax + end + + end + + self:I(self.lid..string.format("Returning free stack %s", tostring(nfree))) + return nfree +end + +--- 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. +-- @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_Old(ai, case, empty) + + -- Recovery case. + case=case or self.case -- Max number of stacks available. local nmaxstacks=100 @@ -7838,14 +7965,14 @@ function AIRBOSS:_RemoveFlight(flight, completely) self:_RemoveFlightFromQueue(self.flights, flight) else - + -- Remove all grades until a final grade is reached. local grades=self.playerscores[flight.name] if grades and #grades>0 then while #grades>0 and grades[#grades].finalscore==nil do table.remove(grades, #grades) end - end + end -- Check if flight should be completely removed, e.g. after the player died or simply left the slot. if completely then @@ -8976,7 +9103,7 @@ function AIRBOSS:_Commencing(playerData, zonecheck) if zonecheck then -- Get auto commence zone. - local zoneCommence=self:_GetZoneCommence(playerData.case) + local zoneCommence=self:_GetZoneCommence(playerData.case, playerData.flag) -- Check if unit is in the zone. local inzone=playerData.unit:IsInZone(zoneCommence) @@ -10911,8 +11038,9 @@ end --- Get zone where player are automatically commence when enter. -- @param #AIRBOSS self -- @param #number case Recovery case. +-- @param #number stack Stack for Case II/III as we commence from stack>=1. -- @return Core.Zone#ZONE Holding zone. -function AIRBOSS:_GetZoneCommence(case) +function AIRBOSS:_GetZoneCommence(case, stack) -- Commence zone. local zone @@ -10949,12 +11077,11 @@ function AIRBOSS:_GetZoneCommence(case) else -- Case II/III + + stack=stack or 1 - -- We simply take the corridor for now. But a bit shorter. Holding starts at 21 and add 2 NM box as commence. - --zone=self:_GetZoneCorridor(case, 23) - - -- Total length. - local l=21 + -- Start point at 21 NM for stack=1. + local l=20+stack -- Offset angle local offset=self:GetRadial(case, false, true) @@ -12717,7 +12844,7 @@ end -- @param #AIRBOSS.PlayerData playerData Player data. function AIRBOSS:_Debrief(playerData) self:F(self.lid..string.format("Debriefing of player %s.", playerData.name)) - + -- Delete scheduler ID. playerData.debriefschedulerID=nil @@ -14375,7 +14502,7 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) -- Check if queue is empty. if #radioqueue==0 then - + if name=="LSO" then self:T(self.lid..string.format("Stopping LSO radio queue.")) self.radiotimer:Stop(self.RQLid) @@ -14385,7 +14512,7 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) self.radiotimer:Stop(self.RQMid) self.RQMid=nil end - + return end @@ -14476,7 +14603,7 @@ function AIRBOSS:_CheckRadioQueue(radioqueue, name) if _remove then table.remove(radioqueue, _remove) end - + return end @@ -14525,19 +14652,19 @@ function AIRBOSS:RadioTransmission(radio, call, loud, delay, interval, click, pi table.insert(self.RQLSO, transmission) caller="LSOCall" - + -- Schedule radio queue checks. if not self.RQLid then self:T(self.lid..string.format("Starting LSO radio queue.")) self.RQLid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQLSO, "LSO"}, 0.02, 0.05) end - + elseif radio.alias=="MARSHAL" then table.insert(self.RQMarshal, transmission) caller="MarshalCall" - + if not self.RQMid then self:T(self.lid..string.format("Starting Marhal radio queue.")) self.RQMid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQMarshal, "MARSHAL"}, 0.02, 0.05) @@ -15879,7 +16006,7 @@ function AIRBOSS:_ResetPlayerStatus(_unitName) -- Remove flight from queues. Collapse marshal stack if necessary. -- Section members are removed from the Spinning queue. If flight is member, he is removed from the section. self:_RemoveFlight(playerData) - + -- Stop pending debrief scheduler. if playerData.debriefschedulerID and self.Scheduler then self.Scheduler:Stop(playerData.debriefschedulerID) @@ -17301,7 +17428,7 @@ function AIRBOSS:_MarkMarshalZone(_unitName, flare) local zoneHolding=self:_GetZoneHolding(case, stack) -- Get Case I commence zone at three position. - local zoneThree=self:_GetZoneCommence(case) + local zoneThree=self:_GetZoneCommence(case, stack) -- Pattern alitude. local patternalt=self:_GetMarshalAltitude(stack, case) From 21a7594134b03d563ec1cef2c73d274d412b1216 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 22 Oct 2019 22:53:04 +0200 Subject: [PATCH 386/485] Update AI_Air_Patrol.lua - fixed bug that altitude and speed are not defined for racetrack patterns --- Moose Development/Moose/AI/AI_Air_Patrol.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/AI/AI_Air_Patrol.lua b/Moose Development/Moose/AI/AI_Air_Patrol.lua index 53e439f8a..2cdfb03d0 100644 --- a/Moose Development/Moose/AI/AI_Air_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Air_Patrol.lua @@ -307,11 +307,14 @@ function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) local CurrentCoord = AIPatrol:GetCoordinate() + local altitude= math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) + local ToTargetCoord = self.PatrolZone:GetRandomPointVec2() - ToTargetCoord:SetAlt( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) ) + ToTargetCoord:SetAlt( altitude ) self:SetTargetDistance( ToTargetCoord ) -- For RTB status check local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + local speedkmh=ToTargetSpeed local FromWP = CurrentCoord:WaypointAir( self.PatrolAltType or "RADIO", From f1c16def9aa93575d601d94d9940850c8f3f4a50 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 22 Oct 2019 23:25:30 +0200 Subject: [PATCH 387/485] Update AI_Air_Patrol.lua - fixed bug that route is not called for racetrack --- Moose Development/Moose/AI/AI_Air_Patrol.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Air_Patrol.lua b/Moose Development/Moose/AI/AI_Air_Patrol.lua index 2cdfb03d0..3f133f8fa 100644 --- a/Moose Development/Moose/AI/AI_Air_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Air_Patrol.lua @@ -386,12 +386,13 @@ function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_AIR_PATROL.___PatrolRoute", self ) PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) - AIPatrol:OptionROEReturnFire() - AIPatrol:OptionROTEvadeFire() - - AIPatrol:Route( PatrolRoute, self.TaskDelay ) end + AIPatrol:OptionROEReturnFire() + AIPatrol:OptionROTEvadeFire() + + AIPatrol:Route( PatrolRoute, self.TaskDelay ) + end end From ddf61d1bf3f1a2a68a91134489e03bcdec2a0956 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 31 Oct 2019 18:55:21 +0100 Subject: [PATCH 388/485] ARTY v1.1.5 - Fixed bug for tac nukes (critial mass insufficient to trigger nuclear fission). --- .../Moose/Functional/Artillery.lua | 2053 +++++++++-------- 1 file changed, 1034 insertions(+), 1019 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index dac84c96d..87d4d0a69 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -1,15 +1,15 @@ --- **Functional** - Control artillery units. --- +-- -- === --- +-- -- The ARTY class can be used to easily assign and manage targets for artillery units using an advanced queueing system. --- +-- -- ## Features: --- +-- -- * Multiple targets can be assigned. No restriction on number of targets. -- * Targets can be given a priority. Engagement of targets is executed a according to their priority. -- * Engagements can be scheduled, i.e. will be executed at a certain time of the day. --- * Multiple relocations of the group can be assigned and scheduled via queueing system. +-- * Multiple relocations of the group can be assigned and scheduled via queueing system. -- * Special weapon types can be selected for each attack, e.g. cruise missiles for Naval units. -- * Automatic rearming once the artillery is out of ammo (optional). -- * Automatic relocation after each firing engagement to prevent counter strikes (optional). @@ -18,17 +18,17 @@ -- * New targets can be added during the mission, e.g. when they are detected by recon units. -- * Targets and relocations can be assigned by placing markers on the F10 map. -- * Finite state machine implementation. Mission designer can interact when certain events occur. --- +-- -- ==== --- +-- -- ## [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) --- +-- -- === --- +-- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** --- +-- -- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) --- +-- -- ==== -- @module Functional.Arty -- @image Artillery.JPG @@ -37,6 +37,7 @@ --- ARTY class -- @type ARTY -- @field #string ClassName Name of the class. +-- @field #string lid Log id for DCS.log file. -- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. -- @field #table targets All targets assigned. -- @field #table moves All moves assigned. @@ -67,7 +68,7 @@ -- @field #number RearmingDistance Safe distance in meters between ARTY group and rearming group or place at which rearming is possible. Default 100 m. -- @field Wrapper.Group#GROUP RearmingGroup Unit designated to rearm the ARTY group. -- @field #number RearmingGroupSpeed Speed in km/h the rearming unit moves at. Default is 50% of the max speed possible of the group. --- @field #boolean RearmingGroupOnRoad If true, rearming group will move to ARTY group or rearming place using mainly roads. Default false. +-- @field #boolean RearmingGroupOnRoad If true, rearming group will move to ARTY group or rearming place using mainly roads. Default false. -- @field Core.Point#COORDINATE RearmingGroupCoord Initial coordinates of the rearming unit. After rearming complete, the unit will return to this position. -- @field Core.Point#COORDINATE RearmingPlaceCoord Coordinates of the rearming place. If the place is more than 100 m away from the ARTY group, the group will go there. -- @field #boolean RearmingArtyOnRoad If true, ARTY group will move to rearming place using mainly roads. Default false. @@ -106,53 +107,53 @@ --- Enables mission designers easily to assign targets for artillery units. Since the implementation is based on a Finite State Model (FSM), the mission designer can -- interact with the process at certain events or states. --- +-- -- A new ARTY object can be created with the @{#ARTY.New}(*group*) contructor. -- The parameter *group* has to be a MOOSE Group object and defines ARTY group. --- +-- -- The ARTY FSM process can be started by the @{#ARTY.Start}() command. -- -- ## The ARTY Process --- +-- -- ![Process](..\Presentations\ARTY\ARTY_Process.png) --- +-- -- ### Blue Branch -- After the FMS process is started the ARTY group will be in the state **CombatReady**. Once a target is assigned the **OpenFire** event will be triggered and the group starts -- firing. At this point the group in in the state **Firing**. -- When the defined number of shots has been fired on the current target the event **CeaseFire** is triggered. The group will stop firing and go back to the state **CombatReady**. -- If another target is defined (or multiple engagements of the same target), the cycle starts anew. --- +-- -- ### Violet Branch -- When the ARTY group runs out of ammunition, the event **Winchester** is triggered and the group enters the state **OutOfAmmo**. -- In this state, the group is unable to engage further targets. --- +-- -- ### Red Branch -- With the @{#ARTY.SetRearmingGroup}(*group*) command, a special group can be defined to rearm the ARTY group. If this unit has been assigned and the group has entered the state -- **OutOfAmmo** the event **Rearm** is triggered followed by a transition to the state **Rearming**. -- If the rearming group is less than 100 meters away from the ARTY group, the rearming process starts. If the rearming group is more than 100 meters away from the ARTY unit, the -- rearming group is routed to a point 20 to 100 m from the ARTY group. --- +-- -- Once the rearming is complete, the **Rearmed** event is triggered and the group enters the state **CombatReady**. At this point targeted can be engaged again. --- +-- -- ### Green Branch -- The ARTY group can be ordered to change its position via the @{#ARTY.AssignMoveCoord}() function as described below. When the group receives the command to move -- the event **Move** is triggered and the state changes to **Moving**. When the unit arrives to its destination the event **Arrived** is triggered and the group -- becomes **CombatReady** again. --- --- Note, that the ARTY group will not open fire while it is in state **Moving**. This property differentiates artillery from tanks. --- +-- +-- Note, that the ARTY group will not open fire while it is in state **Moving**. This property differentiates artillery from tanks. +-- -- ### Yellow Branch -- When a new target is assigned via the @{#ARTY.AssignTargetCoord}() function (see below), the **NewTarget** event is triggered. --- +-- -- ## Assigning Targets -- Assigning targets is a central point of the ARTY class. Multiple targets can be assigned simultanioulsly and are put into a queue. --- Of course, targets can be added at any time during the mission. For example, once they are detected by a reconnaissance unit. --- +-- Of course, targets can be added at any time during the mission. For example, once they are detected by a reconnaissance unit. +-- -- In order to add a target, the function @{#ARTY.AssignTargetCoord}(*coord*, *prio*, *radius*, *nshells*, *maxengage*, *time*, *weapontype*, *name*) has to be used. -- Only the first parameter *coord* is mandatory while all remaining parameters are all optional. --- +-- -- ### Parameters: --- +-- -- * *coord*: Coordinates of the target, given as @{Core.Point#COORDINATE} object. -- * *prio*: Priority of the target. This a number between 1 (high prio) and 100 (low prio). Targets with higher priority are engaged before targets with lower priority. -- * *radius*: Radius in meters which defines the area the ARTY group will attempt to be hitting. Default is 100 meters. @@ -168,64 +169,64 @@ -- For example, this is useful for naval units which carry a bigger arsenal (cannons and missiles). Default is Auto, i.e. DCS logic selects the appropriate weapon type. -- *name*: A special name can be defined for this target. Default name are the coordinates of the target in LL DMS format. If a name is already given for another target -- or the same target should be attacked two or more times with different parameters a suffix "#01", "#02", "#03" is automatically appended to the specified name. --- +-- -- ## Target Queue -- In case multiple targets have been defined, it is important to understand how the target queue works. --- +-- -- Here, the essential parameters are the priority *prio*, the number of engagements *maxengage* and the scheduled *time* as described above. --- +-- -- For example, we have assigned two targets one with *prio*=10 and the other with *prio*=50 and both targets should be engaged three times (*maxengage*=3). -- Let's first consider the case that none of the targets is scheduled to be executed at a certain time (*time*=nil). -- The ARTY group will first engage the target with higher priority (*prio*=10). After the engagement is finished, the target with lower priority is attacked. --- This is because the target with lower prio has been attacked one time less. After the attack on the lower priority task is finished and both targets +-- This is because the target with lower prio has been attacked one time less. After the attack on the lower priority task is finished and both targets -- have been engaged equally often, the target with the higher priority is engaged again. This coninues until a target has engaged three times. -- Once the maximum number of engagements is reached, the target is deleted from the queue. --- +-- -- In other words, the queue is first sorted with respect to the number of engagements and targets with the same number of engagements are sorted with -- respect to their priority. --- +-- -- ### Timed Engagements --- +-- -- As mentioned above, targets can be engaged at a specific time of the day via the *time* parameter. --- +-- -- If the *time* parameter is specified for a target, the first engagement of that target will happen at that time of the day and not before. -- This also applies when multiple engagements are requested via the *maxengage* parameter. The first attack will not happen before the specifed time. -- When that timed attack is finished, the *time* parameter is deleted and the remaining engagements are carried out in the same manner as for untimed targets (described above). --- +-- -- Of course, it can happen that a scheduled task should be executed at a time, when another target is already under attack. -- If the priority of the target is higher than the priority of the current target, then the current attack is cancelled and the engagement of the target with the higher -- priority is started. --- +-- -- By contrast, if the current target has a higher priority than the target scheduled at that time, the current attack is finished before the scheduled attack is started. --- +-- -- ## Determining the Amount of Ammo --- +-- -- In order to determin when a unit is out of ammo and possible initiate the rearming process it is necessary to know which types of weapons have to be counted. -- For most artillery unit types, this is simple because they only have one type of weapon and hence ammunition. --- +-- -- However, there are more complex scenarios. For example, naval units carry a big arsenal of different ammunition types ranging from various cannon shell types -- over surface-to-air missiles to cruise missiles. Obviously, not all of these ammo types can be employed for artillery tasks. --- +-- -- Unfortunately, there is no easy way to count only those ammo types useable as artillery. Therefore, to keep the implementation general the user -- can specify the names of the ammo types by the following functions: --- +-- -- * @{#ARTY.SetShellTypes}(*tableofnames*): Defines the ammo types for unguided cannons, e.g. *tableofnames*={"weapons.shells"}, i.e. **all** types of shells are counted. -- * @{#ARTY.SetRocketTypes}(*tableofnames*): Defines the ammo types of unguided rockets, e.g. *tableofnames*={"weapons.nurs"}, i.e. **all** types of rockets are counted. -- * @{#ARTY.SetMissileTypes}(*tableofnames*): Defines the ammo types of guided missiles, e.g. is *tableofnames*={"weapons.missiles"}, i.e. **all** types of missiles are counted. --- +-- -- **Note** that the default parameters "weapons.shells", "weapons.nurs", "weapons.missiles" **should in priciple** capture all the corresponding ammo types. -- However, the logic searches for the string "weapon.missies" in the ammo type. Especially for missiles, this string is often not contained in the ammo type descriptor. --- +-- -- 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. --- +-- 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. +-- -- ## 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. --- +-- -- The enumerator @{#ARTY.WeaponType} has been defined to select a certain weapon type. Supported values are: --- +-- -- * @{#ARTY.WeaponType}.Auto: Automatic weapon selection by the DCS logic. This is the default setting. -- * @{#ARTY.WeaponType}.Cannon: Only cannons are used during the attack. Corresponding ammo type are shells and can be defined by @{#ARTY.SetShellTypes}. -- * @{#ARTY.WeaponType}.Rockets: Only unguided are used during the attack. Corresponding ammo type are rockets/nurs and can be defined by @{#ARTY.SetRocketTypes}. @@ -233,96 +234,96 @@ -- * @{#ARTY.WeaponType}.TacticalNukes: Use tactical nuclear shells. This works only with units that have shells and is described below. -- * @{#ARTY.WeaponType}.IlluminationShells: Use illumination shells. This works only with units that have shells and is described below. -- * @{#ARTY.WeaponType}.SmokeShells: Use smoke shells. This works only with units that have shells and is described below. --- +-- -- ## Assigning Relocation Movements -- The ARTY group can be commanded to move. This is done by the @{#ARTY.AssignMoveCoord}(*coord*, *time*, *speed*, *onroad*, *cancel*, *name*) function. -- With this multiple timed moves of the group can be scheduled easily. By default, these moves will only be executed if the group is state **CombatReady**. --- +-- -- ### Parameters --- +-- -- * *coord*: Coordinates where the group should move to given as @{Core.Point#COORDINATE} object. -- * *time*: The time when the move should be executed. This has to be given as a string in the format "hh:mm:ss" (hh=hours, mm=minutes, ss=seconds). -- * *speed*: Speed of the group in km/h. -- * *onroad*: If this parameter is set to true, the group uses mainly roads to get to the commanded coordinates. -- * *cancel*: If set to true, any current engagement of targets is cancelled at the time the move should be executed. -- * *name*: Can be used to set a user defined name of the move. By default the name is created from the LL DMS coordinates. --- +-- -- ## Automatic Rearming --- +-- -- If an ARTY group runs out of ammunition, it can be rearmed automatically. --- +-- -- ### Rearming Group -- The first way to activate the automatic rearming is to define a rearming group with the function @{#ARTY.SetRearmingGroup}(*group*). For the blue side, this -- could be a M181 transport truck and for the red side an Ural-375 truck. --- +-- -- Once the ARTY group is out of ammo and the **Rearm** event is triggered, the defined rearming truck will drive to the ARTY group. -- So the rearming truck does not have to be placed nearby the artillery group. When the rearming is complete, the rearming truck will drive back to its original position. --- +-- -- ### Rearming Place -- The second alternative is to define a rearming place, e.g. a FRAP, airport or any other warehouse. This is done with the function @{#ARTY.SetRearmingPlace}(*coord*). -- The parameter *coord* specifies the coordinate of the rearming place which should not be further away then 100 meters from the warehouse. --- +-- -- When the **Rearm** event is triggered, the ARTY group will move to the rearming place. Of course, the group must be mobil. So for a mortar this rearming procedure would not work. --- +-- -- After the rearming is complete, the ARTY group will move back to its original position and resume normal operations. --- +-- -- ### Rearming Group **and** Rearming Place -- If both a rearming group *and* a rearming place are specified like described above, both the ARTY group and the rearming truck will move to the rearming place and meet there. --- +-- -- After the rearming is complete, both groups will move back to their original positions. --- +-- -- ## Simulated Weapons --- +-- -- In addtion to the standard weapons a group has available some special weapon types that are not possible to use in the native DCS environment are simulated. --- +-- -- ### Tactical Nukes --- +-- -- ARTY groups that can fire shells can also be used to fire tactical nukes. This is achieved by setting the weapon type to **ARTY.WeaponType.TacticalNukes** in the -- @{#ARTY.AssignTargetCoord}() function. -- -- By default, they group does not have any nukes available. To give the group the ability the function @{#ARTY.SetTacNukeShells}(*n*) can be used. -- This supplies the group with *n* nuclear shells, where *n* is restricted to the number of conventional shells the group can carry. --- Note that the group must always have convenctional shells left in order to fire a nuclear shell. --- +-- Note that the group must always have convenctional shells left in order to fire a nuclear shell. +-- -- The default explostion strength is 0.075 kilo tons TNT. The can be changed with the @{#ARTY.SetTacNukeWarhead}(*strength*), where *strength* is given in kilo tons TNT. --- +-- -- ### Illumination Shells --- --- ARTY groups that possess shells can fire shells with illumination bombs. First, the group needs to be equipped with this weapon. This is done by the +-- +-- ARTY groups that possess shells can fire shells with illumination bombs. First, the group needs to be equipped with this weapon. This is done by the -- function @{ARTY.SetIlluminationShells}(*n*, *power*), where *n* is the number of shells the group has available and *power* the illumination power in mega candela (mcd). --- +-- -- In order to execute an engagement with illumination shells one has to use the weapon type *ARTY.WeaponType.IlluminationShells* in the -- @{#ARTY.AssignTargetCoord}() function. --- +-- -- In the simulation, the explosive shell that is fired is destroyed once it gets close to the target point but before it can actually impact. -- At this position an illumination bomb is triggered at a random altitude between 500 and 1000 meters. This interval can be set by the function -- @{ARTY.SetIlluminationMinMaxAlt}(*minalt*, *maxalt*). --- +-- -- ### Smoke Shells --- +-- -- In a similar way to illumination shells, ARTY groups can also employ smoke shells. The numer of smoke shells the group has available is set by the function -- @{#ARTY.SetSmokeShells}(*n*, *color*), where *n* is the number of shells and *color* defines the smoke color. Default is SMOKECOLOR.Red. --- +-- -- The weapon type to be used in the @{#ARTY.AssignTargetCoord}() function is *ARTY.WeaponType.SmokeShells*. --- +-- -- The explosive shell the group fired is destroyed shortly before its impact on the ground and smoke of the speficied color is triggered at that position. -- --- +-- -- ## Assignments via Markers on F10 Map --- +-- -- Targets and relocations can be assigned by players via placing a mark on the F10 map. The marker text must contain certain keywords. --- +-- -- This feature can be turned on with the @{#ARTY.SetMarkAssignmentsOn}(*key*, *readonly*). The parameter *key* is optional. When set, it can be used as PIN, i.e. only -- players who know the correct key are able to assign and cancel targets or relocations. Default behavior is that all players belonging to the same coalition as the -- ARTY group are able to assign targets and moves without a key. --- +-- -- ### Target Assignments -- A new target can be assigned by writing **arty engage** in the marker text. -- This is followed by a **comma separated list** of (optional) keywords and parameters. -- First, it is important to address the ARTY group or groups that should engage. This can be done in numrous ways. The keywords are *battery*, *alias*, *cluster*. -- It is also possible to address all ARTY groups by the keyword *everyone* or *allbatteries*. These two can be used synonymously. -- **Note that**, if no battery is assigned nothing will happen. --- +-- -- * *everyone* or *allbatteries* The target is assigned to all batteries. -- * *battery* Name of the ARTY group that the target is assigned to. Note that **the name is case sensitive** and has to be given in quotation marks. Default is all ARTY groups of the right coalition. -- * *alias* Alias of the ARTY group that the target is assigned to. The alias is **case sensitive** and needs to be in quotation marks. @@ -337,7 +338,7 @@ -- * *lldms* Specify the coordinates in Lat/Long degrees, minutes and seconds format. The actual location of the marker is unimportant here. The group will engage the coordinates given in the lldms keyword. -- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below. This can be useful when coordinates in this format are obtained from elsewhere. -- * *readonly* The marker is readonly and cannot be deleted by users. Hence, assignment cannot be cancelled by removing the marker. --- +-- -- Here are examples of valid marker texts: -- arty engage, battery "Blue Paladin Alpha" -- arty engage, everyone @@ -351,14 +352,14 @@ -- arty engage, battery "Blue MRLS 1", key 666 -- arty engage, battery "Paladin Alpha", weapon nukes, shots 1, time 20:15 -- arty engage, battery "Horwitzer 1", lldms 41:51:00N 41:47:58E --- +-- -- Note that the keywords and parameters are *case insensitve*. Only exception are the battery, alias and cluster names. -- These must be exactly the same as the names of the goups defined in the mission editor or the aliases and cluster names defined in the script. --- +-- -- ### Relocation Assignments --- +-- -- Markers can also be used to relocate the group with the keyphrase **arty move**. This is done in a similar way as assigning targets. Here, the (optional) keywords and parameters are: --- +-- -- * *time* Time for which which the relocation/move is schedules, e.g. 08:42. Default is as soon as possible. -- * *speed* The speed in km/h the group will drive at. Default is 70% of its max possible speed. -- * *on road* Group will use mainly roads. Default is off, i.e. it will go in a straight line from its current position to the assigned coordinate. @@ -368,9 +369,9 @@ -- * *cluster* The cluster of ARTY groups that is addessed. Clusters can be defined by the function @{#ARTY.AddToCluster}(*clusters*). Names are **case sensitive** and need to be in quotation marks. -- * *key* A number to authorize the target assignment. Only specifing the correct number will trigger an engagement. -- * *lldms* Specify the coordinates in Lat/Long degrees, minutes and seconds format. The actual location of the marker is unimportant. The group will move to the coordinates given in the lldms keyword. --- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below. +-- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below. -- * *readonly* Marker cannot be deleted by users any more. Hence, assignment cannot be cancelled by removing the marker. --- +-- -- Here are some examples: -- arty move, battery "Blue Paladin" -- arty move, battery "Blue MRLS", canceltarget, speed 10, on road @@ -380,55 +381,55 @@ -- arty move, cluster "Northern Batteries" "Southern Batteries" -- arty move, cluster "Northern Batteries", cluster "Southern Batteries" -- arty move, everyone --- +-- -- ### Requests --- +-- -- Marks can also be to send requests to the ARTY group. This is done by the keyword **arty request**, which can have the keywords --- +-- -- * *target* All assigned targets are reported. -- * *move* All assigned relocation moves are reported. -- * *ammo* Current ammunition status is reported. --- +-- -- For example -- arty request, everyone, ammo -- arty request, battery "Paladin Bravo", targets -- arty request, cluster "All Mortars", move --- +-- -- The actual location of the marker is irrelevant for these requests. --- +-- -- ### Cancel --- +-- -- Current actions can be cancelled by the keyword **arty cancel**. Actions that can be cancelled are current engagements, relocations and rearming assignments. --- +-- -- For example -- arty cancel, target, battery "Paladin Bravo" -- arty cancel, everyone, move -- arty cancel, rearming, battery "MRLS Charly" --- +-- -- ### Settings --- +-- -- A few options can be set by marks. The corresponding keyword is **arty set**. This can be used to define the rearming place and group for a battery. --- +-- -- To set the reamring place of a group at the marker position type -- arty set, battery "Paladin Alpha", rearming place --- +-- -- Setting the rearming group is independent of the position of the mark. Just create one anywhere on the map and type -- arty set, battery "Mortar Bravo", rearming group "Ammo Truck M818" -- Note that the name of the rearming group has to be given in quotation marks and spellt exactly as the group name defined in the mission editor. --- +-- -- ## Transporting --- --- ARTY groups can be transported to another location as @{Cargo.Cargo} by means of classes such as @{AI.AI_Cargo_APC}, @{AI.AI_Cargo_Dispatcher_APC}, +-- +-- ARTY groups can be transported to another location as @{Cargo.Cargo} by means of classes such as @{AI.AI_Cargo_APC}, @{AI.AI_Cargo_Dispatcher_APC}, -- @{AI.AI_Cargo_Helicopter}, @{AI.AI_Cargo_Dispatcher_Helicopter} or @{AI.AI_Cargo_Airplane}. --- +-- -- In order to do this, one needs to define an ARTY object via the @{#ARTY.NewFromCargoGroup}(*cargogroup*, *alias*) function. -- The first argument *cargogroup* has to be a @{Cargo.CargoGroup#CARGO_GROUP} object. The second argument *alias* is a string which can be freely chosen by the user. --- +-- -- ## Fine Tuning --- +-- -- The mission designer has a few options to tailor the ARTY object according to his needs. --- --- * @{#ARTY.SetAutoRelocateToFiringRange}(*maxdist*, *onroad*) lets the ARTY group automatically move to within firing range if a current target is outside the min/max firing range. The +-- +-- * @{#ARTY.SetAutoRelocateToFiringRange}(*maxdist*, *onroad*) lets the ARTY group automatically move to within firing range if a current target is outside the min/max firing range. The -- optional parameter *maxdist* is the maximum distance im km the group will move. If the distance is greater no relocation is performed. Default is 50 km. -- * @{#ARTY.SetAutoRelocateAfterEngagement}(*rmax*, *rmin*) will cause the ARTY group to change its position after each firing assignment. -- Optional parameters *rmax*, *rmin* define the max/min distance for relocation of the group. Default distance is randomly between 300 and 800 m. @@ -443,68 +444,68 @@ -- * @{#ARTY.SetWaitForShotTime}(*waittime*) sets the time after which a target is deleted from the queue if no shooting event occured after the target engagement started. -- Default is 300 seconds. Note that this can for example happen, when the assigned target is out of range. -- * @{#ARTY.SetDebugON}() and @{#ARTY.SetDebugOFF}() can be used to enable/disable the debug mode. --- +-- -- ## Examples --- +-- -- ### Assigning Multiple Targets -- This basic example illustrates how to assign multiple targets and defining a rearming group. -- -- Creat a new ARTY object from a Paladin group. -- paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) --- +-- -- -- Define a rearming group. This is a Transport M818 truck. -- paladin:SetRearmingGroup(GROUP:FindByName("Blue Ammo Truck")) --- +-- -- -- Set the max firing range. A Paladin unit has a range of 20 km. -- paladin:SetMaxFiringRange(20) --- +-- -- -- Low priorty (90) target, will be engage last. Target is engaged two times. At each engagement five shots are fired. -- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 3"):GetCoordinate(), 90, nil, 5, 2) -- -- Medium priorty (nil=50) target, will be engage second. Target is engaged two times. At each engagement ten shots are fired. -- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), nil, nil, 10, 2) -- -- High priorty (10) target, will be engage first. Target is engaged three times. At each engagement twenty shots are fired. -- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 2"):GetCoordinate(), 10, nil, 20, 3) --- +-- -- -- Start ARTY process. -- paladin:Start() -- **Note** --- +-- -- * If a parameter should be set to its default value, it has to be set to *nil* if other non-default parameters follow. Parameters at the end can simply be skiped. --- * In this example, the target coordinates are taken from groups placed in the mission edit using the COORDINATE:GetCoordinate() function. --- +-- * In this example, the target coordinates are taken from groups placed in the mission edit using the COORDINATE:GetCoordinate() function. +-- -- ### Scheduled Engagements -- -- Mission starts at 8 o'clock. -- -- Assign two scheduled targets. --- +-- -- -- Create ARTY object from Paladin group. -- paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) --- +-- -- -- Assign target coordinates. Priority=50 (medium), radius=100 m, use 5 shells per engagement, engage 1 time at two past 8 o'clock. -- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 50, 100, 5, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1") --- +-- -- -- Assign target coordinates. Priority=10 (high), radius=300 m, use 10 shells per engagement, engage 1 time at seven past 8 o'clock. -- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 2"):GetCoordinate(), 10, 300, 10, 1, "08:07:00", ARTY.WeaponType.Auto, "Target 2") --- +-- -- -- Start ARTY process. -- paladin:Start() --- +-- -- ### Specific Weapons -- This example demonstrates how to use specific weapons during an engagement. -- -- Define the Normandy as ARTY object. -- normandy=ARTY:New(GROUP:FindByName("Normandy")) --- +-- -- -- Add target: prio=50, radius=300 m, number of missiles=20, number of engagements=1, start time=08:05 hours, only use cruise missiles for this attack. -- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 20, 300, 50, 1, "08:01:00", ARTY.WeaponType.CruiseMissile) --- +-- -- -- Add target: prio=50, radius=300 m, number of shells=100, number of engagements=1, start time=08:15 hours, only use cannons during this attack. -- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 50, 300, 100, 1, "08:15:00", ARTY.WeaponType.Cannon) --- +-- -- -- Define shells that are counted to check whether the ship is out of ammo. -- -- Note that this is necessary because the Normandy has a lot of other shell type weapons which cannot be used to engage ground targets in an artillery style manner. -- normandy:SetShellTypes({"MK45_127"}) --- +-- -- -- Define missile types that are counted. -- normandy:SetMissileTypes({"BGM"}) --- +-- -- -- Start ARTY process. -- normandy:Start() -- @@ -512,13 +513,13 @@ -- This example demonstates how an ARTY group can be transported to another location as cargo. -- -- Define a group as CARGO_GROUP -- CargoGroupMortars=CARGO_GROUP:New(GROUP:FindByName("Mortars"), "Mortars", "Mortar Platoon Alpha", 100 , 10) --- +-- -- -- Define the mortar CARGO GROUP as ARTY object -- mortars=ARTY:NewFromCargoGroup(CargoGroupMortars, "Mortar Platoon Alpha") --- +-- -- -- Start ARTY process -- mortars:Start() --- +-- -- -- Setup AI cargo dispatcher for e.g. helos -- SetHeloCarriers = SET_GROUP:New():FilterPrefixes("CH-47D"):FilterStart() -- SetCargoMortars = SET_CARGO:New():FilterTypes("Mortars"):FilterStart() @@ -530,6 +531,7 @@ -- @field #ARTY ARTY={ ClassName="ARTY", + lid=nil, Debug=false, targets={}, moves={}, @@ -689,13 +691,9 @@ ARTY.db={ -- @field #number Tassigned Abs. mission time when target was assigned. -- @field #boolean attackgroup If true, use task attack group rather than fire at point for engagement. ---- Some ID to identify who we are in output of the DCS.log file. --- @field #string id -ARTY.id="ARTY | " - --- Arty script version. -- @field #string version -ARTY.version="1.1.3" +ARTY.version="1.1.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -727,152 +725,134 @@ ARTY.version="1.1.3" --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Creates a new ARTY object from a MOOSE CARGO_GROUP object. --- @param #ARTY self --- @param Cargo.CargoGroup#CARGO_GROUP cargogroup The CARGO GROUP object for which artillery tasks should be assigned. --- @param alias (Optional) Alias name the group will be calling itself when sending messages. Default is the group name. --- @return #ARTY ARTY object or nil if group does not exist or is not a ground or naval group. -function ARTY:NewFromCargoGroup(cargogroup, alias) - BASE:F2({cargogroup=cargogroup, alias=alias}) - - if cargogroup then - BASE:T(ARTY.id..string.format("ARTY script version %s. Added CARGO group %s.", ARTY.version, cargogroup:GetName())) - else - BASE:E(ARTY.id.."ERROR: Requested ARTY CARGO GROUP does not exist! (Has to be a MOOSE CARGO(!) group.)") - return nil - end - - -- Get group belonging to the cargo group. - local group=cargogroup:GetObject() - - -- Create ARTY object. - local arty=ARTY:New(group,alias) - - -- Set iscargo flag. - arty.iscargo=true - - -- Set cargo group object. - arty.cargogroup=cargogroup - - return arty -end - --- Creates a new ARTY object from a MOOSE group object. -- @param #ARTY self -- @param Wrapper.Group#GROUP group The GROUP object for which artillery tasks should be assigned. -- @param alias (Optional) Alias name the group will be calling itself when sending messages. Default is the group name. -- @return #ARTY ARTY object or nil if group does not exist or is not a ground or naval group. function ARTY:New(group, alias) - BASE:F2({group=group, alias=alias}) -- Inherits from FSM_CONTROLLABLE local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #ARTY + -- If group name was given. + if type(group)=="string" then + self.groupname=group + group=GROUP:FindByName(group) + if not group then + self:E(string.format("ERROR: Requested ARTY group %s does not exist! (Has to be a MOOSE group.)", self.groupname)) + return nil + end + end + -- Check that group is present. if group then - self:T(ARTY.id..string.format("ARTY script version %s. Added group %s.", ARTY.version, group:GetName())) + self:T(string.format("ARTY script version %s. Added group %s.", ARTY.version, group:GetName())) else - self:E(ARTY.id.."ERROR: Requested ARTY group does not exist! (Has to be a MOOSE group.)") + self:E("ERROR: Requested ARTY group does not exist! (Has to be a MOOSE group.)") return nil end - + -- Check that we actually have a GROUND group. if not (group:IsGround() or group:IsShip()) then - self:E(ARTY.id..string.format("ERROR: ARTY group %s has to be a GROUND or SHIP group!", group:GetName())) + self:E(string.format("ERROR: ARTY group %s has to be a GROUND or SHIP group!", group:GetName())) return nil end - + -- Set the controllable for the FSM. self:SetControllable(group) - + -- Set the group name self.groupname=group:GetName() - + -- Get coalition. self.coalition=group:GetCoalition() - + -- Set an alias name. if alias~=nil then self.alias=tostring(alias) else self.alias=self.groupname end - + + -- Log id. + self.lid=string.format("ARTY %s | ", self.alias) + -- Set the initial coordinates of the ARTY group. self.InitialCoord=group:GetCoordinate() - + -- Get DCS descriptors of group. local DCSgroup=Group.getByName(group:GetName()) local DCSunit=DCSgroup:getUnit(1) self.DCSdesc=DCSunit:getDesc() -- DCS descriptors. - self:T3(ARTY.id.."DCS descriptors for group "..group:GetName()) + self:T3(self.lid.."DCS descriptors for group "..group:GetName()) for id,desc in pairs(self.DCSdesc) do self:T3({id=id, desc=desc}) end - + -- Maximum speed in km/h. self.SpeedMax=group:GetSpeedMax() - + -- Group is mobile or not (e.g. mortars). if self.SpeedMax>1 then self.ismobile=true else self.ismobile=false end - + -- Set speed to 0.7 of maximum. self.Speed=self.SpeedMax * 0.7 - + -- Displayed name (similar to type name below) self.DisplayName=self.DCSdesc.displayName - + -- Is this infantry or not. self.IsArtillery=DCSunit:hasAttribute("Artillery") - + -- Type of group. self.Type=group:GetTypeName() - + -- Initial group strength. self.IniGroupStrength=#group:GetUnits() - --------------- + --------------- -- Transitions: --------------- -- Entry. self:AddTransition("*", "Start", "CombatReady") - + -- Blue branch. self:AddTransition("CombatReady", "OpenFire", "Firing") self:AddTransition("Firing", "CeaseFire", "CombatReady") - + -- Violett branch. self:AddTransition("CombatReady", "Winchester", "OutOfAmmo") - -- Red branch. + -- Red branch. self:AddTransition({"CombatReady", "OutOfAmmo"}, "Rearm", "Rearming") self:AddTransition("Rearming", "Rearmed", "Rearmed") - + -- Green branch. self:AddTransition("*", "Move", "Moving") self:AddTransition("Moving", "Arrived", "Arrived") - + -- Yellow branch. self:AddTransition("*", "NewTarget", "*") - + -- Not in diagram. self:AddTransition("*", "CombatReady", "CombatReady") self:AddTransition("*", "Status", "*") self:AddTransition("*", "NewMove", "*") self:AddTransition("*", "Dead", "*") self:AddTransition("*", "Respawn", "CombatReady") - + -- Transport as cargo (not in diagram). self:AddTransition("*", "Loaded", "InTransit") self:AddTransition("InTransit", "UnLoaded", "CombatReady") - + -- Unknown transitons. To be checked if adding these causes problems. self:AddTransition("Rearming", "Arrived", "Rearming") self:AddTransition("Rearming", "Move", "Rearming") @@ -886,7 +866,7 @@ function ARTY:New(group, alias) -- @param #string Event Event. -- @param #string To To state. -- @param #table target Array holding the target info. - + --- User function for OnAfter "OpenFire" event. -- @function [parent=#ARTY] OnAfterOpenFire -- @param #ARTY self @@ -1040,7 +1020,7 @@ function ARTY:New(group, alias) --- Function to start the ARTY FSM process. -- @function [parent=#ARTY] Start -- @param #ARTY self - + --- Function to start the ARTY FSM process after a delay. -- @function [parent=#ARTY] __Start -- @param #ARTY self @@ -1049,7 +1029,7 @@ function ARTY:New(group, alias) --- Function to update the status of the ARTY group and tigger FSM events. Triggers the FSM event "Status". -- @function [parent=#ARTY] Status -- @param #ARTY self - + --- Function to update the status of the ARTY group and tigger FSM events after a delay. Triggers the FSM event "Status". -- @function [parent=#ARTY] __Status -- @param #ARTY self @@ -1059,7 +1039,7 @@ function ARTY:New(group, alias) -- @function [parent=#ARTY] Dead -- @param #ARTY self -- @param #string unitname Name of the unit that died. - + --- Function called when a unit of the ARTY group died after a delay. Triggers the FSM event "Dead". -- @function [parent=#ARTY] __Dead -- @param #ARTY self @@ -1129,7 +1109,7 @@ function ARTY:New(group, alias) -- @function [parent=#ARTY] __Arrived -- @param #ARTY self -- @param #number delay Delay in seconds. - + --- Tell ARTY group it is combat ready. Triggers the FSM event "CombatReady". -- @function [parent=#ARTY] CombatReady -- @param #ARTY self @@ -1137,7 +1117,7 @@ function ARTY:New(group, alias) --- Tell ARTY group it is combat ready after a delay. Triggers the FSM event "CombatReady". -- @function [parent=#ARTY] __CombatReady -- @param #ARTY self - -- @param #number delay Delay in seconds. + -- @param #number delay Delay in seconds. --- Tell ARTY group it is out of ammo. Triggers the FSM event "Winchester". -- @function [parent=#ARTY] Winchester @@ -1146,7 +1126,7 @@ function ARTY:New(group, alias) --- Tell ARTY group it is out of ammo after a delay. Triggers the FSM event "Winchester". -- @function [parent=#ARTY] __Winchester -- @param #ARTY self - -- @param #number delay Delay in seconds. + -- @param #number delay Delay in seconds. --- Respawn ARTY group. -- @function [parent=#ARTY] Respawn @@ -1155,11 +1135,43 @@ function ARTY:New(group, alias) --- Respawn ARTY group after a delay. -- @function [parent=#ARTY] __Respawn -- @param #ARTY self - -- @param #number delay Delay in seconds. + -- @param #number delay Delay in seconds. return self end +--- Creates a new ARTY object from a MOOSE CARGO_GROUP object. +-- @param #ARTY self +-- @param Cargo.CargoGroup#CARGO_GROUP cargogroup The CARGO GROUP object for which artillery tasks should be assigned. +-- @param alias (Optional) Alias name the group will be calling itself when sending messages. Default is the group name. +-- @return #ARTY ARTY object or nil if group does not exist or is not a ground or naval group. +function ARTY:NewFromCargoGroup(cargogroup, alias) + BASE:F2({cargogroup=cargogroup, alias=alias}) + + if cargogroup then + BASE:T(self.lid..string.format("ARTY script version %s. Added CARGO group %s.", ARTY.version, cargogroup:GetName())) + else + BASE:E(self.lid.."ERROR: Requested ARTY CARGO GROUP does not exist! (Has to be a MOOSE CARGO(!) group.)") + return nil + end + + -- Get group belonging to the cargo group. + local group=cargogroup:GetObject() + + -- Create ARTY object. + local arty=ARTY:New(group,alias) + + -- Set iscargo flag. + arty.iscargo=true + + -- Set cargo group object. + arty.cargogroup=cargogroup + + return arty +end + + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- User Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1181,7 +1193,7 @@ end -- paladin:Start() function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, weapontype, name, unique) self:F({coord=coord, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage, time=time, weapontype=weapontype, name=name, unique=unique}) - + -- Set default values. nshells=nshells or 5 radius=radius or 100 @@ -1210,26 +1222,26 @@ function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, w else text="ERROR: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter!" MESSAGE:New(text, 30):ToAll() - self:E(ARTY.id..text) + self:E(self.lid..text) return nil end if text~=nil then - self:E(ARTY.id..text) + self:E(self.lid..text) end - + -- Name of the target. - local _name=name or coord:ToStringLLDMS() + local _name=name or coord:ToStringLLDMS() local _unique=true - + -- Check if the name has already been used for another target. If so, the function returns a new unique name. _name,_unique=self:_CheckName(self.targets, _name, not unique) - + -- Target name should be unique and is not. if unique==true and _unique==false then - self:T(ARTY.id..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name)) + self:T(self.lid..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name)) return nil end - + -- Time in seconds. local _time if type(time)=="string" then @@ -1239,16 +1251,16 @@ function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, w else _time=timer.getAbsTime() end - + -- Prepare target array. local _target={name=_name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio, maxengage=maxengage, time=_time, weapontype=weapontype} - + -- Add to table. table.insert(self.targets, _target) - + -- Trigger new target event. self:__NewTarget(1, _target) - + return _name end @@ -1268,7 +1280,7 @@ end -- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 10, 300, 10, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1") -- paladin:Start() function ARTY:AssignAttackGroup(group, prio, radius, nshells, maxengage, time, weapontype, name, unique) - + -- Set default values. nshells=nshells or 5 radius=radius or 100 @@ -1285,24 +1297,24 @@ function ARTY:AssignAttackGroup(group, prio, radius, nshells, maxengage, time, w if type(group)=="string" then group=GROUP:FindByName(group) end - + if group and group:IsAlive() then - + local coord=group:GetCoordinate() - + -- Name of the target. local _name=group:GetName() local _unique=true - + -- Check if the name has already been used for another target. If so, the function returns a new unique name. _name,_unique=self:_CheckName(self.targets, _name, not unique) - + -- Target name should be unique and is not. if unique==true and _unique==false then - self:T(ARTY.id..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name)) + self:T(self.lid..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name)) return nil end - + -- Time in seconds. local _time if type(time)=="string" then @@ -1312,7 +1324,7 @@ function ARTY:AssignAttackGroup(group, prio, radius, nshells, maxengage, time, w else _time=timer.getAbsTime() end - + -- Prepare target array. local target={} --#ARTY.Target target.attackgroup=true @@ -1326,18 +1338,18 @@ function ARTY:AssignAttackGroup(group, prio, radius, nshells, maxengage, time, w target.time=_time target.maxengage=maxengage target.weapontype=weapontype - + -- Add to table. table.insert(self.targets, target) - + -- Trigger new target event. self:__NewTarget(1, target) - + return _name else self:E("ERROR: Group does not exist!") end - + return nil end @@ -1355,31 +1367,31 @@ end -- @return #string Name of the move. Can be used for further reference, e.g. deleting the move from the list. function ARTY:AssignMoveCoord(coord, time, speed, onroad, cancel, name, unique) self:F({coord=coord, time=time, speed=speed, onroad=onroad, cancel=cancel, name=name, unique=unique}) - + -- Reject move if the group is immobile. if not self.ismobile then - self:T(ARTY.id..string.format("%s: group is immobile. Rejecting move request!", self.groupname)) - return nil + self:T(self.lid..string.format("%s: group is immobile. Rejecting move request!", self.groupname)) + return nil end - + -- Default if unique==nil then unique=false end - + -- Name of the target. local _name=name or coord:ToStringLLDMS() local _unique=true - + -- Check if the name has already been used for another target. If so, the function returns a new unique name. _name,_unique=self:_CheckName(self.moves, _name, not unique) - + -- Move name should be unique and is not. if unique==true and _unique==false then - self:T(ARTY.id..string.format("%s: move %s should have a unique name but name was already given. Rejecting move!", self.groupname, _name)) + self:T(self.lid..string.format("%s: move %s should have a unique name but name was already given. Rejecting move!", self.groupname, _name)) return nil end - + -- Set speed. if speed then -- Make sure, given speed is less than max physiaclly possible speed of group. @@ -1389,7 +1401,7 @@ function ARTY:AssignMoveCoord(coord, time, speed, onroad, cancel, name, unique) else speed=self.SpeedMax*0.7 end - + -- Default is off road. if onroad==nil then onroad=false @@ -1399,7 +1411,7 @@ function ARTY:AssignMoveCoord(coord, time, speed, onroad, cancel, name, unique) if cancel==nil then cancel=false end - + -- Time in seconds. local _time if type(time)=="string" then @@ -1409,13 +1421,13 @@ function ARTY:AssignMoveCoord(coord, time, speed, onroad, cancel, name, unique) else _time=timer.getAbsTime() end - + -- Prepare move array. local _move={name=_name, coord=coord, time=_time, speed=speed, onroad=onroad, cancel=cancel} - + -- Add to table. table.insert(self.moves, _move) - + return _name end @@ -1435,16 +1447,16 @@ end -- @return self function ARTY:AddToCluster(clusters) self:F({clusters=clusters}) - + -- Convert input to table. local names if type(clusters)=="table" then - names=clusters + names=clusters elseif type(clusters)=="string" then names={clusters} else -- error message - self:E(ARTY.id.."ERROR: Input parameter must be a string or a table in ARTY:AddToCluster()!") + self:E(self.lid.."ERROR: Input parameter must be a string or a table in ARTY:AddToCluster()!") return end @@ -1452,7 +1464,7 @@ function ARTY:AddToCluster(clusters) for _,cluster in pairs(names) do table.insert(self.clusters, cluster) end - + return self end @@ -1526,7 +1538,7 @@ function ARTY:SetRearmingGroupSpeed(speed) return self end ---- Define if rearming group uses mainly roads to drive to the ARTY group or rearming place. +--- Define if rearming group uses mainly roads to drive to the ARTY group or rearming place. -- @param #ARTY self -- @param #boolean onroad If true, rearming group uses mainly roads. If false, it drives directly to the ARTY group or rearming place. -- @return self @@ -1539,7 +1551,7 @@ function ARTY:SetRearmingGroupOnRoad(onroad) return self end ---- Define if ARTY group uses mainly roads to drive to the rearming place. +--- Define if ARTY group uses mainly roads to drive to the rearming place. -- @param #ARTY self -- @param #boolean onroad If true, ARTY group uses mainly roads. If false, it drives directly to the rearming place. -- @return self @@ -1566,7 +1578,7 @@ end -- @param #ARTY self -- @param #number maxdistance (Optional) The maximum distance in km the group will travel to get within firing range. Default is 50 km. No automatic relocation is performed if targets are assigned which are further away. -- @param #boolean onroad (Optional) If true, ARTY group uses roads whenever possible. Default false, i.e. group will move in a straight line to the assigned coordinate. --- @return self +-- @return self function ARTY:SetAutoRelocateToFiringRange(maxdistance, onroad) self:F({distance=maxdistance, onroad=onroad}) self.autorelocate=true @@ -1588,10 +1600,10 @@ function ARTY:SetAutoRelocateAfterEngagement(rmax, rmin) self.relocateafterfire=true self.relocateRmax=rmax or 800 self.relocateRmin=rmin or 300 - + -- Ensure that Rmin<=Rmax self.relocateRmin=math.min(self.relocateRmin, self.relocateRmax) - + return self end @@ -1651,26 +1663,26 @@ end -- @param #string name Name of the target. function ARTY:RemoveTarget(name) self:F2(name) - + -- Get target ID from namd local id=self:_GetTargetIndexByName(name) - + if id then - + -- Remove target from table. - self:T(ARTY.id..string.format("Group %s: Removing target %s (id=%d).", self.groupname, name, id)) + self:T(self.lid..string.format("Group %s: Removing target %s (id=%d).", self.groupname, name, id)) table.remove(self.targets, id) - + -- Delete marker belonging to this engagement. if self.markallow then local batteryname,markTargetID, markMoveID=self:_GetMarkIDfromName(name) if batteryname==self.groupname and markTargetID~=nil then COORDINATE:RemoveMark(markTargetID) - end + end end - + end - self:T(ARTY.id..string.format("Group %s: Number of targets = %d.", self.groupname, #self.targets)) + self:T(self.lid..string.format("Group %s: Number of targets = %d.", self.groupname, #self.targets)) end --- Delete a move from move list. @@ -1678,16 +1690,16 @@ end -- @param #string name Name of the target. function ARTY:RemoveMove(name) self:F2(name) - + -- Get move ID from name. local id=self:_GetMoveIndexByName(name) - + if id then - + -- Remove move from table. - self:T(ARTY.id..string.format("Group %s: Removing move %s (id=%d).", self.groupname, name, id)) + self:T(self.lid..string.format("Group %s: Removing move %s (id=%d).", self.groupname, name, id)) table.remove(self.moves, id) - + -- Delete marker belonging to this relocation move. if self.markallow then local batteryname,markTargetID,markMoveID=self:_GetMarkIDfromName(name) @@ -1695,9 +1707,9 @@ function ARTY:RemoveMove(name) COORDINATE:RemoveMark(markMoveID) end end - + end - self:T(ARTY.id..string.format("Group %s: Number of moves = %d.", self.groupname, #self.moves)) + self:T(self.lid..string.format("Group %s: Number of moves = %d.", self.groupname, #self.moves)) end --- Delete ALL targets from current target list. @@ -1782,7 +1794,7 @@ function ARTY:SetIlluminationShells(n, power) end --- Set minimum and maximum detotation altitude for illumination shells. A value between min/max is selected randomly. --- The illumination bomb will burn for 300 seconds (5 minutes). Assuming a descent rate of ~3 m/s the "optimal" altitude would be 900 m. +-- The illumination bomb will burn for 300 seconds (5 minutes). Assuming a descent rate of ~3 m/s the "optimal" altitude would be 900 m. -- @param #ARTY self -- @param #number minalt (Optional) Minium altitude in meters. Default 500 m. -- @param #number maxalt (Optional) Maximum altitude in meters. Default 1000 m. @@ -1790,7 +1802,7 @@ end function ARTY:SetIlluminationMinMaxAlt(minalt, maxalt) self.illuMinalt=minalt or 500 self.illuMaxalt=maxalt or 1000 - + if self.illuMinalt>self.illuMaxalt then self.illuMinalt=self.illuMaxalt end @@ -1856,15 +1868,15 @@ end -- @param #string To To state. function ARTY:onafterStart(Controllable, From, Event, To) self:_EventFromTo("onafterStart", Event, From, To) - + -- Debug output. local text=string.format("Started ARTY version %s for group %s.", ARTY.version, Controllable:GetName()) - self:E(ARTY.id..text) + self:I(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - + -- Get Ammo. self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0=self:GetAmmo(self.Debug) - + -- Init nuclear explosion parameters if they were not set by user. if self.nukerange==nil then self.nukerange=1500/75000*self.nukewarhead -- linear dependence @@ -1872,7 +1884,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) if self.nukefires==nil then self.nukefires=20/1000/1000*self.nukerange*self.nukerange end - + -- Init nuclear shells. if self.Nukes~=nil then self.Nukes0=math.min(self.Nukes, self.Nshells0) @@ -1880,7 +1892,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) self.Nukes=0 self.Nukes0=0 end - + -- Init illumination shells. if self.Nillu~=nil then self.Nillu0=math.min(self.Nillu, self.Nshells0) @@ -1888,15 +1900,15 @@ function ARTY:onafterStart(Controllable, From, Event, To) self.Nillu=0 self.Nillu0=0 end - + -- Init smoke shells. if self.Nsmoke~=nil then self.Nsmoke0=math.min(self.Nsmoke, self.Nshells0) else self.Nsmoke=0 self.Nsmoke0=0 - end - + end + -- Check if we have and arty type that is in the DB. local _dbproperties=self:_CheckDB(self.DisplayName) self:T({dbproperties=_dbproperties}) @@ -1906,7 +1918,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) self[property]=value end end - + -- Some mobility consitency checks if group cannot move. if not self.ismobile then self.RearmingPlaceCoord=nil @@ -1914,35 +1926,35 @@ function ARTY:onafterStart(Controllable, From, Event, To) self.autorelocate=false --self.RearmingGroupSpeed=20 end - + -- Check that default speed is below max speed. self.Speed=math.min(self.Speed, self.SpeedMax) -- Set Rearming group speed if not specified by user if self.RearmingGroup then - + -- Get max speed of rearming group. local speedmax=self.RearmingGroup:GetSpeedMax() - self:T(ARTY.id..string.format("%s, rearming group %s max speed = %.1f km/h.", self.groupname, self.RearmingGroup:GetName(), speedmax)) - + self:T(self.lid..string.format("%s, rearming group %s max speed = %.1f km/h.", self.groupname, self.RearmingGroup:GetName(), speedmax)) + if self.RearmingGroupSpeed==nil then -- Set rearming group speed to 50% of max possible speed. self.RearmingGroupSpeed=speedmax*0.5 else - -- Ensure that speed is <= max speed. + -- Ensure that speed is <= max speed. self.RearmingGroupSpeed=math.min(self.RearmingGroupSpeed, self.RearmingGroup:GetSpeedMax()) end else -- Just to have a reasonable number for output format below. self.RearmingGroupSpeed=23 end - + local text=string.format("\n******************************************************\n") text=text..string.format("Arty group = %s\n", self.groupname) text=text..string.format("Arty alias = %s\n", self.alias) text=text..string.format("Artillery attribute = %s\n", tostring(self.IsArtillery)) text=text..string.format("Type = %s\n", self.Type) - text=text..string.format("Display Name = %s\n", self.DisplayName) + text=text..string.format("Display Name = %s\n", self.DisplayName) text=text..string.format("Number of units = %d\n", self.IniGroupStrength) text=text..string.format("Speed max = %d km/h\n", self.SpeedMax) text=text..string.format("Speed default = %d km/h\n", self.Speed) @@ -1961,9 +1973,9 @@ function ARTY:onafterStart(Controllable, From, Event, To) text=text..string.format("Number of illum. = %d\n", self.Nillu0) text=text..string.format("Illuminaton Power = %.3f mcd\n", self.illuPower/1000000) text=text..string.format("Illuminaton Minalt = %d m\n", self.illuMinalt) - text=text..string.format("Illuminaton Maxalt = %d m\n", self.illuMaxalt) + text=text..string.format("Illuminaton Maxalt = %d m\n", self.illuMaxalt) text=text..string.format("Number of smoke = %d\n", self.Nsmoke0) - text=text..string.format("Smoke color = %d\n", self.smokeColor) + text=text..string.format("Smoke color = %d\n", self.smokeColor) if self.RearmingGroup or self.RearmingPlaceCoord then text=text..string.format("Rearming safe dist. = %d m\n", self.RearmingDistance) end @@ -1996,7 +2008,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) text=text..string.format("- %s\n", self:_TargetInfo(target)) local possible=self:_CheckWeaponTypePossible(target) if not possible then - self:E(ARTY.id..string.format("WARNING: Selected weapon type %s is not possible", self:_WeaponTypeName(target.weapontype))) + self:E(self.lid..string.format("WARNING: Selected weapon type %s is not possible", self:_WeaponTypeName(target.weapontype))) end if self.Debug then local zone=ZONE_RADIUS:New(target.name, target.coord:GetVec2(), target.radius) @@ -2019,17 +2031,17 @@ function ARTY:onafterStart(Controllable, From, Event, To) text=text..string.format("Missile types:\n") for _,_type in pairs(self.ammomissiles) do text=text..string.format("- %s\n", _type) - end + end text=text..string.format("******************************************************") if self.Debug then - self:E(ARTY.id..text) + self:I(self.lid..text) else - self:T(ARTY.id..text) + self:T(self.lid..text) end - + -- Set default ROE to weapon hold. self.Controllable:OptionROEHoldFire() - + -- Add event handler. self:HandleEvent(EVENTS.Shot) --, self._OnEventShot) self:HandleEvent(EVENTS.Dead) --, self._OnEventDead) @@ -2039,7 +2051,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) if self.markallow then world.addEventHandler(self) end - + -- Start checking status. self:__Status(self.StatusInterval) end @@ -2073,10 +2085,10 @@ function ARTY:_StatusReport(display) local Nnukes=self.Nukes local Nillu=self.Nillu local Nsmoke=self.Nsmoke - + local Tnow=timer.getTime() local Clock=self:_SecondsToClock(timer.getAbsTime()) - + local text=string.format("\n******************* STATUS ***************************\n") text=text..string.format("ARTY group = %s\n", self.groupname) text=text..string.format("Clock = %s\n", Clock) @@ -2087,7 +2099,7 @@ function ARTY:_StatusReport(display) text=text..string.format("Number of missiles = %d\n", Nmissiles) text=text..string.format("Number of nukes = %d\n", Nnukes) text=text..string.format("Number of illum. = %d\n", Nillu) - text=text..string.format("Number of smoke = %d\n", Nsmoke) + text=text..string.format("Number of smoke = %d\n", Nsmoke) if self.currentTarget then text=text..string.format("Current Target = %s\n", tostring(self.currentTarget.name)) text=text..string.format("Curr. Tgt assigned = %d\n", Tnow-self.currentTarget.Tassigned) @@ -2109,9 +2121,9 @@ function ARTY:_StatusReport(display) text=text..string.format("- %s\n", self:_MoveInfo(self.moves[i])) end text=text..string.format("******************************************************") - env.info(ARTY.id..text) + env.info(self.lid..text) MESSAGE:New(text, 20):Clear():ToCoalitionIf(self.coalition, display) - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2123,159 +2135,161 @@ end -- @param Core.Event#EVENTDATA EventData function ARTY:OnEventShot(EventData) self:F(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:T3(ARTY.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) - self:T3(ARTY.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) - self:T3(ARTY.id.."EVENT SHOT: Weapon type = ".._weapon) - self:T3(ARTY.id.."EVENT SHOT: Weapon name = ".._weaponName) - + self:T3(self.lid.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) + self:T3(self.lid.."EVENT SHOT: Ini group = "..EventData.IniGroupName) + self:T3(self.lid.."EVENT SHOT: Weapon type = ".._weapon) + self:T3(self.lid.."EVENT SHOT: Weapon name = ".._weaponName) + local group = EventData.IniGroup --Wrapper.Group#GROUP - + if group and group:IsAlive() then - + if EventData.IniGroupName == self.groupname then - + if self.currentTarget then - + -- Increase number of shots fired by this group on this target. self.Nshots=self.Nshots+1 - + -- Debug output. local text=string.format("%s, fired shot %d of %d with weapon %s on target %s.", self.alias, self.Nshots, self.currentTarget.nshells, _weaponName, self.currentTarget.name) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 5):Clear():ToAllIf(self.report or self.Debug) - + -- Last known position of the weapon fired. local _lastpos={x=0, y=0, z=0} - + --- Track the position of the weapon if it is supposed to model a tac nuke, illumination or smoke shell. -- @param #table _weapon local function _TrackWeapon(_data) - + -- When the pcall status returns false the weapon has hit. local _weaponalive,_currpos = pcall( function() return _data.weapon:getPoint() end) - + -- Debug - self:T3(ARTY.id..string.format("ARTY %s: Weapon still in air: %s", self.groupname, tostring(_weaponalive))) - + self:T3(self.lid..string.format("ARTY %s: Weapon still in air: %s", self.groupname, tostring(_weaponalive))) + -- Destroy weapon before impact. local _destroyweapon=false - + if _weaponalive then - + -- Update last position. _lastpos={x=_currpos.x, y=_currpos.y, z=_currpos.z} -- Coordinate and distance to target. - local _coord=COORDINATE:NewFromVec3(_lastpos) - local _dist=_coord:Get2DDistance(_data.target.coord) - + local _coord=COORDINATE:NewFromVec3(_lastpos) + local _dist=_coord:Get2DDistance(_data.target.coord) + -- Debug - self:T3(ARTY.id..string.format("ARTY %s weapon to target dist = %d m", self.groupname,_dist)) - + self:T3(self.lid..string.format("ARTY %s weapon to target dist = %d m", self.groupname,_dist)) + if _data.target.weapontype==ARTY.WeaponType.IlluminationShells then - + -- Check if within distace. if _dist<_data.target.radius then - + -- Get random coordinate within certain radius of the target. local _cr=_data.target.coord:GetRandomCoordinateInRadius(_data.target.radius) - + -- Get random altitude over target. local _alt=_cr:GetLandHeight()+math.random(self.illuMinalt, self.illuMaxalt) - + -- Adjust explosion height of coordinate. local _ci=COORDINATE:New(_cr.x,_alt,_cr.z) - + -- Create illumination flare. _ci:IlluminationBomb(self.illuPower) - - -- Destroy actual shell. - _destroyweapon=true - end - - elseif _data.target.weapontype==ARTY.WeaponType.SmokeShells then - - if _dist<_data.target.radius then - - -- Get random coordinate within a certain radius. - local _cr=_coord:GetRandomCoordinateInRadius(_data.target.radius) - - -- Fire smoke at this coordinate. - _cr:Smoke(self.smokeColor) - + -- Destroy actual shell. _destroyweapon=true - - end - + end + + elseif _data.target.weapontype==ARTY.WeaponType.SmokeShells then + + if _dist<_data.target.radius then + + -- Get random coordinate within a certain radius. + local _cr=_coord:GetRandomCoordinateInRadius(_data.target.radius) + + -- Fire smoke at this coordinate. + _cr:Smoke(self.smokeColor) + + -- Destroy actual shell. + _destroyweapon=true + + end + end - + if _destroyweapon then - - self:T2(ARTY.id..string.format("ARTY %s destroying shell, stopping timer.", self.groupname)) - + + self:T2(self.lid..string.format("ARTY %s destroying shell, stopping timer.", self.groupname)) + -- Destroy weapon and stop timer. _data.weapon:destroy() return nil - + else -- TODO: Make dt input parameter. local dt=0.02 - - self:T3(ARTY.id..string.format("ARTY %s tracking weapon again in %.3f seconds", self.groupname, dt)) - + + self:T3(self.lid..string.format("ARTY %s tracking weapon again in %.3f seconds", self.groupname, dt)) + -- Check again in 0.05 seconds. return timer.getTime() + dt - + end - + else - + -- Get impact coordinate. local _impactcoord=COORDINATE:NewFromVec3(_lastpos) - + + self:I(self.lid..string.format("ARTY %s weapon NOT ALIVE any more.", self.groupname)) + -- Create a "nuclear" explosion and blast at the impact point. - if _weapon.weapontype==ARTY.WeaponType.TacticalNukes then - self:T2(ARTY.id..string.format("ARTY %s triggering nuclear explosion in one second.", self.groupname)) + if _data.target.weapontype==ARTY.WeaponType.TacticalNukes then + self:T(self.lid..string.format("ARTY %s triggering nuclear explosion in one second.", self.groupname)) SCHEDULER:New(nil, ARTY._NuclearBlast, {self,_impactcoord}, 1.0) end - + -- Stop timer. return nil - + end - + end - + -- Start track the shell if we want to model a tactical nuke. local _tracknuke = self.currentTarget.weapontype==ARTY.WeaponType.TacticalNukes and self.Nukes>0 local _trackillu = self.currentTarget.weapontype==ARTY.WeaponType.IlluminationShells and self.Nillu>0 local _tracksmoke = self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells and self.Nsmoke>0 if _tracknuke or _trackillu or _tracksmoke then - - self:T(ARTY.id..string.format("ARTY %s: Tracking of weapon starts in two seconds.", self.groupname)) - + + self:T(self.lid..string.format("ARTY %s: Tracking of weapon starts in two seconds.", self.groupname)) + local _peter={} _peter.weapon=EventData.weapon _peter.target=UTILS.DeepCopy(self.currentTarget) - + timer.scheduleFunction(_TrackWeapon, _peter, timer.getTime() + 2.0) end - + -- Get current ammo. local _nammo,_nshells,_nrockets,_nmissiles=self:GetAmmo() - + -- Decrease available nukes because we just fired one. if self.currentTarget.weapontype==ARTY.WeaponType.TacticalNukes then self.Nukes=self.Nukes-1 @@ -2290,60 +2304,60 @@ function ARTY:OnEventShot(EventData) if self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells then self.Nsmoke=self.Nsmoke-1 end - + -- Check if we are completely out of ammo. local _outofammo=false if _nammo==0 then - self:T(ARTY.id..string.format("Group %s completely out of ammo.", self.groupname)) + self:T(self.lid..string.format("Group %s completely out of ammo.", self.groupname)) _outofammo=true end - + -- Check if we are out of ammo of the weapon type used for this target. -- Note that should not happen because we only open fire with the available number of shots. local _partlyoutofammo=self:_CheckOutOfAmmo({self.currentTarget}) - + -- Weapon type name for current target. local _weapontype=self:_WeaponTypeName(self.currentTarget.weapontype) - self:T(ARTY.id..string.format("Group %s ammo: total=%d, shells=%d, rockets=%d, missiles=%d", self.groupname, _nammo, _nshells, _nrockets, _nmissiles)) - self:T(ARTY.id..string.format("Group %s uses weapontype %s for current target.", self.groupname, _weapontype)) - + self:T(self.lid..string.format("Group %s ammo: total=%d, shells=%d, rockets=%d, missiles=%d", self.groupname, _nammo, _nshells, _nrockets, _nmissiles)) + self:T(self.lid..string.format("Group %s uses weapontype %s for current target.", self.groupname, _weapontype)) + -- Default switches for cease fire and relocation. local _ceasefire=false local _relocate=false - + -- Check if number of shots reached max. if self.Nshots >= self.currentTarget.nshells then - + -- Debug message local text=string.format("Group %s stop firing on target %s.", self.groupname, self.currentTarget.name) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - + -- Cease fire. _ceasefire=true - + -- Relocate if enabled. _relocate=self.relocateafterfire end - + -- Check if we are (partly) out of ammo. if _outofammo or _partlyoutofammo then _ceasefire=true - end - + end + -- Relocate position. if _relocate then self:_Relocate() - end - + end + -- Cease fire on current target. if _ceasefire then self:CeaseFire(self.currentTarget) end - + else - self:E(ARTY.id..string.format("WARNING: No current target for group %s?!", self.groupname)) - end + self:E(self.lid..string.format("WARNING: No current target for group %s?!", self.groupname)) + end end end end @@ -2362,7 +2376,7 @@ function ARTY:onEvent(Event) -- Set battery and coalition. --local batteryname=self.groupname --local batterycoalition=self.Controllable:GetCoalition() - + self:T2(string.format("Event captured = %s", tostring(self.groupname))) self:T2(string.format("Event id = %s", tostring(Event.id))) self:T2(string.format("Event time = %s", tostring(Event.time))) @@ -2374,23 +2388,23 @@ function ARTY:onEvent(Event) local _unitname=Event.initiator:getName() self:T2(string.format("Event ini unit name = %s", tostring(_unitname))) end - + if Event.id==world.event.S_EVENT_MARK_ADDED then self:T2({event="S_EVENT_MARK_ADDED", battery=self.groupname, vec3=Event.pos}) - + elseif Event.id==world.event.S_EVENT_MARK_CHANGE then self:T({event="S_EVENT_MARK_CHANGE", battery=self.groupname, vec3=Event.pos}) - + -- Handle event. self:_OnEventMarkChange(Event) - + elseif Event.id==world.event.S_EVENT_MARK_REMOVED then self:T2({event="S_EVENT_MARK_REMOVED", battery=self.groupname, vec3=Event.pos}) - + -- Hande event. self:_OnEventMarkRemove(Event) end - + end --- Function called when a F10 map mark was removed. @@ -2401,15 +2415,15 @@ function ARTY:_OnEventMarkRemove(Event) -- Get battery coalition and name. local batterycoalition=self.coalition --local batteryname=self.groupname - + if Event.text~=nil and Event.text:find("BATTERY") then - + -- Init defaults. local _cancelmove=false local _canceltarget=false local _name="" local _id=nil - + -- Check for key phrases of relocation or engagements in marker text. If not, return. if Event.text:find("Marked Relocation") then _cancelmove=true @@ -2422,22 +2436,22 @@ function ARTY:_OnEventMarkRemove(Event) else return end - + -- Check if there is a task which matches. if _id==nil then return end - + -- Check if the coalition is the same or an authorization key has been defined. if (batterycoalition==Event.coalition and self.markkey==nil) or self.markkey~=nil then - + -- Authentify key local _validkey=self:_MarkerKeyAuthentification(Event.text) - + -- Check if we have the right coalition. if _validkey then - - -- This should be the unique name of the target or move. + + -- This should be the unique name of the target or move. if _cancelmove then if self.currentMove and self.currentMove.name==_name then -- We do clear tasks here because in Arrived() it can cause a CTD if the group did actually arrive! @@ -2452,17 +2466,17 @@ function ARTY:_OnEventMarkRemove(Event) if self.currentTarget and self.currentTarget.name==_name then -- Cease fire. self:CeaseFire(self.currentTarget) - -- We still need to remove the target, because there might be more planned engagements (maxengage>1). + -- We still need to remove the target, because there might be more planned engagements (maxengage>1). self:RemoveTarget(_name) else -- Remove target from queue self:RemoveTarget(_name) end end - - end + + end end - end + end end --- Function called when a F10 map mark was changed. This happens when a user enters text. @@ -2472,60 +2486,60 @@ function ARTY:_OnEventMarkChange(Event) -- Check if marker has a text and the "arty" keyword. if Event.text~=nil and Event.text:lower():find("arty") then - + -- Convert (wrong x-->z, z-->x) vec3 -- DONE: This needs to be "fixed", once DCS gives the correct numbers for x and z. -- Was fixed in DCS 2.5.5.34644! local vec3={y=Event.pos.y, x=Event.pos.x, z=Event.pos.z} --local vec3={y=Event.pos.y, x=Event.pos.z, z=Event.pos.x} - + -- Get coordinate from vec3. local _coord=COORDINATE:NewFromVec3(vec3) - + -- Adjust y component to actual land height. When a coordinate is create it uses y=5 m! _coord.y=_coord:GetLandHeight() - + -- Get battery coalition and name. local batterycoalition=self.coalition local batteryname=self.groupname - + -- Check if the coalition is the same or an authorization key has been defined. if (batterycoalition==Event.coalition and self.markkey==nil) or self.markkey~=nil then - + -- Evaluate marker text and extract parameters. local _assign=self:_Markertext(Event.text) -- Check if ENGAGE or MOVE or REQUEST keywords were found. if _assign==nil or not (_assign.engage or _assign.move or _assign.request or _assign.cancel or _assign.set) then - self:T(ARTY.id..string.format("WARNING: %s, no keyword ENGAGE, MOVE, REQUEST, CANCEL or SET in mark text! Command will not be executed. Text:\n%s", self.groupname, Event.text)) + self:T(self.lid..string.format("WARNING: %s, no keyword ENGAGE, MOVE, REQUEST, CANCEL or SET in mark text! Command will not be executed. Text:\n%s", self.groupname, Event.text)) return end - - -- Check if job is assigned to this ARTY group. Default is for all ARTY groups. + + -- Check if job is assigned to this ARTY group. Default is for all ARTY groups. local _assigned=false - + -- If any array is filled something has been assigned. if _assign.everyone then - + -- Everyone was addressed. _assigned=true - + else --#_assign.battery>0 or #_assign.aliases>0 or #_assign.cluster>0 then - -- Loop over batteries. + -- Loop over batteries. for _,bat in pairs(_assign.battery) do if self.groupname==bat then _assigned=true end end - - -- Loop over aliases. + + -- Loop over aliases. for _,alias in pairs(_assign.aliases) do if self.alias==alias then _assigned=true end end - + -- Loop over clusters. for _,bat in pairs(_assign.cluster) do for _,cluster in pairs(self.clusters) do @@ -2536,10 +2550,10 @@ function ARTY:_OnEventMarkChange(Event) end end - + -- We were not addressed. if not _assigned then - self:T3(ARTY.id..string.format("INFO: ARTY group %s was not addressed! Mark text:\n%s", self.groupname, Event.text)) + self:T3(self.lid..string.format("INFO: ARTY group %s was not addressed! Mark text:\n%s", self.groupname, Event.text)) return end @@ -2547,10 +2561,10 @@ function ARTY:_OnEventMarkChange(Event) if _assign.coord then _coord=_assign.coord end - + -- Check if the authorization key is required and if it is valid. local _validkey=self:_MarkerKeyAuthentification(Event.text) - + -- Handle requests and return. if _assign.request and _validkey then if _assign.requestammo then @@ -2564,14 +2578,14 @@ function ARTY:_OnEventMarkChange(Event) end if _assign.requeststatus then self:_MarkRequestStatus() - end + end if _assign.requestrearming then self:Rearm() - end + end -- Requests Done ==> End of story! return end - + -- Cancel stuff and return. if _assign.cancel and _validkey then if _assign.cancelmove and self.currentMove then @@ -2605,7 +2619,7 @@ function ARTY:_OnEventMarkChange(Event) if _assign.setrearminggroup then _coord:RemoveMark(Event.idx) local rearminggroupcoord=_assign.setrearminggroup:GetCoordinate() - rearminggroupcoord:MarkToCoalition(string.format("Rearming group for battery %s", self.groupname), self.coalition, false, string.format("New rearming group for battery %s defined.", self.groupname)) + rearminggroupcoord:MarkToCoalition(string.format("Rearming group for battery %s", self.groupname), self.coalition, false, string.format("New rearming group for battery %s defined.", self.groupname)) self:SetRearmingGroup(_assign.setrearminggroup) if self.Debug then rearminggroupcoord:SmokeOrange() @@ -2614,51 +2628,51 @@ function ARTY:_OnEventMarkChange(Event) -- Set stuff Done ==> End of story! return end - + -- Handle engagements and relocations. if _validkey then - + -- Remove old mark because it might contain confidential data such as the key. -- Also I don't know who can see the mark which was created. _coord:RemoveMark(Event.idx) - + -- Anticipate marker ID. -- WARNING: Make sure, no marks are set until the COORDINATE:MarkToCoalition() is called or the target/move name will be wrong and target cannot be removed by deleting its marker. local _id=UTILS._MarkID+1 - + if _assign.move then - + -- Create a new name. This determins the string we search when deleting a move! local _name=self:_MarkMoveName(_id) - + local text=string.format("%s, received new relocation assignment.", self.alias) text=text..string.format("\nCoordinates %s",_coord:ToStringLLDMS()) MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) - + -- Assign a relocation of the arty group. local _movename=self:AssignMoveCoord(_coord, _assign.time, _assign.speed, _assign.onroad, _assign.movecanceltarget,_name, true) - + if _movename~=nil then local _mid=self:_GetMoveIndexByName(_movename) local _move=self.moves[_mid] - + -- Create new target name. local clock=tostring(self:_SecondsToClock(_move.time)) local _markertext=_movename..string.format(", Time=%s, Speed=%d km/h, Use Roads=%s.", clock, _move.speed, tostring(_move.onroad)) - + -- Create a new mark. This will trigger the mark added event. local _randomcoord=_coord:GetRandomCoordinateInRadius(100) _randomcoord:MarkToCoalition(_markertext, batterycoalition, self.markreadonly or _assign.readonly) else local text=string.format("%s, relocation not possible.", self.alias) MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) - end - + end + else - + -- Create a new name. local _name=self:_MarkTargetName(_id) - + local text=string.format("%s, received new target assignment.", self.alias) text=text..string.format("\nCoordinates %s",_coord:ToStringLLDMS()) if _assign.time then @@ -2678,30 +2692,30 @@ function ARTY:_OnEventMarkChange(Event) end if _assign.weapontype then text=text..string.format("\nWeapon %s",self:_WeaponTypeName(_assign.weapontype)) - end + end MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) - + -- Assign a new firing engagement. -- Note, we set unique=true so this target gets only added once. local _targetname=self:AssignTargetCoord(_coord,_assign.prio,_assign.radius,_assign.nshells,_assign.maxengage,_assign.time,_assign.weapontype, _name, true) - + if _targetname~=nil then local _tid=self:_GetTargetIndexByName(_targetname) local _target=self.targets[_tid] - + -- Create new target name. local clock=tostring(self:_SecondsToClock(_target.time)) local weapon=self:_WeaponTypeName(_target.weapontype) local _markertext=_targetname..string.format(", Priority=%d, Radius=%d m, Shots=%d, Engagements=%d, Weapon=%s, Time=%s", _target.prio, _target.radius, _target.nshells, _target.maxengage, weapon, clock) - + -- Create a new mark. This will trigger the mark added event. local _randomcoord=_coord:GetRandomCoordinateInRadius(250) _randomcoord:MarkToCoalition(_markertext, batterycoalition, self.markreadonly or _assign.readonly) - end + end end end - - end + + end end end @@ -2717,13 +2731,13 @@ function ARTY:OnEventDead(EventData) -- Check for correct group. if EventData and EventData.IniGroupName and EventData.IniGroupName==_name then - + -- Name of the dead unit. local unitname=tostring(EventData.IniUnitName) - + -- Dead Unit. - self:T(ARTY.id..string.format("%s: Captured dead event for unit %s.", _name, unitname)) - + self:T(self.lid..string.format("%s: Captured dead event for unit %s.", _name, unitname)) + -- FSM Dead event. We give one second for update of data base. --self:__Dead(1, unitname) self:Dead(unitname) @@ -2744,76 +2758,79 @@ end function ARTY:onafterStatus(Controllable, From, Event, To) self:_EventFromTo("onafterStatus", Event, From, To) + -- Get ammo. + local ntot, nshells, nrockets, nmissiles=self:GetAmmo() + -- FSM state. - local fsmstate=self:GetState() - self:I(ARTY.id..string.format("Status of group %s: %s", self.alias, fsmstate)) - + local fsmstate=self:GetState() + self:I(self.lid..string.format("Status %s, Ammo total=%d: shells=%d [smoke=%d, illu=%d, nukes=%d*%.3f kT], rockets=%d, missiles=%d", fsmstate, ntot, nshells, self.Nsmoke, self.Nillu, self.Nukes, self.nukewarhead/1000000, nrockets, nmissiles)) + if self.Controllable and self.Controllable:IsAlive() then - + -- We have a cargo group ==> check if group was loaded into a carrier. if self.cargogroup then if self.cargogroup:IsLoaded() and not self:is("InTransit") then -- Group is now InTransit state. Current target is canceled. - self:T(ARTY.id..string.format("Group %s has been loaded into a carrier and is now transported.", self.alias)) + self:T(self.lid..string.format("Group %s has been loaded into a carrier and is now transported.", self.alias)) self:Loaded() elseif self.cargogroup:IsUnLoaded() then -- Group has been unloaded and is combat ready again. - self:T(ARTY.id..string.format("Group %s has been unloaded from the carrier.", self.alias)) + self:T(self.lid..string.format("Group %s has been unloaded from the carrier.", self.alias)) self:UnLoaded() end end - + -- Debug current status info. if self.Debug then self:_StatusReport() end - - -- Group is being transported as cargo ==> skip everything and check again in 5 seconds. + + -- Group is being transported as cargo ==> skip everything and check again in 5 seconds. if self:is("InTransit") then self:__Status(-5) return end - + -- Group on the move. if self:is("Moving") then - self:T2(ARTY.id..string.format("%s: Moving", Controllable:GetName())) + self:T2(self.lid..string.format("%s: Moving", Controllable:GetName())) end - + -- Group is rearming. if self:is("Rearming") then local _rearmed=self:_CheckRearmed() if _rearmed then - self:T2(ARTY.id..string.format("%s: Rearming ==> Rearmed", Controllable:GetName())) + self:T2(self.lid..string.format("%s: Rearming ==> Rearmed", Controllable:GetName())) self:Rearmed() end end - + -- Group finished rearming. if self:is("Rearmed") then local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) - self:T2(ARTY.id..string.format("%s: Rearmed. Distance ARTY to InitalCoord = %d m", Controllable:GetName(), distance)) + self:T2(self.lid..string.format("%s: Rearmed. Distance ARTY to InitalCoord = %d m", Controllable:GetName(), distance)) -- Check that ARTY group is back and set it to combat ready. if distance <= self.RearmingDistance then - self:T2(ARTY.id..string.format("%s: Rearmed ==> CombatReady", Controllable:GetName())) + self:T2(self.lid..string.format("%s: Rearmed ==> CombatReady", Controllable:GetName())) self:CombatReady() end end - + -- Group arrived at destination. if self:is("Arrived") then - self:T2(ARTY.id..string.format("%s: Arrived ==> CombatReady", Controllable:GetName())) + self:T2(self.lid..string.format("%s: Arrived ==> CombatReady", Controllable:GetName())) self:CombatReady() end - + -- Group is firing on target. if self:is("Firing") then -- Check that firing started after ~5 min. If not, target is removed. self:_CheckShootingStarted() end - + -- Check if targets are in range and update target.inrange value. self:_CheckTargetsInRange() - + -- Check if selected weapon type for target is possible at all. E.g. request rockets for Paladin. local notpossible={} for i=1,#self.targets do @@ -2824,44 +2841,44 @@ function ARTY:onafterStatus(Controllable, From, Event, To) end end for _,targetname in pairs(notpossible) do - self:E(ARTY.id..string.format("%s: Removing target %s because requested weapon is not possible with this type of unit.", self.groupname, targetname)) + self:E(self.lid..string.format("%s: Removing target %s because requested weapon is not possible with this type of unit.", self.groupname, targetname)) self:RemoveTarget(targetname) end - + -- Get a valid timed target if it is due to be attacked. local _timedTarget=self:_CheckTimedTargets() - + -- Get a valid normal target (one that is not timed). local _normalTarget=self:_CheckNormalTargets() - + -- Get a commaned move to another location. local _move=self:_CheckMoves() - + if _move then - + -- Command to move. self:Move(_move) - + elseif _timedTarget then - + -- Cease fire on current target first. if self.currentTarget then self:CeaseFire(self.currentTarget) end - + -- Open fire on timed target. self:OpenFire(_timedTarget) - + elseif _normalTarget then - + -- Open fire on normal target. self:OpenFire(_normalTarget) - + end - + -- Get ammo. local nammo, nshells, nrockets, nmissiles=self:GetAmmo() - + -- Check if we have a target in the queue for which weapons are still available. local gotsome=false if #self.targets>0 then @@ -2875,23 +2892,23 @@ function ARTY:onafterStatus(Controllable, From, Event, To) -- No targets in the queue. gotsome=true end - + -- No ammo available. Either completely blank or only queued targets for ammo which is out. if (nammo==0 or not gotsome) and not (self:is("Moving") or self:is("Rearming") or self:is("OutOfAmmo")) then self:Winchester() end - + -- Group is out of ammo. if self:is("OutOfAmmo") then - self:T2(ARTY.id..string.format("%s: OutOfAmmo ==> Rearm ==> Rearming", Controllable:GetName())) + self:T2(self.lid..string.format("%s: OutOfAmmo ==> Rearm ==> Rearming", Controllable:GetName())) self:Rearm() - end - + end + -- Call status again in ~10 sec. self:__Status(self.StatusInterval) - + else - self:E(ARTY.id..string.format("Arty group %s is not alive!", self.groupname)) + self:E(self.lid..string.format("Arty group %s is not alive!", self.groupname)) end end @@ -2908,7 +2925,7 @@ function ARTY:onbeforeLoaded(Controllable, From, Event, To) if self.currentTarget then self:CeaseFire(self.currentTarget) end - + return true end @@ -2934,7 +2951,7 @@ end function ARTY:onenterCombatReady(Controllable, From, Event, To) self:_EventFromTo("onenterCombatReady", Event, From, To) -- Debug info - self:T3(ARTY.id..string.format("onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) + self:T3(self.lid..string.format("onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2949,35 +2966,35 @@ end -- @return #boolean If true, proceed to onafterOpenfire. function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onbeforeOpenFire", Event, From, To) - + -- Check that group has no current target already. if self.currentTarget then -- This should not happen. Some earlier check failed. - self:E(ARTY.id..string.format("ERROR: Group %s already has a target %s!", self.groupname, self.currentTarget.name)) + self:E(self.lid..string.format("ERROR: Group %s already has a target %s!", self.groupname, self.currentTarget.name)) -- Deny transition. return false end - + -- Check if target is in range. if not self:_TargetInRange(target) then -- This should not happen. Some earlier check failed. - self:E(ARTY.id..string.format("ERROR: Group %s, target %s is out of range!", self.groupname, self.currentTarget.name)) + self:E(self.lid..string.format("ERROR: Group %s, target %s is out of range!", self.groupname, self.currentTarget.name)) -- Deny transition. return false end -- Get the number of available shells, rockets or missiles requested for this target. local nfire=self:_CheckWeaponTypeAvailable(target) - + -- Adjust if less than requested ammo is left. target.nshells=math.min(target.nshells, nfire) - + -- No ammo left ==> deny transition. if target.nshells<1 then local text=string.format("%s, no ammo left to engage target %s with selected weapon type %s.") return false end - + return true end @@ -2990,10 +3007,10 @@ end -- @param #ARTY.Target target Array holding the target info. function ARTY:onafterOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onafterOpenFire", Event, From, To) - + -- Get target array index. local id=self:_GetTargetIndexByName(target.name) - + -- Target is now under fire and has been engaged once more. if id then -- Set under fire flag. @@ -3003,10 +3020,10 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) -- Set time the target was assigned. self.currentTarget.Tassigned=timer.getTime() end - + -- Distance to target local range=Controllable:GetCoordinate():Get2DDistance(target.coord) - + -- Get ammo. local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() local nfire=Nammo @@ -3033,28 +3050,28 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) nfire=Nmissiles _type="cruise missiles" end - + -- Adjust if less than requested ammo is left. target.nshells=math.min(target.nshells, nfire) - + -- Send message. local text=string.format("%s, opening fire on target %s with %d %s. Distance %.1f km.", Controllable:GetName(), target.name, target.nshells, _type, range/1000) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report) - + --if self.Debug then -- local _coord=target.coord --Core.Point#COORDINATE -- local text=string.format("ARTY %s, Target %s, n=%d, weapon=%s", self.Controllable:GetName(), target.name, target.nshells, self:_WeaponTypeName(target.weapontype)) -- _coord:MarkToAll(text) --end - + -- Start firing. if target.attackgroup then self:_AttackGroup(target) else self:_FireAtCoord(target.coord, target.radius, target.nshells, target.weapontype) end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3068,17 +3085,17 @@ end -- @param #table target Array holding the target info. function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) self:_EventFromTo("onafterCeaseFire", Event, From, To) - + if target then - + -- Send message. local text=string.format("%s, ceasing fire on target %s.", Controllable:GetName(), target.name) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report) - + -- Get target array index. local id=self:_GetTargetIndexByName(target.name) - + -- We have a target. if id then -- Target was actually engaged. (Could happen that engagement was aborted while group was still aiming.) @@ -3090,28 +3107,28 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) -- Target is not under fire any more. self.targets[id].underfire=false end - + -- If number of engagements has been reached, the target is removed. if target.engaged >= target.maxengage then self:RemoveTarget(target.name) end - + -- Set ROE to weapon hold. self.Controllable:OptionROEHoldFire() - + -- Clear tasks. self.Controllable:ClearTasks() - + else - self:E(ARTY.id..string.format("ERROR: No target in cease fire for group %s.", self.groupname)) + self:E(self.lid..string.format("ERROR: No target in cease fire for group %s.", self.groupname)) end - + -- Set number of shots to zero. self.Nshots=0 - + -- ARTY group has no current target any more. self.currentTarget=nil - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3124,12 +3141,12 @@ end -- @param #string To To state. function ARTY:onafterWinchester(Controllable, From, Event, To) self:_EventFromTo("onafterWinchester", Event, From, To) - + -- Send message. local text=string.format("%s, winchester!", Controllable:GetName()) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3143,24 +3160,24 @@ end -- @return #boolean If true, proceed to onafterRearm. function ARTY:onbeforeRearm(Controllable, From, Event, To) self:_EventFromTo("onbeforeRearm", Event, From, To) - + local _rearmed=self:_CheckRearmed() if _rearmed then - self:T(ARTY.id..string.format("%s, group is already armed to the teeth. Rearming request denied!", self.groupname)) + self:T(self.lid..string.format("%s, group is already armed to the teeth. Rearming request denied!", self.groupname)) return false else - self:T(ARTY.id..string.format("%s, group might be rearmed.", self.groupname)) + self:T(self.lid..string.format("%s, group might be rearmed.", self.groupname)) end - + -- Check if a reaming unit or rearming place was specified. if self.RearmingGroup and self.RearmingGroup:IsAlive() then return true elseif self.RearmingPlaceCoord then - return true + return true else return false end - + end --- After "Rearm" event. Send message if reporting is on. Route rearming unit to ARTY group. @@ -3171,13 +3188,13 @@ end -- @param #string To To state. function ARTY:onafterRearm(Controllable, From, Event, To) self:_EventFromTo("onafterRearm", Event, From, To) - + -- Coordinate of ARTY unit. local coordARTY=self.Controllable:GetCoordinate() - + -- Remember current coordinates so that we find our way back home. self.InitialCoord=coordARTY - + -- Coordinate of rearming group. local coordRARM=nil if self.RearmingGroup then @@ -3186,71 +3203,71 @@ function ARTY:onafterRearm(Controllable, From, Event, To) -- Remember the coordinates of the rearming unit. After rearming it will go back to this position. self.RearmingGroupCoord=coordRARM end - + if self.RearmingGroup and self.RearmingPlaceCoord and self.ismobile then - + -- CASE 1: Rearming unit and ARTY group meet at rearming place. - + -- Send message. local text=string.format("%s, %s, request rearming at rearming place.", Controllable:GetName(), self.RearmingGroup:GetName()) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) - + -- Distances. local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord) - + -- Route ARTY group to rearming place. if dA > self.RearmingDistance then local _tocoord=self:_VicinityCoord(self.RearmingPlaceCoord, self.RearmingDistance/4, self.RearmingDistance/2) self:AssignMoveCoord(_tocoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE TO REARMING PLACE", true) end - + -- Route Rearming group to rearming place. if dR > self.RearmingDistance then local ToCoord=self:_VicinityCoord(self.RearmingPlaceCoord, self.RearmingDistance/4, self.RearmingDistance/2) self:_Move(self.RearmingGroup, ToCoord, self.RearmingGroupSpeed, self.RearmingGroupOnRoad) end - + elseif self.RearmingGroup then - + -- CASE 2: Rearming unit drives to ARTY group. - + -- Send message. local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingGroup:GetName()) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) - + -- Distance between ARTY group and rearming unit. local distance=coordARTY:Get2DDistance(coordRARM) - + -- If distance is larger than ~100 m, the Rearming unit is routed to the ARTY group. if distance > self.RearmingDistance then - + -- Route rearming group to ARTY group. self:_Move(self.RearmingGroup, self:_VicinityCoord(coordARTY), self.RearmingGroupSpeed, self.RearmingGroupOnRoad) end - + elseif self.RearmingPlaceCoord then - + -- CASE 3: ARTY drives to rearming place. - + -- Send message. local text=string.format("%s, moving to rearming place.", Controllable:GetName()) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) - + -- Distance. local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) - + -- Route ARTY group to rearming place. if dA > self.RearmingDistance then local _tocoord=self:_VicinityCoord(self.RearmingPlaceCoord) self:AssignMoveCoord(_tocoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE TO REARMING PLACE", true) - end - + end + end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3263,23 +3280,23 @@ end -- @param #string To To state. function ARTY:onafterRearmed(Controllable, From, Event, To) self:_EventFromTo("onafterRearmed", Event, From, To) - + -- Send message. local text=string.format("%s, rearming complete.", Controllable:GetName()) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) - + -- "Rearm" tactical nukes as well. self.Nukes=self.Nukes0 self.Nillu=self.Nillu0 self.Nsmoke=self.Nsmoke0 - + -- Route ARTY group back to where it came from (if distance is > 100 m). local dist=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) if dist > self.RearmingDistance then self:AssignMoveCoord(self.InitialCoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE REARMING COMPLETE", true) end - + -- Route unit back to where it came from (if distance is > 100 m). if self.RearmingGroup and self.RearmingGroup:IsAlive() then local d=self.RearmingGroup:GetCoordinate():Get2DDistance(self.RearmingGroupCoord) @@ -3290,7 +3307,7 @@ function ARTY:onafterRearmed(Controllable, From, Event, To) self.RearmingGroup:ClearTasks() end end - + end --- Check if ARTY group is rearmed, i.e. has its full amount of ammo. @@ -3301,27 +3318,27 @@ function ARTY:_CheckRearmed() -- Get current ammo. local nammo,nshells,nrockets,nmissiles=self:GetAmmo() - + -- Number of units still alive. local units=self.Controllable:GetUnits() local nunits=0 if units then nunits=#units end - + -- Full Ammo count. local FullAmmo=self.Nammo0 * nunits / self.IniGroupStrength - + -- Rearming status in per cent. local _rearmpc=nammo/FullAmmo*100 - + -- Send message if rearming > 1% complete if _rearmpc>1 then local text=string.format("%s, rearming %d %% complete.", self.alias, _rearmpc) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) end - + -- Return if ammo is full. -- TODO: Strangely, I got the case that a Paladin got one more shell than it can max carry, i.e. 40 not 39 when rearming when it still had some ammo left. Need to report. if nammo>=FullAmmo then @@ -3342,16 +3359,16 @@ end -- @param #string To To state. -- @param #table move Table containing the move parameters. -- @param Core.Point#COORDINATE ToCoord Coordinate to which the ARTY group should move. --- @param #boolean OnRoad If true group should move on road mainly. +-- @param #boolean OnRoad If true group should move on road mainly. -- @return #boolean If true, proceed to onafterMove. function ARTY:onbeforeMove(Controllable, From, Event, To, move) self:_EventFromTo("onbeforeMove", Event, From, To) - + -- Check if group can actually move... if not self.ismobile then return false end - + -- Check if group is engaging. if self.currentTarget then if move.cancel then @@ -3362,7 +3379,7 @@ function ARTY:onbeforeMove(Controllable, From, Event, To, move) return false end end - + return true end @@ -3379,21 +3396,21 @@ function ARTY:onafterMove(Controllable, From, Event, To, move) -- Set alarm state to green and ROE to weapon hold. self.Controllable:OptionAlarmStateGreen() self.Controllable:OptionROEHoldFire() - + -- Take care of max speed. local _Speed=math.min(move.speed, self.SpeedMax) - + -- Smoke coordinate if self.Debug then move.coord:SmokeRed() end - + -- Set current move. self.currentMove=move -- Route group to coodinate. self:_Move(self.Controllable, move.coord, move.speed, move.onroad) - + end --- After "Arrived" event. Group has reached its destination. @@ -3407,15 +3424,15 @@ function ARTY:onafterArrived(Controllable, From, Event, To) -- Set alarm state to auto. self.Controllable:OptionAlarmStateAuto() - + -- WARNING: calling ClearTasks() here causes CTD of DCS when move is over. Dont know why? combotask? --self.Controllable:ClearTasks() - + -- Send message local text=string.format("%s, arrived at destination.", Controllable:GetName()) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) - + -- Remove executed move from queue. if self.currentMove then self:RemoveMove(self.currentMove.name) @@ -3435,11 +3452,11 @@ end -- @param #table target Array holding the target parameters. function ARTY:onafterNewTarget(Controllable, From, Event, To, target) self:_EventFromTo("onafterNewTarget", Event, From, To) - + -- Debug message. local text=string.format("Adding new target %s.", target.name) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:T(ARTY.id..text) + self:T(self.lid..text) end --- After "NewMove" event. @@ -3451,11 +3468,11 @@ end -- @param #table move Array holding the move parameters. function ARTY:onafterNewMove(Controllable, From, Event, To, move) self:_EventFromTo("onafterNewTarget", Event, From, To) - + -- Debug message. local text=string.format("Adding new move %s.", move.name) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:T(ARTY.id..text) + self:T(self.lid..text) end @@ -3468,24 +3485,24 @@ end -- @param #string Unitname Name of the unit that died. function ARTY:onafterDead(Controllable, From, Event, To, Unitname) self:_EventFromTo("onafterDead", Event, From, To) - + -- Number of units still alive. --local nunits=self.Controllable and self.Controllable:CountAliveUnits() or 0 local nunits=self.Controllable:CountAliveUnits() - + -- Message. local text=string.format("%s, our unit %s just died! %d units left.", self.groupname, Unitname, nunits) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:I(ARTY.id..text) - + self:I(self.lid..text) + -- Go to stop state. if nunits==0 then - + -- Cease Fire on current target. if self.currentTarget then self:CeaseFire(self.currentTarget) end - + if self.respawnafterdeath then -- Respawn group. if not self.respawning then @@ -3497,7 +3514,7 @@ function ARTY:onafterDead(Controllable, From, Event, To, Unitname) self:Stop() end end - + end @@ -3509,16 +3526,16 @@ end -- @param #string To To state. function ARTY:onafterRespawn(Controllable, From, Event, To) self:_EventFromTo("onafterRespawn", Event, From, To) - + env.info("FF Respawning arty group") - + local group=self.Controllable --Wrapper.Group#GROUP - + -- Respawn group. self.Controllable=group:Respawn() - + self.respawning=false - + -- Call status again. self:__Status(-1) end @@ -3531,18 +3548,18 @@ end -- @param #string To To state. function ARTY:onafterStop(Controllable, From, Event, To) self:_EventFromTo("onafterStop", Event, From, To) - + -- Debug info. - self:I(ARTY.id..string.format("Stopping ARTY FSM for group %s.", tostring(Controllable:GetName()))) - + self:I(self.lid..string.format("Stopping ARTY FSM for group %s.", tostring(Controllable:GetName()))) + -- Cease Fire on current target. if self.currentTarget then self:CeaseFire(self.currentTarget) end - + -- Remove all targets. --self:RemoveAllTargets() - + -- Unhandle event. self:UnHandleEvent(EVENTS.Shot) self:UnHandleEvent(EVENTS.Dead) @@ -3563,7 +3580,7 @@ function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) -- Controllable. local group=self.Controllable --Wrapper.Group#GROUP - + -- Tactical nukes are actually cannon shells. if weapontype==ARTY.WeaponType.TacticalNukes or weapontype==ARTY.WeaponType.IlluminationShells or weapontype==ARTY.WeaponType.SmokeShells then weapontype=ARTY.WeaponType.Cannon @@ -3571,13 +3588,13 @@ function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) -- Set ROE to weapon free. group:OptionROEOpenFire() - + -- Get Vec2 local vec2=coord:GetVec2() - + -- Get task. local fire=group:TaskFireAtPoint(vec2, radius, nshells, weapontype) - + -- Execute task. group:SetTask(fire) end @@ -3589,9 +3606,9 @@ function ARTY:_AttackGroup(target) -- Controllable. local group=self.Controllable --Wrapper.Group#GROUP - + local weapontype=target.weapontype - + -- Tactical nukes are actually cannon shells. if weapontype==ARTY.WeaponType.TacticalNukes or weapontype==ARTY.WeaponType.IlluminationShells or weapontype==ARTY.WeaponType.SmokeShells then weapontype=ARTY.WeaponType.Cannon @@ -3599,13 +3616,13 @@ function ARTY:_AttackGroup(target) -- Set ROE to weapon free. group:OptionROEOpenFire() - + -- Target group. local targetgroup=GROUP:FindByName(target.name) - + -- Get task. local fire=group:TaskAttackGroup(targetgroup, weapontype, AI.Task.WeaponExpend.ONE, 1) - + -- Execute task. group:SetTask(fire) end @@ -3619,78 +3636,78 @@ function ARTY:_NuclearBlast(_coord) local S0=self.nukewarhead local R0=self.nukerange - + -- Number of fires local N0=self.nukefires - + -- Create an explosion at the last known position. _coord:Explosion(S0) - + -- Huge fire at direct impact point. --if self.nukefire then _coord:BigSmokeAndFireHuge() --end - + -- Create a table of fire coordinates within the demolition zone. local _fires={} - for i=1,N0 do + for i=1,N0 do local _fire=_coord:GetRandomCoordinateInRadius(R0) local _dist=_fire:Get2DDistance(_coord) table.insert(_fires, {distance=_dist, coord=_fire}) end - + -- Sort scenery wrt to distance from impact point. local _sort = function(a,b) return a.distance < b.distance end table.sort(_fires,_sort) - + local function _explosion(R) -- At R=R0 ==> explosion strength is 1% of S0 at impact point. local alpha=math.log(100) local strength=S0*math.exp(-alpha*R/R0) - self:T2(ARTY.id..string.format("Nuclear explosion strength s(%.1f m) = %.5f (s/s0=%.1f %%), alpha=%.3f", R, strength, strength/S0*100, alpha)) + self:T2(self.lid..string.format("Nuclear explosion strength s(%.1f m) = %.5f (s/s0=%.1f %%), alpha=%.3f", R, strength, strength/S0*100, alpha)) return strength end - + local function ignite(_fires) for _,fire in pairs(_fires) do local _fire=fire.coord --Core.Point#COORDINATE - + -- Get distance to impact and calc exponential explosion strength. local R=_fire:Get2DDistance(_coord) local S=_explosion(R) - self:T2(ARTY.id..string.format("Explosion r=%.1f, s=%.3f", R, S)) - + self:T2(self.lid..string.format("Explosion r=%.1f, s=%.3f", R, S)) + -- Get a random Big Smoke and fire object. local _preset=math.random(0,7) local _density=S/S0 --math.random()+0.1 - + _fire:BigSmokeAndFire(_preset,_density) _fire:Explosion(S) - + end end - + if self.nukefire==true then ignite(_fires) end - ---[[ + +--[[ local ZoneNuke=ZONE_RADIUS:New("Nukezone", _coord:GetVec2(), 2000) -- Scan for Scenery objects. ZoneNuke:Scan(Object.Category.SCENERY) - + -- Array with all possible hideouts, i.e. scenery objects in the vicinity of the group. local scenery={} for SceneryTypeName, SceneryData in pairs(ZoneNuke:GetScannedScenery()) do for SceneryName, SceneryObject in pairs(SceneryData) do - + local SceneryObject = SceneryObject -- Wrapper.Scenery#SCENERY - + -- Position of the scenery object. local spos=SceneryObject:GetCoordinate() - + -- Distance from group to impact point. local distance= spos:Get2DDistance(_coord) @@ -3700,18 +3717,18 @@ function ARTY:_NuclearBlast(_coord) local text=string.format("%s scenery: %s, Coord %s", self.Controllable:GetName(), SceneryObject:GetTypeName(), SceneryObject:GetCoordinate():ToStringLLDMS()) self:T2(SUPPRESSION.id..text) end - + -- Add to table. table.insert(scenery, {object=SceneryObject, distance=distance}) - - --SceneryObject:Destroy() + + --SceneryObject:Destroy() end end - + -- Sort scenery wrt to distance from impact point. -- local _sort = function(a,b) return a.distance < b.distance end -- table.sort(scenery,_sort) - + -- for _,object in pairs(scenery) do -- local sobject=object -- Wrapper.Scenery#SCENERY -- sobject:Destroy() @@ -3729,30 +3746,30 @@ end -- @param #boolean OnRoad If true, use (mainly) roads. function ARTY:_Move(group, ToCoord, Speed, OnRoad) self:F2({group=group:GetName(), Speed=Speed, OnRoad=OnRoad}) - + -- Clear all tasks. group:ClearTasks() group:OptionAlarmStateGreen() group:OptionROEHoldFire() - + -- Set formation. local formation = "Off Road" - + -- Get max speed of group. local SpeedMax=group:GetSpeedMax() - + -- Set speed. Speed=Speed or SpeedMax*0.7 - + -- Make sure, we do not go above max speed possible. Speed=math.min(Speed, SpeedMax) - + -- Current coordinates of group. local cpini=group:GetCoordinate() -- Core.Point#COORDINATE - - -- Distance between current and final point. + + -- Distance between current and final point. local dist=cpini:Get2DDistance(ToCoord) - + -- Waypoint and task arrays. local path={} local task={} @@ -3763,13 +3780,13 @@ function ARTY:_Move(group, ToCoord, Speed, OnRoad) -- Route group on road if requested. if OnRoad then - + -- Get path on road. local _pathonroad=cpini:GetPathOnRoad(ToCoord) - + -- Check if we actually got a path. There are situations where nil is returned. In that case, we go directly. if _pathonroad then - + -- Just take the first and last point. local _first=_pathonroad[1] local _last=_pathonroad[#_pathonroad] @@ -3778,47 +3795,47 @@ function ARTY:_Move(group, ToCoord, Speed, OnRoad) _first:SmokeGreen() _last:SmokeGreen() end - + -- First point on road. path[#path+1]=_first:WaypointGround(Speed, "On Road") task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) - + -- Last point on road. path[#path+1]=_last:WaypointGround(Speed, "On Road") task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) end - + end - + -- Last waypoint at ToCoord. path[#path+1]=ToCoord:WaypointGround(Speed, formation) task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, true) - + --if self.Debug then -- cpini:SmokeBlue() -- ToCoord:SmokeBlue() --end - + -- Init waypoints of the group. local Waypoints={} - + -- New points are added to the default route. for i=1,#path do table.insert(Waypoints, i, path[i]) end - + -- Set task for all waypoints. for i=1,#Waypoints do group:SetTaskWaypoint(Waypoints[i], task[i]) end - + -- Submit task and route group along waypoints. group:Route(Waypoints) end --- Function called when group is passing a waypoint. --- @param Wrapper.Group#GROUP group Group for which waypoint passing should be monitored. +-- @param Wrapper.Group#GROUP group Group for which waypoint passing should be monitored. -- @param #ARTY arty ARTY object. -- @param #number i Waypoint number that has been reached. -- @param #boolean final True if it is the final waypoint. @@ -3829,8 +3846,8 @@ function ARTY._PassingWaypoint(group, arty, i, final) if final then text=string.format("%s, arrived at destination.", group:GetName()) end - arty:T(ARTY.id..text) - + arty:T(self.lid..text) + --[[ if final then MESSAGE:New(text, 10):ToCoalitionIf(group:GetCoalition(), arty.Debug or arty.report) @@ -3838,7 +3855,7 @@ function ARTY._PassingWaypoint(group, arty, i, final) MESSAGE:New(text, 10):ToAllIf(arty.Debug) end ]] - + -- Arrived event. if final and arty.groupname==group:GetName() then arty:Arrived() @@ -3852,7 +3869,7 @@ function ARTY:_Relocate() -- Current position. local _pos=self.Controllable:GetCoordinate() - + local _new=nil local _gotit=false local _n=0 @@ -3861,7 +3878,7 @@ function ARTY:_Relocate() -- Get a random coordinate. _new=_pos:GetRandomCoordinateInRadius(self.relocateRmax, self.relocateRmin) local _surface=_new:GetSurfaceType() - + -- Check that new coordinate is not water(-ish). if _surface~=land.SurfaceType.WATER and _surface~=land.SurfaceType.SHALLOW_WATER then _gotit=true @@ -3869,7 +3886,7 @@ function ARTY:_Relocate() -- Increase counter. _n=_n+1 until _gotit or _n>_nmax - + -- Assign relocation. if _gotit then self:AssignMoveCoord(_new, nil, nil, false, false, "RELOCATION MOVE AFTER FIRING") @@ -3885,70 +3902,70 @@ end -- @return #number Number of missiles the group has left. function ARTY:GetAmmo(display) self:F3({display=display}) - + -- Default is display false. if display==nil then display=false end - + -- Init counter. local nammo=0 local nshells=0 local nrockets=0 local nmissiles=0 - + -- Get all units. local units=self.Controllable:GetUnits() if units==nil then return nammo, nshells, nrockets, nmissiles end - + for _,unit in pairs(units) do - + if unit and unit:IsAlive() then - + -- Output. local text=string.format("ARTY group %s - unit %s:\n", self.groupname, unit:GetName()) - + -- Get ammo table. local ammotable=unit:GetAmmo() if ammotable ~= nil then - + local weapons=#ammotable - + -- Display ammo table if display then - self:E(ARTY.id..string.format("Number of weapons %d.", weapons)) - self:E({ammotable=ammotable}) - self:E(ARTY.id.."Ammotable:") + self:I(self.lid..string.format("Number of weapons %d.", weapons)) + self:I({ammotable=ammotable}) + self:I(self.lid.."Ammotable:") for id,bla in pairs(ammotable) do - self:E({id=id, ammo=bla}) + self:I({id=id, ammo=bla}) end end - + -- Loop over all weapons. for w=1,weapons do - + -- Number of current weapon. local Nammo=ammotable[w]["count"] - + -- Typename of current weapon local Tammo=ammotable[w]["desc"]["typeName"] - + local _weaponString = self:_split(Tammo,"%.") local _weaponName = _weaponString[#_weaponString] - + -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3 local Category=ammotable[w].desc.category - + -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6 local MissileCategory=nil if Category==Weapon.Category.MISSILE then MissileCategory=ammotable[w].desc.missileCategory end - - + + -- Check for correct shell type. local _gotshell=false if #self.ammoshells>0 then @@ -3975,7 +3992,7 @@ function ARTY:GetAmmo(display) else if Category==Weapon.Category.ROCKET then _gotrocket=true - end + end end -- Check for correct missile type. @@ -3989,67 +4006,67 @@ function ARTY:GetAmmo(display) else if Category==Weapon.Category.MISSILE then _gotmissile=true - end + end end - + -- We are specifically looking for shells or rockets here. - if _gotshell then - + if _gotshell then + -- Add up all shells. nshells=nshells+Nammo - + -- Debug info. text=text..string.format("- %d shells of type %s\n", Nammo, _weaponName) - + elseif _gotrocket then - + -- Add up all rockets. nrockets=nrockets+Nammo - + -- Debug info. text=text..string.format("- %d rockets of type %s\n", Nammo, _weaponName) - + elseif _gotmissile then - + -- Add up all cruise missiles (category 5) if MissileCategory==Weapon.MissileCategory.CRUISE then nmissiles=nmissiles+Nammo end - + -- Debug info. text=text..string.format("- %d %s missiles of type %s\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName) - + else - + -- Debug info. text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) - + end - + end end -- Debug text and send message. if display then - self:E(ARTY.id..text) + self:I(self.lid..text) else - self:T3(ARTY.id..text) + self:T3(self.lid..text) end MESSAGE:New(text, 10):ToAllIf(display) - + end end - + -- Total amount of ammunition. nammo=nshells+nrockets+nmissiles - + return nammo, nshells, nrockets, nmissiles end --- Returns a name of a missile category. -- @param #ARTY self -- @param #number categorynumber Number of missile category from weapon missile category enumerator. See https://wiki.hoggitworld.com/view/DCS_Class_Weapon --- @return #string Missile category name. +-- @return #string Missile category name. function ARTY:_MissileCategoryName(categorynumber) local cat="unknown" if categorynumber==Weapon.MissileCategory.AAM then @@ -4076,7 +4093,7 @@ end --- Extract engagement assignments and parameters from mark text. -- @param #ARTY self -- @param #string text Marker text. --- @return #boolean If true, authentification successful. +-- @return #boolean If true, authentification successful. function ARTY:_MarkerKeyAuthentification(text) -- Set battery and coalition. @@ -4086,34 +4103,34 @@ function ARTY:_MarkerKeyAuthentification(text) -- Get assignment. local mykey=nil if self.markkey~=nil then - - -- keywords are split by "," + + -- keywords are split by "," local keywords=self:_split(text, ",") for _,key in pairs(keywords) do local s=self:_split(key, " ") local val=s[2] - if key:lower():find("key") then + if key:lower():find("key") then mykey=tonumber(val) - self:T(ARTY.id..string.format("Authorisation Key=%s.", val)) + self:T(self.lid..string.format("Authorisation Key=%s.", val)) end end - + end - + -- Check if the authorization key is required and if it is valid. local _validkey=true - + -- Check if group needs authorization. if self.markkey~=nil then -- Assume key is incorrect. _validkey=false - + -- If key was found, check if matches. if mykey~=nil then - _validkey=self.markkey==mykey - end - self:T2(ARTY.id..string.format("%s, authkey=%s == %s=playerkey ==> valid=%s", self.groupname, tostring(self.markkey), tostring(mykey), tostring(_validkey))) - + _validkey=self.markkey==mykey + end + self:T2(self.lid..string.format("%s, authkey=%s == %s=playerkey ==> valid=%s", self.groupname, tostring(self.markkey), tostring(mykey), tostring(_validkey))) + -- Send message local text="" if mykey==nil then @@ -4135,8 +4152,8 @@ end -- @return #table Table with assignment parameters, e.g. number of shots, radius, time etc. function ARTY:_Markertext(text) self:F(text) - - -- Assignment parameters. + + -- Assignment parameters. local assignment={} assignment.battery={} assignment.aliases={} @@ -4154,7 +4171,7 @@ function ARTY:_Markertext(text) assignment.cancelrearm=false assignment.setrearmingplace=false assignment.setrearminggroup=false - + -- Check for correct keywords. if text:lower():find("arty engage") or text:lower():find("arty attack") then assignment.engage=true @@ -4165,95 +4182,95 @@ function ARTY:_Markertext(text) elseif text:lower():find("arty cancel") then assignment.cancel=true elseif text:lower():find("arty set") then - assignment.set=true + assignment.set=true else - self:E(ARTY.id..'ERROR: Neither "ARTY ENGAGE" nor "ARTY MOVE" nor "ARTY RELOCATE" nor "ARTY REQUEST" nor "ARTY CANCEL" nor "ARTY SET" keyword specified!') + self:E(self.lid..'ERROR: Neither "ARTY ENGAGE" nor "ARTY MOVE" nor "ARTY RELOCATE" nor "ARTY REQUEST" nor "ARTY CANCEL" nor "ARTY SET" keyword specified!') return nil end - - -- keywords are split by "," + + -- keywords are split by "," local keywords=self:_split(text, ",") self:T({keywords=keywords}) for _,keyphrase in pairs(keywords) do - + -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma. local str=self:_split(keyphrase, " ") local key=str[1] local val=str[2] - + -- Debug output. - self:T3(ARTY.id..string.format("%s, keyphrase = %s, key = %s, val = %s", self.groupname, tostring(keyphrase), tostring(key), tostring(val))) - + self:T3(self.lid..string.format("%s, keyphrase = %s, key = %s, val = %s", self.groupname, tostring(keyphrase), tostring(key), tostring(val))) + -- Battery name, i.e. which ARTY group should fire. if key:lower():find("battery") then - + local v=self:_split(keyphrase, '"') - - for i=2,#v,2 do + + for i=2,#v,2 do table.insert(assignment.battery, v[i]) - self:T2(ARTY.id..string.format("Key Battery=%s.", v[i])) + self:T2(self.lid..string.format("Key Battery=%s.", v[i])) end elseif key:lower():find("alias") then - + local v=self:_split(keyphrase, '"') - - for i=2,#v,2 do + + for i=2,#v,2 do table.insert(assignment.aliases, v[i]) - self:T2(ARTY.id..string.format("Key Aliases=%s.", v[i])) + self:T2(self.lid..string.format("Key Aliases=%s.", v[i])) end elseif key:lower():find("cluster") then - + local v=self:_split(keyphrase, '"') - - for i=2,#v,2 do + + for i=2,#v,2 do table.insert(assignment.cluster, v[i]) - self:T2(ARTY.id..string.format("Key Cluster=%s.", v[i])) + self:T2(self.lid..string.format("Key Cluster=%s.", v[i])) end - + elseif keyphrase:lower():find("everyone") or keyphrase:lower():find("all batteries") or keyphrase:lower():find("allbatteries") then - + assignment.everyone=true - self:T(ARTY.id..string.format("Key Everyone=true.")) - + self:T(self.lid..string.format("Key Everyone=true.")) + elseif keyphrase:lower():find("irrevocable") or keyphrase:lower():find("readonly") then - + assignment.readonly=true - self:T2(ARTY.id..string.format("Key Readonly=true.")) - + self:T2(self.lid..string.format("Key Readonly=true.")) + elseif (assignment.engage or assignment.move) and key:lower():find("time") then - + if val:lower():find("now") then assignment.time=self:_SecondsToClock(timer.getTime0()+2) else assignment.time=val - end - self:T2(ARTY.id..string.format("Key Time=%s.", val)) - + end + self:T2(self.lid..string.format("Key Time=%s.", val)) + elseif assignment.engage and key:lower():find("shot") then - + assignment.nshells=tonumber(val) - self:T(ARTY.id..string.format("Key Shot=%s.", val)) - + self:T(self.lid..string.format("Key Shot=%s.", val)) + elseif assignment.engage and key:lower():find("prio") then - + assignment.prio=tonumber(val) self:T2(string.format("Key Prio=%s.", val)) - + elseif assignment.engage and key:lower():find("maxengage") then - + assignment.maxengage=tonumber(val) - self:T2(ARTY.id..string.format("Key Maxengage=%s.", val)) - + self:T2(self.lid..string.format("Key Maxengage=%s.", val)) + elseif assignment.engage and key:lower():find("radius") then - + assignment.radius=tonumber(val) - self:T2(ARTY.id..string.format("Key Radius=%s.", val)) - + self:T2(self.lid..string.format("Key Radius=%s.", val)) + elseif assignment.engage and key:lower():find("weapon") then - + if val:lower():find("cannon") then assignment.weapontype=ARTY.WeaponType.Cannon elseif val:lower():find("rocket") then @@ -4265,107 +4282,107 @@ function ARTY:_Markertext(text) elseif val:lower():find("illu") then assignment.weapontype=ARTY.WeaponType.IlluminationShells elseif val:lower():find("smoke") then - assignment.weapontype=ARTY.WeaponType.SmokeShells + assignment.weapontype=ARTY.WeaponType.SmokeShells else assignment.weapontype=ARTY.WeaponType.Auto - end - self:T2(ARTY.id..string.format("Key Weapon=%s.", val)) - + end + self:T2(self.lid..string.format("Key Weapon=%s.", val)) + elseif (assignment.move or assignment.set) and key:lower():find("speed") then - + assignment.speed=tonumber(val) - self:T2(ARTY.id..string.format("Key Speed=%s.", val)) - + self:T2(self.lid..string.format("Key Speed=%s.", val)) + elseif (assignment.move or assignment.set) and (keyphrase:lower():find("on road") or keyphrase:lower():find("onroad") or keyphrase:lower():find("use road")) then - + assignment.onroad=true - self:T2(ARTY.id..string.format("Key Onroad=true.")) - + self:T2(self.lid..string.format("Key Onroad=true.")) + elseif assignment.move and (keyphrase:lower():find("cancel target") or keyphrase:lower():find("canceltarget")) then - + assignment.movecanceltarget=true - self:T2(ARTY.id..string.format("Key Cancel Target (before move)=true.")) - + self:T2(self.lid..string.format("Key Cancel Target (before move)=true.")) + elseif assignment.request and keyphrase:lower():find("rearm") then - + assignment.requestrearming=true - self:T2(ARTY.id..string.format("Key Request Rearming=true.")) - + self:T2(self.lid..string.format("Key Request Rearming=true.")) + elseif assignment.request and keyphrase:lower():find("ammo") then - + assignment.requestammo=true - self:T2(ARTY.id..string.format("Key Request Ammo=true.")) + self:T2(self.lid..string.format("Key Request Ammo=true.")) elseif assignment.request and keyphrase:lower():find("target") then - + assignment.requesttargets=true - self:T2(ARTY.id..string.format("Key Request Targets=true.")) + self:T2(self.lid..string.format("Key Request Targets=true.")) elseif assignment.request and keyphrase:lower():find("status") then - + assignment.requeststatus=true - self:T2(ARTY.id..string.format("Key Request Status=true.")) + self:T2(self.lid..string.format("Key Request Status=true.")) elseif assignment.request and (keyphrase:lower():find("move") or keyphrase:lower():find("relocation")) then - + assignment.requestmoves=true - self:T2(ARTY.id..string.format("Key Request Moves=true.")) - + self:T2(self.lid..string.format("Key Request Moves=true.")) + elseif assignment.cancel and (keyphrase:lower():find("engagement") or keyphrase:lower():find("attack") or keyphrase:lower():find("target")) then - + assignment.canceltarget=true - self:T2(ARTY.id..string.format("Key Cancel Target=true.")) - + self:T2(self.lid..string.format("Key Cancel Target=true.")) + elseif assignment.cancel and (keyphrase:lower():find("move") or keyphrase:lower():find("relocation")) then - + assignment.cancelmove=true - self:T2(ARTY.id..string.format("Key Cancel Move=true.")) + self:T2(self.lid..string.format("Key Cancel Move=true.")) elseif assignment.cancel and keyphrase:lower():find("rearm") then - + assignment.cancelrearm=true - self:T2(ARTY.id..string.format("Key Cancel Rearm=true.")) + self:T2(self.lid..string.format("Key Cancel Rearm=true.")) elseif assignment.set and keyphrase:lower():find("rearming place") then - + assignment.setrearmingplace=true - self:T(ARTY.id..string.format("Key Set Rearming Place=true.")) + self:T(self.lid..string.format("Key Set Rearming Place=true.")) elseif assignment.set and keyphrase:lower():find("rearming group") then local v=self:_split(keyphrase, '"') local groupname=v[2] - + local group=GROUP:FindByName(groupname) if group and group:IsAlive() then assignment.setrearminggroup=group end - - self:T2(ARTY.id..string.format("Key Set Rearming Group = %s.", tostring(groupname))) - + + self:T2(self.lid..string.format("Key Set Rearming Group = %s.", tostring(groupname))) + elseif key:lower():find("lldms") then - + local _flat = "%d+:%d+:%d+%s*[N,S]" local _flon = "%d+:%d+:%d+%s*[W,E]" local _lat=keyphrase:match(_flat) local _lon=keyphrase:match(_flon) - self:T2(ARTY.id..string.format("Key LLDMS: lat=%s, long=%s format=DMS", _lat,_lon)) - + self:T2(self.lid..string.format("Key LLDMS: lat=%s, long=%s format=DMS", _lat,_lon)) + if _lat and _lon then - + -- Convert DMS string to DD numbers format. local _latitude, _longitude=self:_LLDMS2DD(_lat, _lon) - self:T2(ARTY.id..string.format("Key LLDMS: lat=%.3f, long=%.3f format=DD", _latitude,_longitude)) - + self:T2(self.lid..string.format("Key LLDMS: lat=%.3f, long=%.3f format=DD", _latitude,_longitude)) + -- Convert LL to coordinate object. if _latitude and _longitude then assignment.coord=COORDINATE:NewFromLLDD(_latitude,_longitude) end - + end - end + end end - + return assignment end @@ -4441,19 +4458,19 @@ end -- @return #number ID of the marked relocation move or nil function ARTY:_GetMarkIDfromName(name) - -- keywords are split by "," + -- keywords are split by "," local keywords=self:_split(name, ",") local battery=nil local markTID=nil local markMID=nil - + for _,key in pairs(keywords) do local str=self:_split(key, "=") local par=str[1] local val=str[2] - + if par:find("BATTERY") then battery=val end @@ -4463,9 +4480,9 @@ function ARTY:_GetMarkIDfromName(name) if par:find("Marked Relocation ID") then markMID=tonumber(val) end - + end - + return battery, markTID, markMID end @@ -4478,22 +4495,22 @@ end -- @param #ARTY self function ARTY:_SortTargetQueuePrio() self:F2() - + -- Sort results table wrt times they have already been engaged. local function _sort(a, b) return (a.engaged < b.engaged) or (a.engaged==b.engaged and a.prio < b.prio) end table.sort(self.targets, _sort) - + -- Debug output. - self:T3(ARTY.id.."Sorted targets wrt prio and number of engagements:") + self:T3(self.lid.."Sorted targets wrt prio and number of engagements:") for i=1,#self.targets do local _target=self.targets[i] - self:T3(ARTY.id..string.format("Target %s", self:_TargetInfo(_target))) + self:T3(self.lid..string.format("Target %s", self:_TargetInfo(_target))) end end ---- Sort array with respect to time. Array elements must have a .time entry. +--- Sort array with respect to time. Array elements must have a .time entry. -- @param #ARTY self -- @param #table queue Array to sort. Should have elemnt .time. function ARTY:_SortQueueTime(queue) @@ -4515,12 +4532,12 @@ function ARTY:_SortQueueTime(queue) table.sort(queue, _sort) -- Debug output. - self:T3(ARTY.id.."Sorted queue wrt time:") + self:T3(self.lid.."Sorted queue wrt time:") for i=1,#queue do local _queue=queue[i] local _time=tostring(_queue.time) local _clock=tostring(self:_SecondsToClock(_queue.time)) - self:T3(ARTY.id..string.format("%s: time=%s, clock=%s", _queue.name, _time, _clock)) + self:T3(self.lid..string.format("%s: time=%s, clock=%s", _queue.name, _time, _clock)) end end @@ -4545,115 +4562,115 @@ end 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))) - + + self:T3(self.lid..string.format("Before: Target %s - in range = %s", _target.name, tostring(_target.inrange))) + -- Check if target is in range. 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))) - + self:T3(self.lid..string.format("Inbetw: Target %s - in range = %s, toofar = %s, tooclose = %s", _target.name, tostring(_target.inrange), tostring(_toofar), tostring(_tooclose))) + if _remove then - + -- The ARTY group is immobile and not cargo but the target is not in range! table.insert(targets2delete, _target.name) - - else - + + else + -- Init default for assigning moves into range. local _movetowards=false local _moveaway=false - + 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) - + -- Send group towards/away from target. if _toofar then _movetowards=true elseif _tooclose then _moveaway=true end - + 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) + self:T(self.lid..text) MESSAGE:New(text,10):ToCoalitionIf(self.coalition, 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. + + -- 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. + + -- 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.coalition, 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))) + + self:T3(self.lid..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) end 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. @@ -4661,75 +4678,75 @@ end -- @return #table Target which is due to be attacked now or nil if no target could be found. function ARTY:_CheckNormalTargets() self:F3() - + -- Sort targets w.r.t. prio and number times engaged already. self:_SortTargetQueuePrio() - + -- No target engagements if rearming! if self:is("Rearming") then return nil end - + -- Loop over all sorted targets. - for i=1,#self.targets do + for i=1,#self.targets do local _target=self.targets[i] - + -- Debug info. - self:T3(ARTY.id..string.format("Check NORMAL target %d: %s", i, self:_TargetInfo(_target))) - + self:T3(self.lid..string.format("Check NORMAL target %d: %s", i, self:_TargetInfo(_target))) + -- Check that target no time, is not under fire currently and in range. if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged and self:_TargetInRange(_target) and self:_CheckWeaponTypeAvailable(_target)>0 then - + -- Debug info. - self:T2(ARTY.id..string.format("Found NORMAL target %s", self:_TargetInfo(_target))) - + self:T2(self.lid..string.format("Found NORMAL target %s", self:_TargetInfo(_target))) + return _target end end - + return nil end --- Check all timed targets and return the target which should be attacked next. -- @param #ARTY self --- @return #table Target which is due to be attacked now. +-- @return #table Target which is due to be attacked now. function ARTY:_CheckTimedTargets() self:F3() - + -- Current time. local Tnow=timer.getAbsTime() - + -- Sort Targets wrt time. self:_SortQueueTime(self.targets) - + -- No target engagements if rearming! if self:is("Rearming") then return nil end - + for i=1,#self.targets do local _target=self.targets[i] - + -- Debug info. - self:T3(ARTY.id..string.format("Check TIMED target %d: %s", i, self:_TargetInfo(_target))) - - -- Check if target has an attack time which has already passed. Also check that target is not under fire already and that it is in range. + self:T3(self.lid..string.format("Check TIMED target %d: %s", i, self:_TargetInfo(_target))) + + -- Check if target has an attack time which has already passed. Also check that target is not under fire already and that it is in range. if _target.time and Tnow>=_target.time and _target.underfire==false and self:_TargetInRange(_target) and self:_CheckWeaponTypeAvailable(_target)>0 then - + -- Check if group currently has a target and whether its priorty is lower than the timed target. if self.currentTarget then if self.currentTarget.prio > _target.prio then -- Current target under attack but has lower priority than this target. - self:T2(ARTY.id..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInfo(_target))) + self:T2(self.lid..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInfo(_target))) return _target end else -- No current target. - self:T2(ARTY.id..string.format("Found TIMED target %s.", self:_TargetInfo(_target))) + self:T2(self.lid..string.format("Found TIMED target %s.", self:_TargetInfo(_target))) return _target end end - + end return nil @@ -4737,37 +4754,37 @@ end --- Check all moves and return the one which should be executed next. -- @param #ARTY self --- @return #table Move which is due. +-- @return #table Move which is due. function ARTY:_CheckMoves() self:F3() - + -- Current time. local Tnow=timer.getAbsTime() - + -- Sort Targets wrt time. self:_SortQueueTime(self.moves) - + -- Check if we are currently firing. local firing=false if self.currentTarget then firing=true end - + -- Loop over all moves in queue. for i=1,#self.moves do - + -- Shortcut. local _move=self.moves[i] - + if string.find(_move.name, "REARMING MOVE") and ((self.currentMove and self.currentMove.name~=_move.name) or self.currentMove==nil) then - -- We got an rearming assignment which has priority. + -- We got an rearming assignment which has priority. return _move elseif (Tnow >= _move.time) and (firing==false or _move.cancel) and (not self.currentMove) and (not self:is("Rearming")) then -- Time for move is reached and maybe current target should be cancelled. return _move - end + end end - + return nil end @@ -4775,35 +4792,35 @@ end -- @param #ARTY self function ARTY:_CheckShootingStarted() self:F2() - + if self.currentTarget then - + -- Current time. local Tnow=timer.getTime() - + -- Get name and id of target. local name=self.currentTarget.name - + -- Time that passed after current target has been assigned. local dt=Tnow-self.currentTarget.Tassigned - + -- Debug info if self.Nshots==0 then - self:T(ARTY.id..string.format("%s, waiting for %d seconds for first shot on target %s.", self.groupname, dt, name)) + self:T(self.lid..string.format("%s, waiting for %d seconds for first shot on target %s.", self.groupname, dt, name)) end - + -- Check if we waited long enough and no shot was fired. if dt > self.WaitForShotTime and self.Nshots==0 then - + -- Debug info. - self:T(ARTY.id..string.format("%s, no shot event after %d seconds. Removing current target %s from list.", self.groupname, self.WaitForShotTime, name)) - + self:T(self.lid..string.format("%s, no shot event after %d seconds. Removing current target %s from list.", self.groupname, self.WaitForShotTime, name)) + -- CeaseFire. self:CeaseFire(self.currentTarget) - + -- Remove target from list. self:RemoveTarget(name) - + end end end @@ -4814,17 +4831,17 @@ end -- @return #number Arrayindex of target. function ARTY:_GetTargetIndexByName(name) self:F2(name) - + for i=1,#self.targets do local targetname=self.targets[i].name - self:T3(ARTY.id..string.format("Have target with name %s. Index = %d", targetname, i)) + self:T3(self.lid..string.format("Have target with name %s. Index = %d", targetname, i)) if targetname==name then - self:T2(ARTY.id..string.format("Found target with name %s. Index = %d", name, i)) + self:T2(self.lid..string.format("Found target with name %s. Index = %d", name, i)) return i end end - - self:T2(ARTY.id..string.format("WARNING: Target with name %s could not be found. (This can happen.)", name)) + + self:T2(self.lid..string.format("WARNING: Target with name %s could not be found. (This can happen.)", name)) return nil end @@ -4834,17 +4851,17 @@ end -- @return #number Arrayindex of move. function ARTY:_GetMoveIndexByName(name) self:F2(name) - + for i=1,#self.moves do local movename=self.moves[i].name - self:T3(ARTY.id..string.format("Have move with name %s. Index = %d", movename, i)) + self:T3(self.lid..string.format("Have move with name %s. Index = %d", movename, i)) if movename==name then - self:T2(ARTY.id..string.format("Found move with name %s. Index = %d", name, i)) + self:T2(self.lid..string.format("Found move with name %s. Index = %d", name, i)) return i end end - - self:T2(ARTY.id..string.format("WARNING: Move with name %s could not be found. (This can happen.)", name)) + + self:T2(self.lid..string.format("WARNING: Move with name %s could not be found. (This can happen.)", name)) return nil end @@ -4859,48 +4876,48 @@ function ARTY:_CheckOutOfAmmo(targets) -- Special weapon type requested ==> Check if corresponding ammo is empty. local _partlyoutofammo=false - + for _,Target in pairs(targets) do - + if Target.weapontype==ARTY.WeaponType.Auto and _nammo==0 then - self:T(ARTY.id..string.format("Group %s, auto weapon requested for target %s but all ammo is empty.", self.groupname, Target.name)) + self:T(self.lid..string.format("Group %s, auto weapon requested for target %s but all ammo is empty.", self.groupname, Target.name)) _partlyoutofammo=true - + elseif Target.weapontype==ARTY.WeaponType.Cannon and _nshells==0 then - - self:T(ARTY.id..string.format("Group %s, cannons requested for target %s but shells empty.", self.groupname, Target.name)) + + self:T(self.lid..string.format("Group %s, cannons requested for target %s but shells empty.", self.groupname, Target.name)) _partlyoutofammo=true - + elseif Target.weapontype==ARTY.WeaponType.TacticalNukes and self.Nukes<=0 then - - self:T(ARTY.id..string.format("Group %s, tactical nukes requested for target %s but nukes empty.", self.groupname, Target.name)) + + self:T(self.lid..string.format("Group %s, tactical nukes requested for target %s but nukes empty.", self.groupname, Target.name)) _partlyoutofammo=true elseif Target.weapontype==ARTY.WeaponType.IlluminationShells and self.Nillu<=0 then - - self:T(ARTY.id..string.format("Group %s, illumination shells requested for target %s but illumination shells empty.", self.groupname, Target.name)) + + self:T(self.lid..string.format("Group %s, illumination shells requested for target %s but illumination shells empty.", self.groupname, Target.name)) _partlyoutofammo=true elseif Target.weapontype==ARTY.WeaponType.SmokeShells and self.Nsmoke<=0 then - - self:T(ARTY.id..string.format("Group %s, smoke shells requested for target %s but smoke shells empty.", self.groupname, Target.name)) + + self:T(self.lid..string.format("Group %s, smoke shells requested for target %s but smoke shells empty.", self.groupname, Target.name)) _partlyoutofammo=true - + elseif Target.weapontype==ARTY.WeaponType.Rockets and _nrockets==0 then - - self:T(ARTY.id..string.format("Group %s, rockets requested for target %s but rockets empty.", self.groupname, Target.name)) + + self:T(self.lid..string.format("Group %s, rockets requested for target %s but rockets empty.", self.groupname, Target.name)) _partlyoutofammo=true elseif Target.weapontype==ARTY.WeaponType.CruiseMissile and _nmissiles==0 then - - self:T(ARTY.id..string.format("Group %s, cruise missiles requested for target %s but all missiles empty.", self.groupname, Target.name)) + + self:T(self.lid..string.format("Group %s, cruise missiles requested for target %s but all missiles empty.", self.groupname, Target.name)) _partlyoutofammo=true - + end - + end - + return _partlyoutofammo end @@ -4912,7 +4929,7 @@ function ARTY:_CheckWeaponTypeAvailable(target) -- Get current ammo of group. local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() - + -- Check if enough ammo is there for the selected weapon type. local nfire=Nammo if target.weapontype==ARTY.WeaponType.Auto then @@ -4930,7 +4947,7 @@ function ARTY:_CheckWeaponTypeAvailable(target) elseif target.weapontype==ARTY.WeaponType.CruiseMissile then nfire=Nmissiles end - + return nfire end --- Check if a selected weapon type is in principle possible for this group. The current amount of ammo might be zero but the group still can be rearmed at a later point in time. @@ -4956,7 +4973,7 @@ function ARTY:_CheckWeaponTypePossible(target) elseif target.weapontype==ARTY.WeaponType.CruiseMissile then possible=self.Nmissiles0>0 end - + return possible end @@ -4967,7 +4984,7 @@ end -- @param #boolean makeunique If true, a new unique name is returned by appending the running index. -- @return #string Unique name, which is not already given for another target. function ARTY:_CheckName(givennames, name, makeunique) - self:F2({givennames=givennames, name=name}) + self:F2({givennames=givennames, name=name}) local newname=name local counter=1 @@ -4976,54 +4993,54 @@ function ARTY:_CheckName(givennames, name, makeunique) if makeunique==nil then makeunique=true end - + repeat -- until a unique name is found. - + -- We assume the name is unique. local _unique=true - + -- Loop over all targets already defined. for _,_target in pairs(givennames) do - + -- Target name. local _givenname=_target.name - + -- Name is already used by another target. if _givenname==newname then - + -- Name is already used for another target ==> try again with new name. _unique=false - + end - + -- Debug info. - self:T3(ARTY.id..string.format("%d: givenname = %s, newname=%s, unique = %s, makeunique = %s", n, tostring(_givenname), newname, tostring(_unique), tostring(makeunique))) + self:T3(self.lid..string.format("%d: givenname = %s, newname=%s, unique = %s, makeunique = %s", n, tostring(_givenname), newname, tostring(_unique), tostring(makeunique))) end - + -- Create a new name if requested and try again. if _unique==false and makeunique==true then - + -- Define newname = "name #01" newname=string.format("%s #%02d", name, counter) - + -- Increase counter. counter=counter+1 end - + -- Name is not unique and we don't want to make it unique. if _unique==false and makeunique==false then - self:T3(ARTY.id..string.format("Name %s is not unique. Return false.", tostring(newname))) - + self:T3(self.lid..string.format("Name %s is not unique. Return false.", tostring(newname))) + -- Return return name, false end - + -- Increase loop counter. We try max 100 times. n=n+1 until (_unique or n==nmax) - + -- Debug output and return new name. - self:T3(ARTY.id..string.format("Original name %s, new name = %s", name, newname)) + self:T3(self.lid..string.format("Original name %s, new name = %s", name, newname)) return newname, true end @@ -5037,22 +5054,22 @@ end -- @return #boolean True if target should be removed since ARTY group is immobile and not cargo. function ARTY:_TargetInRange(target, message) self:F3(target) - + -- Default is no message. if message==nil then message=false end -- Distance between ARTY group and target. - self:E({controllable=self.Controllable, targetcoord=target.coord}) + self:T3({controllable=self.Controllable, targetcoord=target.coord}) local _dist=self.Controllable:GetCoordinate():Get2DDistance(target.coord) - + -- Assume we are in range. local _inrange=true local _tooclose=false local _toofar=false local text="" - + if _dist < self.minrange then _inrange=false _tooclose=true @@ -5062,13 +5079,13 @@ function ARTY:_TargetInRange(target, message) _toofar=true text=string.format("%s, target is out of range. Distance of %.1f km is greater than max range of %.1f km.", self.alias, _dist/1000, self.maxrange/1000) end - + -- Debug output. if not _inrange then - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 5):ToCoalitionIf(self.coalition, (self.report and message) or (self.Debug and 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 @@ -5099,12 +5116,12 @@ function ARTY:_WeaponTypeName(tnumber) elseif tnumber==ARTY.WeaponType.IlluminationShells then name="Illumination Shells" elseif tnumber==ARTY.WeaponType.SmokeShells then - name="Smoke Shells" + name="Smoke Shells" end return name end ---- Find a random coordinate in the vicinity of another coordinate. +--- Find a random coordinate in the vicinity of another coordinate. -- @param #ARTY self -- @param Core.Point#COORDINATE coord Center coordinate. -- @param #number rmin (Optional) Minimum distance in meters from center coordinate. Default 20 m. @@ -5119,11 +5136,11 @@ function ARTY:_VicinityCoord(coord, rmin, rmax) local vec2=coord:GetRandomVec2InRadius(rmax, rmin) local pops=COORDINATE:NewFromVec2(vec2) -- Debug info. - self:T3(ARTY.id..string.format("Vicinity distance = %d (rmin=%d, rmax=%d)", pops:Get2DDistance(coord), rmin, rmax)) + self:T3(self.lid..string.format("Vicinity distance = %d (rmin=%d, rmax=%d)", pops:Get2DDistance(coord), rmin, rmax)) return pops end ---- Print event-from-to string to DCS log file. +--- Print event-from-to string to DCS log file. -- @param #ARTY self -- @param #string BA Before/after info. -- @param #string Event Event. @@ -5131,7 +5148,7 @@ end -- @param #string To To state. function ARTY:_EventFromTo(BA, Event, From, To) local text=string.format("%s: %s EVENT %s: %s --> %s", BA, self.groupname, Event, From, To) - self:T3(ARTY.id..text) + self:T3(self.lid..text) end --- Split string. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua @@ -5140,7 +5157,7 @@ end -- @param #string sep Speparator for split. -- @return #table Split text. function ARTY:_split(str, sep) - self:F3({str=str, sep=sep}) + self:F3({str=str, sep=sep}) local result = {} local regex = ("([^%s]+)"):format(sep) for each in str:gmatch(regex) do @@ -5182,29 +5199,29 @@ function ARTY:_LLDMS2DD(l1,l2) -- Make an array of lat and long. local _latlong={l1,l2} - + local _latitude=nil local _longitude=nil - + for _,ll in pairs(_latlong) do - + -- Format is expected as "DD:MM:SS" or "D:M:S". - local _format = "%d+:%d+:%d+" + local _format = "%d+:%d+:%d+" local _ldms=ll:match(_format) - + if _ldms then - + -- Split DMS to degrees, minutes and seconds. local _dms=self:_split(_ldms, ":") local _deg=tonumber(_dms[1]) local _min=tonumber(_dms[2]) local _sec=tonumber(_dms[3]) - + -- Convert DMS to DD. local function DMS2DD(d,m,s) return d+m/60+s/3600 end - + -- Detect with hemisphere is meant. if ll:match("N") then _latitude=DMS2DD(_deg,_min,_sec) @@ -5215,19 +5232,19 @@ function ARTY:_LLDMS2DD(l1,l2) elseif ll:match("E") then _longitude=DMS2DD(_deg,_min,_sec) end - + -- Debug text. local text=string.format("DMS %02d Deg %02d min %02d sec",_deg,_min,_sec) - self:T2(ARTY.id..text) + self:T2(self.lid..text) - end + end end - + -- Debug text. local text=string.format("\nLatitude %s", tostring(_latitude)) text=text..string.format("\nLongitude %s", tostring(_longitude)) - self:T2(ARTY.id..text) - + self:T2(self.lid..text) + return _latitude,_longitude end @@ -5237,14 +5254,14 @@ end -- @return #string Time in format Hours:minutes:seconds. function ARTY:_SecondsToClock(seconds) self:F3({seconds=seconds}) - + if seconds==nil then return nil end - + -- Seconds local seconds = tonumber(seconds) - + -- Seconds of this day. local _seconds=seconds%(60*60*24) @@ -5264,23 +5281,23 @@ end -- @param #string clock String of clock time. E.g., "06:12:35". function ARTY:_ClockToSeconds(clock) self:F3({clock=clock}) - + if clock==nil then return nil end - + -- Seconds init. local seconds=0 - + -- Split additional days. local dsplit=self:_split(clock, "+") - + -- Convert days to seconds. if #dsplit>1 then seconds=seconds+tonumber(dsplit[2])*60*60*24 end - -- Split hours, minutes, seconds + -- Split hours, minutes, seconds local tsplit=self:_split(dsplit[1], ":") -- Get time in seconds @@ -5298,13 +5315,11 @@ function ARTY:_ClockToSeconds(clock) end i=i+1 end - - self:T3(ARTY.id..string.format("Clock %s = %d seconds", clock, seconds)) + + self:T3(self.lid..string.format("Clock %s = %d seconds", clock, seconds)) return seconds end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - \ No newline at end of file From faa2debbae06447ec4bb38eb44d3b78d895c6c23 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 4 Nov 2019 22:17:26 +0100 Subject: [PATCH 389/485] DATABASE & RAT - DATABASE: spawned units (ships) are registered as airbases. - RAT v2.3.8: script will not crash if (spawned) AIRBASE cannot be found . --- Moose Development/Moose/Core/Database.lua | 6 ++++++ Moose Development/Moose/Functional/RAT.lua | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 4463defbc..1a2af6d62 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -923,6 +923,12 @@ function DATABASE:_EventOnBirth( Event ) if Event.IniObjectCategory == 1 then self:AddUnit( Event.IniDCSUnitName ) self:AddGroup( Event.IniDCSGroupName ) + -- Add airbase if it was spawned later in the mission. + local DCSAirbase = Airbase.getByName(Event.IniDCSUnitName) + if DCSAirbase then + self:I(string.format("Adding airbase %s", tostring(Event.IniDCSUnitName))) + self:AddAirbase(Event.IniDCSUnitName) + end end end if Event.IniObjectCategory == 1 then diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 071163246..322c3ea86 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -550,7 +550,7 @@ RAT.id="RAT | " --- RAT version. -- @list version RAT.version={ - version = "2.3.7", + version = "2.3.8", print = true, } @@ -3392,11 +3392,19 @@ function RAT:_GetAirportsOfMap() local _name=airbase:getName() local _myab=AIRBASE:FindByName(_name) - -- Add airport to table. - table.insert(self.airports_map, _myab) + if _myab then - local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() - self:T(RAT.id..text) + -- Add airport to table. + table.insert(self.airports_map, _myab) + + local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() + self:T(RAT.id..text) + + else + + self:E(RAT.id..string.format("WARNING: Airbase %s does not exsist as MOOSE object!", tostring(_name))) + + end end end From efccefecadc8a6fb51022fd08e64e310c59aa917 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 9 Nov 2019 20:55:28 +0100 Subject: [PATCH 390/485] RANGE v2.2.0 - Added option for range instructor and range control radio (needs voice overs). - Reduced number of smokes for strafe pits. - Updated docs. - SetAutoSave will auto load as well. - other --- Moose Development/Moose/Functional/Range.lua | 539 +++++++++++++++---- 1 file changed, 449 insertions(+), 90 deletions(-) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index a1d716058..d3fc4d1aa 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -9,24 +9,34 @@ -- -- [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is highly recommended for this class. -- --- ## Features: +-- **Main Features:** -- -- * 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. --- * Range can be illuminated by illumination bombs for night practices. +-- * Range can be illuminated by illumination bombs for night missions. -- * 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. +-- * Persistence: Bombing range results can be saved to disk and loaded the next time the mission is started. +-- * Range control voice overs (>40) for hit assessment. -- -- === -- --- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) --- ### [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M) +-- ## Youtube Videos: + -- +-- * [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- * [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M) +-- +-- === +-- +-- ## Missions: Example missions will be added later. +-- +-- === +-- +-- ## Sound files: Check out the pinned messages in the Moose discord *#func-range* channel. -- -- === -- @@ -79,10 +89,23 @@ -- @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. -- @field #boolean autosave If true, automatically save results every X seconds. --- @extends Core.Base#BASE +-- @field #number instructorfreq Frequency on which the range control transmitts. +-- @field Core.RadioQueue#RADIOQUEUE instructor Instructor radio queue. +-- @field #number rangecontrolfreq Frequency on which the range control transmitts. +-- @field Core.RadioQueue#RADIOQUEUE rangecontrol Range control radio queue. +-- @field #string soundpath Path inside miz file where the sound files are located. Default is "Range Soundfiles/". +-- @extends Core.Fsm#FSM ---- 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" defines the name of the range. It has to be unique since this is also the name displayed in the radio menu. +--- *Don't only practice your art, but force your way into its secrets; art deserves that, for it and knowledge can raise man to the Divine.* - Ludwig van Beethoven +-- +-- === +-- +-- ![Banner Image](..\Presentations\RANGE\RANGE_Main.png) +-- +-- # The Range Concept +-- +-- The RANGE class 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* 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. @@ -97,7 +120,8 @@ -- 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 +-- # Strafe Pits +-- -- 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. @@ -117,7 +141,8 @@ -- 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 +-- # 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. @@ -126,10 +151,18 @@ -- * 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. -- +-- ## Adding Groups +-- -- 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. +-- +-- ## Specifying Coordinates +-- +-- It is also possible to specify coordinates rather than unit or static objects as bombing target locations. This has the advantage, that even when the unit/static object is dead, the specified +-- coordinate will still be a valid impact point. This can be done via the @{#RANGE.AddBombingTargetCoordinate}(*coord*, *name*, *goodhitrange*) function. -- --- ## Fine Tuning +-- # 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. @@ -144,22 +177,61 @@ -- * @{#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 +-- # 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 main range menu can be found at "F10. Other..." --> "F*X*. On the Range..." --> "F1. ...". -- -- The range menu contains the following submenues: +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_Main.png) -- --- * "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": Temperature, wind and QFE pressure information is provided. +-- * "F1. Statistics...": Range results of all players and personal stats. +-- * "F2. Mark Targets": Mark range targets by smoke or flares. +-- * "F3. My Settings" Personal settings. +-- * "F4. Range Info": Information about the range, such as bearing and range. +-- +-- ## F1 Statistics +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_Stats.png) +-- +-- ## F2 Mark Targets +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_Stats.png) +-- +-- ## F3 My Settings +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_MySettings.png) +-- +-- ## F4 Range Info +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_RangeInfo.png) +-- +-- # Voice Overs +-- +-- Voice over sound files can be downloaded from the Moose Discord. Check the pinned messages in the *#func-range* channel. +-- +-- Instructor radio will inform players when they enter or exit the range zone and provide the radio frequency of the range control for hit assessment. +-- This can be enabled via the @{#RANGE.SetInstructorRadio}(*frequency*) functions, where *frequency* is the AM frequency in MHz. +-- +-- The range control can be enabled via the @{#RANGE.SetRangeControl}(*frequency*) functions, where *frequency* is the AM frequency in MHz. +-- +-- By default, the sound files are placed in the "Range Soundfiles/" folder inside the mission (.miz) file. Another folder can be specified via the @{#RANGE.SetSoundfilesPath}(*path*) function. +-- +-- # Persistence +-- +-- To automatically save bombing results to disk, use the @{#RANGE.SetAutosave}() function. Bombing results will be saved as csv file in your "Saved Games\DCS.openbeta\Logs" directory. +-- Each range has a separate file, which is named "RANGE-<*RangeName*>_BombingResults.csv". +-- +-- The next time you start the mission, these results are also automatically loaded. +-- +-- Strafing results are currently **not** saved. -- --- ## Examples +-- # Examples -- --- ### Goldwater Range +-- ## 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. -- @@ -190,7 +262,7 @@ -- -- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is (implicitly) used in this example. -- --- ## Debugging +-- # 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 @@ -213,44 +285,49 @@ -- -- @field #RANGE RANGE={ - ClassName = "RANGE", - Debug=false, - id=nil, - rangename=nil, - location=nil, - messages=true, - rangeradius=5000, - rangezone=nil, - strafeTargets={}, - bombingTargets={}, - nbombtargets=0, - nstrafetargets=0, - MenuAddedTo = {}, - planes = {}, - strafeStatus = {}, + ClassName = "RANGE", + Debug = false, + id = nil, + rangename = nil, + location = nil, + messages = true, + rangeradius = 5000, + rangezone = nil, + strafeTargets = {}, + bombingTargets = {}, + nbombtargets = 0, + nstrafetargets = 0, + MenuAddedTo = {}, + planes = {}, + strafeStatus = {}, strafePlayerResults = {}, - bombPlayerResults = {}, - PlayerSettings = {}, - dtBombtrack=0.005, - BombtrackThreshold=25000, - Tmsg=30, - examinergroupname=nil, - examinerexclusive=nil, - strafemaxalt=914, - ndisplayresult=10, - BombSmokeColor=SMOKECOLOR.Red, - StrafeSmokeColor=SMOKECOLOR.Green, - StrafePitSmokeColor=SMOKECOLOR.White, - illuminationminalt=500, - illuminationmaxalt=1000, - scorebombdistance=1000, - TdelaySmoke=3.0, - eventmoose=true, - trackbombs=true, - trackrockets=true, - trackmissiles=true, - defaultsmokebomb=true, - autosave=false, + bombPlayerResults = {}, + PlayerSettings = {}, + dtBombtrack = 0.005, + BombtrackThreshold = 25000, + Tmsg = 30, + examinergroupname = nil, + examinerexclusive = nil, + strafemaxalt = 914, + ndisplayresult = 10, + BombSmokeColor = SMOKECOLOR.Red, + StrafeSmokeColor = SMOKECOLOR.Green, + StrafePitSmokeColor = SMOKECOLOR.White, + illuminationminalt = 500, + illuminationmaxalt = 1000, + scorebombdistance = 1000, + TdelaySmoke = 3.0, + eventmoose = true, + trackbombs = true, + trackrockets = true, + trackmissiles = true, + defaultsmokebomb = true, + autosave = false, + instructorfreq = nil, + instructor = nil, + rangecontrolfreq = nil, + rangecontrol = nil, + soundpath = "Range Soundfiles/" } --- Default range parameters. @@ -325,6 +402,105 @@ RANGE.TargetType={ -- @field #string player Player name. -- @field #string airframe Aircraft type of player. -- @field #number time Time via timer.getAbsTime() in seconds of impact. +-- @field #string date OS date. + +--- Sound file data. +-- @type RANGE.Soundfile +-- @field #string filename Name of the file +-- @field #number duration Duration in seconds. + +--- Sound files. +-- @type RANGE.Sound +-- @field #RANGE.Soundfile RC0 +-- @field #RANGE.Soundfile RC1 +-- @field #RANGE.Soundfile RC2 +-- @field #RANGE.Soundfile RC3 +-- @field #RANGE.Soundfile RC4 +-- @field #RANGE.Soundfile RC5 +-- @field #RANGE.Soundfile RC6 +-- @field #RANGE.Soundfile RC7 +-- @field #RANGE.Soundfile RC8 +-- @field #RANGE.Soundfile RC9 +-- @field #RANGE.Soundfile RCAccuracy +-- @field #RANGE.Soundfile RCDegrees +-- @field #RANGE.Soundfile RCExcellentHit +-- @field #RANGE.Soundfile RCExcellentPass +-- @field #RANGE.Soundfile RCFeet +-- @field #RANGE.Soundfile RCFor +-- @field #RANGE.Soundfile RCGoodHit +-- @field #RANGE.Soundfile RCGoodPass +-- @field #RANGE.Soundfile RCHitsOnTarget +-- @field #RANGE.Soundfile RCImpact +-- @field #RANGE.Soundfile RCIneffectiveHit +-- @field #RANGE.Soundfile RCIneffectivePass +-- @field #RANGE.Soundfile RCInvalidHit +-- @field #RANGE.Soundfile RCLeftStrafePitTooQuickly +-- @field #RANGE.Soundfile RCPercent +-- @field #RANGE.Soundfile RCPoorHit +-- @field #RANGE.Soundfile RCPoorPass +-- @field #RANGE.Soundfile RCRollingInOnStrafeTarget +-- @field #RANGE.Soundfile RCTotalRoundsFired +-- @field #RANGE.Soundfile RCWeaponImpactedTooFar +-- @field #RANGE.Soundfile IR0 +-- @field #RANGE.Soundfile IR1 +-- @field #RANGE.Soundfile IR2 +-- @field #RANGE.Soundfile IR3 +-- @field #RANGE.Soundfile IR4 +-- @field #RANGE.Soundfile IR5 +-- @field #RANGE.Soundfile IR6 +-- @field #RANGE.Soundfile IR7 +-- @field #RANGE.Soundfile IR8 +-- @field #RANGE.Soundfile IR9 +-- @field #RANGE.Soundfile IRDecimal +-- @field #RANGE.Soundfile IRMegaHertz +-- @field #RANGE.Soundfile IREnterRange +-- @field #RANGE.Soundfile IRExitRange +RANGE.Sound = { + RC0={filename="RC-0.ogg", duration=0.60}, + RC1={filename="RC-1.ogg", duration=0.47}, + RC2={filename="RC-2.ogg", duration=0.43}, + RC3={filename="RC-3.ogg", duration=0.50}, + RC4={filename="RC-4.ogg", duration=0.58}, + RC5={filename="RC-5.ogg", duration=0.54}, + RC6={filename="RC-6.ogg", duration=0.61}, + RC7={filename="RC-7.ogg", duration=0.53}, + RC8={filename="RC-8.ogg", duration=0.34}, + RC9={filename="RC-9.ogg", duration=0.54}, + RCAccuracy={filename="RC-Accuracy.ogg", duration=0.67}, + RCDegrees={filename="RC-Degrees.ogg", duration=0.59}, + RCExcellentHit={filename="RC-ExcellentHit.ogg", duration=0.76}, + RCExcellentPass={filename="RC-ExcellentPass.ogg", duration=0.89}, + RCFeet={filename="RC-Feet.ogg", duration=0.49}, + RCFor={filename="RC-For.ogg", duration=0.64}, + RCGoodHit={filename="RC-GoodHit.ogg", duration=0.52}, + RCGoodPass={filename="RC-GoodPass.ogg", duration=0.62}, + RCHitsOnTarget={filename="RC-HitsOnTarget.ogg", duration=0.88}, + RCImpact={filename="RC-Impact.ogg", duration=0.61}, + RCIneffectiveHit={filename="RC-IneffectiveHit.ogg", duration=0.86}, + RCIneffectivePass={filename="RC-IneffectivePass.ogg", duration=0.99}, + RCInvalidHit={filename="RC-InvalidHit.ogg", duration=2.97}, + RCLeftStrafePitTooQuickly={filename="RC-LeftStrafePitTooQuickly.ogg", duration=3.09}, + RCPercent={filename="RC-Percent.ogg", duration=0.56}, + RCPoorHit={filename="RC-PoorHit.ogg", duration=0.54}, + RCPoorPass={filename="RC-PoorPass.ogg", duration=0.68}, + RCRollingInOnStrafeTarget={filename="RC-RollingInOnStrafeTarget.ogg", duration=1.38}, + RCTotalRoundsFired={filename="RC-TotalRoundsFired.ogg", duration=1.22}, + RCWeaponImpactedTooFar={filename="RC-WeaponImpactedTooFar.ogg", duration=3.73}, + IR0={filename="IR-0.ogg", duration=0.55}, + IR1={filename="IR-1.ogg", duration=0.41}, + IR2={filename="IR-2.ogg", duration=0.37}, + IR3={filename="IR-3.ogg", duration=0.41}, + IR4={filename="IR-4.ogg", duration=0.37}, + IR5={filename="IR-5.ogg", duration=0.43}, + IR6={filename="IR-6.ogg", duration=0.55}, + IR7={filename="IR-7.ogg", duration=0.43}, + IR8={filename="IR-8.ogg", duration=0.38}, + IR9={filename="IR-9.ogg", duration=0.55}, + IRDecimal={filename="IR-Decimal.ogg", duration=0.54}, + IRMegaHertz={filename="IR-MegaHertz.ogg", duration=0.87}, + IREnterRange={filename="IR-EnterRange.ogg", duration=4.83}, + IRExitRange={filename="IR-ExitRange.ogg", duration=3.10}, +} --- Global list of all defined range names. -- @field #table Names @@ -340,7 +516,7 @@ RANGE.MenuF10Root=nil --- Range script version. -- @field #string version -RANGE.version="2.1.2" +RANGE.version="2.2.0" --TODO list: --TODO: Verbosity level for messages. @@ -431,14 +607,14 @@ function RANGE:New(rangename) -- @function [parent=#RANGE] Impact -- @param #RANGE self -- @param #RANGE.BombResult result Data of bombing run. - -- @param #RANGE.Playerdata player Data of player settings etc. + -- @param #RANGE.PlayerData player Data of player settings etc. --- Triggers the FSM delayed event "Impact". -- @function [parent=#RANGE] __Impact -- @param #RANGE self -- @param #number delay Delay in seconds before the function is called. -- @param #RANGE.BombResult result Data of the bombing run. - -- @param #RANGE.Playerdata player Data of player settings etc. + -- @param #RANGE.PlayerData player Data of player settings etc. --- On after "Impact" event user function. Called when a bomb/rocket/missile impacted. -- @function [parent=#RANGE] OnAfterImpact @@ -447,18 +623,18 @@ function RANGE:New(rangename) -- @param #string Event Event. -- @param #string To To state. -- @param #RANGE.BombResult result Data of the bombing run. - -- @param #RANGE.Playerdata player Data of player settings etc. + -- @param #RANGE.PlayerData player Data of player settings etc. --- Triggers the FSM event "EnterRange". -- @function [parent=#RANGE] EnterRange -- @param #RANGE self - -- @param #RANGE.Playerdata player Data of player settings etc. + -- @param #RANGE.PlayerData player Data of player settings etc. --- Triggers the FSM delayed event "EnterRange". -- @function [parent=#RANGE] __EnterRange -- @param #RANGE self -- @param #number delay Delay in seconds before the function is called. - -- @param #RANGE.Playerdata player Data of player settings etc. + -- @param #RANGE.PlayerData player Data of player settings etc. --- On after "EnterRange" event user function. Called when a player enters the range zone. -- @function [parent=#RANGE] OnAfterEnterRange @@ -466,18 +642,18 @@ function RANGE:New(rangename) -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. - -- @param #RANGE.Playerdata player Data of player settings etc. + -- @param #RANGE.PlayerData player Data of player settings etc. --- Triggers the FSM event "ExitRange". -- @function [parent=#RANGE] ExitRange -- @param #RANGE self - -- @param #RANGE.Playerdata player Data of player settings etc. + -- @param #RANGE.PlayerData player Data of player settings etc. --- Triggers the FSM delayed event "ExitRange". -- @function [parent=#RANGE] __ExitRange -- @param #RANGE self -- @param #number delay Delay in seconds before the function is called. - -- @param #RANGE.Playerdata player Data of player settings etc. + -- @param #RANGE.PlayerData player Data of player settings etc. --- On after "ExitRange" event user function. Called when a player leaves the range zone. -- @function [parent=#RANGE] OnAfterExitRange @@ -485,7 +661,7 @@ function RANGE:New(rangename) -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. - -- @param #RANGE.Playerdata player Data of player settings etc. + -- @param #RANGE.PlayerData player Data of player settings etc. -- Return object. return self @@ -573,6 +749,63 @@ function RANGE:onafterStart() end end + + -- Init range control. + if self.rangecontrolfreq then + + -- Radio queue. + self.rangecontrol=RADIOQUEUE:New(self.rangecontrolfreq) + + -- Init numbers. + self.rangecontrol:SetDigit(0, RANGE.Sound.RC0.filename, RANGE.Sound.RC0.duration, self.soundpath) + self.rangecontrol:SetDigit(1, RANGE.Sound.RC1.filename, RANGE.Sound.RC1.duration, self.soundpath) + self.rangecontrol:SetDigit(2, RANGE.Sound.RC2.filename, RANGE.Sound.RC2.duration, self.soundpath) + self.rangecontrol:SetDigit(3, RANGE.Sound.RC3.filename, RANGE.Sound.RC3.duration, self.soundpath) + self.rangecontrol:SetDigit(4, RANGE.Sound.RC4.filename, RANGE.Sound.RC4.duration, self.soundpath) + self.rangecontrol:SetDigit(5, RANGE.Sound.RC5.filename, RANGE.Sound.RC5.duration, self.soundpath) + self.rangecontrol:SetDigit(6, RANGE.Sound.RC6.filename, RANGE.Sound.RC6.duration, self.soundpath) + self.rangecontrol:SetDigit(7, RANGE.Sound.RC7.filename, RANGE.Sound.RC7.duration, self.soundpath) + self.rangecontrol:SetDigit(8, RANGE.Sound.RC8.filename, RANGE.Sound.RC8.duration, self.soundpath) + self.rangecontrol:SetDigit(9, RANGE.Sound.RC9.filename, RANGE.Sound.RC9.duration, self.soundpath) + + -- Set location where the messages are transmitted from. + self.rangecontrol:SetSenderCoordinate(self.location) + + -- Start range control radio queue. + self.rangecontrol:Start(1, 0.1) + + -- Init range control. + if self.instructorfreq then + + -- Radio queue. + self.instructor=RADIOQUEUE:New(self.instructorfreq) + + -- Init numbers. + self.instructor:SetDigit(0, RANGE.Sound.IR0.filename, RANGE.Sound.IR0.duration, self.soundpath) + self.instructor:SetDigit(1, RANGE.Sound.IR1.filename, RANGE.Sound.IR1.duration, self.soundpath) + self.instructor:SetDigit(2, RANGE.Sound.IR2.filename, RANGE.Sound.IR2.duration, self.soundpath) + self.instructor:SetDigit(3, RANGE.Sound.IR3.filename, RANGE.Sound.IR3.duration, self.soundpath) + self.instructor:SetDigit(4, RANGE.Sound.IR4.filename, RANGE.Sound.IR4.duration, self.soundpath) + self.instructor:SetDigit(5, RANGE.Sound.IR5.filename, RANGE.Sound.IR5.duration, self.soundpath) + self.instructor:SetDigit(6, RANGE.Sound.IR6.filename, RANGE.Sound.IR6.duration, self.soundpath) + self.instructor:SetDigit(7, RANGE.Sound.IR7.filename, RANGE.Sound.IR7.duration, self.soundpath) + self.instructor:SetDigit(8, RANGE.Sound.IR8.filename, RANGE.Sound.IR8.duration, self.soundpath) + self.instructor:SetDigit(9, RANGE.Sound.IR9.filename, RANGE.Sound.IR9.duration, self.soundpath) + + -- Set location where the messages are transmitted from. + self.instructor:SetSenderCoordinate(self.location) + + -- Start instructor radio queue. + self.instructor:Start(1, 0.1) + + end + + end + + -- Load prev results. + if self.autosave then + self:Load() + end -- Debug mode: smoke all targets and range zone. if self.Debug then @@ -831,6 +1064,34 @@ function RANGE:TrackMissilesOFF() end +--- Enable range control and set frequency. +-- @param #RANGE self +-- @param #number frequency Frequency in MHz. Default 256 MHz. +-- @return #RANGE self +function RANGE:SetRangeControl(frequency) + self.rangecontrolfreq=frequency or 256 + return self +end + +--- Enable instructor radio and set frequency. +-- @param #RANGE self +-- @param #number frequency Frequency in MHz. Default 305 MHz. +-- @return #RANGE self +function RANGE:SetInstructorRadio(frequency) + self.instructorfreq=frequency or 305 + return self +end + +--- Set sound files folder within miz file. +-- @param #RANGE self +-- @param #string path Path for sound files. Default "ATIS Soundfiles/". Mind the slash "/" at the end! +-- @return #RANGE self +function RANGE:SetSoundfilesPath(path) + self.soundpath=tostring(path or "Range Soundfiles/") + self:I(self.lid..string.format("Setting sound files path to %s", self.soundpath)) + return self +end + --- Add new strafe pit. For a strafe pit, hits from guns are counted. One pit can consist of several units. -- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading. -- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach. @@ -1565,7 +1826,7 @@ function RANGE:OnEventShot(EventData) if _distance == nil or _temp < _distance then _distance = _temp _closetTarget = _bombtarget - _closeCoord=targetcoord + _closeCoord=targetcoord if _distance <= 0.5*_bombtarget.goodhitrange then _hitquality = "EXCELLENT" elseif _distance <= _bombtarget.goodhitrange then @@ -1609,8 +1870,12 @@ 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) + local _message=string.format("%s, weapon impacted too far from nearest range target (>%.1f km). No score!", _callsign, self.scorebombdistance/1000) self:_DisplayMessageToGroup(_unit, _message, nil, false) + + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCWeaponImpactedTooFar.filename, RANGE.Sound.RCWeaponImpactedTooFar.duration, self.soundpath, nil, nil, _message, self.subduration) + end else self:T(self.id.."Weapon impacted outside range zone.") @@ -1649,15 +1914,49 @@ function RANGE:onafterStatus(From, Event, To) -- Check player status. self:_CheckPlayers() - -- Save results. - if self.autosave then - self:Save() - end - -- Check back in ~10 seconds. self:__Status(-10) end +--- Function called after player enters the range zone. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #RANGE.PlayerData player Player data. +function RANGE:onafterEnterRange(From, Event, To, player) + + if self.instructor and self.rangecontrol then + + -- Range control radio frequency split. + local RF=UTILS.Split(string.format("%.3f", self.rangecontrolfreq), ".") + + -- Radio message that player entered the range + self.instructor:NewTransmission(RANGE.Sound.IREnterRange.filename, RANGE.Sound.IREnterRange.duration, self.soundpath) + self.instructor:Number2Transmission(RF[1]) + if tonumber(RF[2])>0 then + self.instructor:NewTransmission(RANGE.Sound.IRDecimal.filename, RANGE.Sound.IRDecimal.duration, self.soundpath) + self.instructor:Number2Transmission(RF[2]) + end + self.instructor:NewTransmission(RANGE.Sound.IRMegaHertz.filename, RANGE.Sound.IRMegaHertz.duration, self.soundpath) + end + +end + +--- Function called after player leaves the range zone. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #RANGE.PlayerData player Player data. +function RANGE:onafterExitRange(From, Event, To, player) + + if self.instructor then + self.instructor:NewTransmission(RANGE.Sound.IRExitRange.filename, RANGE.Sound.IRExitRange.duration, self.soundpath) + end + +end + --- Function called after bomb impact on range. -- @param #RANGE self @@ -1665,7 +1964,7 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param #RANGE.BombResult result Result of bomb impact. --- @param #RANGE.PlayerData player +-- @param #RANGE.PlayerData player Player data table. function RANGE:onafterImpact(From, Event, To, result, player) -- Only display target name if there is more than one bomb target. @@ -1682,6 +1981,25 @@ function RANGE:onafterImpact(From, Event, To, result, player) text=text.."." end text=text..string.format(" %s hit.", result.quality) + + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCImpact.filename, RANGE.Sound.RCImpact.duration, self.soundpath, nil, nil, text, self.subduration) + self.rangecontrol:Number2Transmission(string.format("%03d", result.radial), nil, 0.1) + self.rangecontrol:NewTransmission(RANGE.Sound.RCDegrees.filename, RANGE.Sound.RCDegrees.duration, self.soundpath) + self.rangecontrol:NewTransmission(RANGE.Sound.RCFor.filename, RANGE.Sound.RCFor.duration, self.soundpath) + self.rangecontrol:Number2Transmission(string.format("%d", UTILS.MetersToFeet(result.distance))) + self.rangecontrol:NewTransmission(RANGE.Sound.RCFeet.filename, RANGE.Sound.RCFeet.duration, self.soundpath) + if result.quality=="POOR" then + self.rangecontrol:NewTransmission(RANGE.Sound.RCPoorHit.filename, RANGE.Sound.RCPoorHit.duration, self.soundpath, nil, 0.5) + elseif result.quality=="INEFFECTIVE" then + self.rangecontrol:NewTransmission(RANGE.Sound.RCIneffectiveHit.filename, RANGE.Sound.RCIneffectiveHit.duration, self.soundpath, nil, 0.5) + elseif result.quality=="GOOD" then + self.rangecontrol:NewTransmission(RANGE.Sound.RCGoodHit.filename, RANGE.Sound.RCGoodHit.duration, self.soundpath, nil, 0.5) + elseif result.quality=="EXCELLENT" then + self.rangecontrol:NewTransmission(RANGE.Sound.RCExcellentHit.filename, RANGE.Sound.RCExcellentHit.duration, self.soundpath, nil, 0.5) + end + + end -- Unit. local unit=UNIT:FindByName(player.unitname) @@ -1689,6 +2007,11 @@ function RANGE:onafterImpact(From, Event, To, result, player) -- Send message. self:_DisplayMessageToGroup(unit, text, nil, true) self:T(self.id..text) + + -- Save results. + if self.autosave then + self:Save() + end end @@ -1746,7 +2069,11 @@ function RANGE:onafterSave(From, Event, To) local quality=result.quality local time=UTILS.SecondsToClock(result.time) local airframe=result.airframe - scores=scores..string.format("\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time) + local date="n/a" + if os then + date=os.date() + end + scores=scores..string.format("\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time, date) end end @@ -1783,7 +2110,7 @@ function RANGE:onafterLoad(From, Event, To) f:close() return data else - self:E(self.id..string.format("ERROR: Could not load player results from file %s", tostring(filename))) + self:E(self.id..string.format("WARNING: Could not load player results from file %s. File might not exist just yet.", tostring(filename))) return nil end end @@ -1833,6 +2160,7 @@ function RANGE:onafterLoad(From, Event, To) result.weapon=tostring(resultdata[7]) result.airframe=tostring(resultdata[8]) result.time=UTILS.ClockToSeconds(resultdata[9] or "00:00:00") + result.date=resultdata[10] or "n/a" -- Create player array if necessary. self.bombPlayerResults[playername]=self.bombPlayerResults[playername] or {} @@ -2106,6 +2434,7 @@ function RANGE:_DisplayRangeInfo(_unitname) local position=self.location --Core.Point#COORDINATE local bulls=position:ToStringBULLS(unit:GetCoalition(), settings) local lldms=position:ToStringLLDMS(settings) + local llddm=position:ToStringLLDDM(settings) local rangealt=position:GetLandHeight() local vec3=coord:GetDirectionVec3(position) local angle=coord:GetAngleDegrees(vec3) @@ -2149,6 +2478,7 @@ function RANGE:_DisplayRangeInfo(_unitname) text=text..string.format("Bearing %s, Range %s\n", Bs, trange) text=text..string.format("%s\n", bulls) text=text..string.format("%s\n", lldms) + text=text..string.format("%s\n", llddm) text=text..string.format("Altitude ASL: %s\n", trangealt) text=text..string.format("Max strafing alt AGL: %s\n", tstrafemaxalt) text=text..string.format("# of strafe targets: %d\n", self.nstrafetargets) @@ -2192,13 +2522,13 @@ function RANGE:_DisplayBombTargets(_unitname) if coord then - local ca2g=coord:ToStringA2G(_unit, _settings) - local lldms=coord:ToStringLLDMS() - _text=_text..string.format("\n- %s: %s %s", bombtarget.name or "unknown", ca2g, lldms) + local ca2g=coord:ToStringA2G(_unit,_settings) + --local lldms=coord:ToStringLLDMS(_settings) + _text=_text..string.format("\n- %s:\n%s", bombtarget.name or "unknown", ca2g) end end - self:_DisplayMessageToGroup(_unit,_text, nil, true, true) + self:_DisplayMessageToGroup(_unit,_text, 60, true, true) end end @@ -2235,7 +2565,7 @@ function RANGE:_DisplayStrafePits(_unitname) end local mycoord=coord:ToStringA2G(_unit, _settings) - _text=_text..string.format("\n- %s: %s - heading %03d°",_strafepit.name, mycoord, heading) + _text=_text..string.format("\n- %s: heading %03d°\n%s",_strafepit.name, heading, mycoord) end self:_DisplayMessageToGroup(_unit,_text, nil, true, true) @@ -2283,7 +2613,7 @@ function RANGE:_DisplayRangeWeather(_unitname) 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)) + --tT=string.format("%d°F", UTILS.CelciusToFarenheit(T)) tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) tP=string.format("%.2f inHg", P*hPa2inHg) end @@ -2332,8 +2662,8 @@ function RANGE:_CheckPlayers() ------------------------------ if not playersettings.inzone then - self:EnterRange(playersettings) playersettings.inzone=true + self:EnterRange(playersettings) end else @@ -2343,8 +2673,8 @@ function RANGE:_CheckPlayers() ------------------------------- if playersettings.inzone==true then - self:ExitRange(playersettings) playersettings.inzone=false + self:ExitRange(playersettings) end end @@ -2409,6 +2739,10 @@ function RANGE:_CheckInZone(_unitName) -- Send message. self:_DisplayMessageToGroup(_unit, _msg, nil, true) + + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCLeftStrafePitTooQuickly.filename, RANGE.Sound.RCLeftStrafePitTooQuickly.duration, self.soundpath) + end else @@ -2417,16 +2751,21 @@ function RANGE:_CheckInZone(_unitName) -- Result. local _result = self.strafeStatus[_unitID] + local _sound = nil --#RANGE.Soundfile -- Judge this pass. Text is displayed on summary. if _result.hits >= _result.zone.goodPass*2 then _result.text = "EXCELLENT PASS" + _sound=RANGE.Sound.RCExcellentPass elseif _result.hits >= _result.zone.goodPass then _result.text = "GOOD PASS" + _sound=RANGE.Sound.RCGoodPass elseif _result.hits >= _result.zone.goodPass/2 then _result.text = "INEFFECTIVE PASS" + _sound=RANGE.Sound.RCIneffectivePass else _result.text = "POOR PASS" + _sound=RANGE.Sound.RCPoorPass end -- Calculate accuracy of run. Number of hits wrt number of rounds fired. @@ -2437,13 +2776,28 @@ function RANGE:_CheckInZone(_unitName) end -- Message text. - local _text=string.format("%s, %s with %d hits on target %s.", self:_myname(_unitName), _result.text, _result.hits, _result.zone.name) + local _text=string.format("%s, hits on target %s: %d", self:_myname(_unitName), _result.zone.name, _result.hits) if shots and accur then _text=_text..string.format("\nTotal rounds fired %d. Accuracy %.1f %%.", shots, accur) end + _text=_text..string.format("\n%s", _result.text) -- Send message. self:_DisplayMessageToGroup(_unit, _text) + + -- Voice over. + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCHitsOnTarget.filename, RANGE.Sound.RCHitsOnTarget.duration, self.soundpath) + self.rangecontrol:Number2Transmission(string.format("%d", _result.hits)) + if shots and accur then + self.rangecontrol:NewTransmission(RANGE.Sound.RCTotalRoundsFired.filename, RANGE.Sound.RCTotalRoundsFired.duration, self.soundpath, nil, 0.2) + self.rangecontrol:Number2Transmission(string.format("%d", shots), nil, 0.2) + self.rangecontrol:NewTransmission(RANGE.Sound.RCAccuracy.filename, RANGE.Sound.RCAccuracy.duration, self.soundpath, nil, 0.2) + self.rangecontrol:Number2Transmission(string.format("%d", UTILS.Round(accur, 0))) + self.rangecontrol:NewTransmission(RANGE.Sound.RCPercent.filename, RANGE.Sound.RCPercent.duration, self.soundpath) + end + self.rangecontrol:NewTransmission(_sound.filename, _sound.duration, self.soundpath, nil, 0.5) + end -- Set strafe status to nil. self.strafeStatus[_unitID] = nil @@ -2490,6 +2844,10 @@ function RANGE:_CheckInZone(_unitName) -- Rolling in! local _msg=string.format("%s, rolling in on strafe pit %s.", self:_myname(_unitName), _targetZone.name) + + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCRollingInOnStrafeTarget.filename, RANGE.Sound.RCRollingInOnStrafeTarget.duration, self.soundpath) + end -- Send message. self:_DisplayMessageToGroup(_unit, _msg, 10, true) @@ -2996,7 +3354,7 @@ function RANGE:_SmokeStrafeTargetBoxes(unitname) for _,_target in pairs(self.strafeTargets) do local zone=_target.polygon --Core.Zone#ZONE - zone:SmokeZone(self.StrafePitSmokeColor) + zone:SmokeZone(self.StrafePitSmokeColor, 4) for _,_point in pairs(_target.smokepoints) do _point:SmokeOrange() --Corners are smoked orange. end @@ -3189,7 +3547,8 @@ function RANGE:_myname(unitname) local pname=unit:GetPlayerName() local csign=unit:GetCallsign() - return string.format("%s (%s)", csign, pname) + --return string.format("%s (%s)", csign, pname) + return string.format("%s", pname) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- From 14de37f3907c5786cf233c52c1f747ca9143aa9d Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 23 Nov 2019 20:36:45 +0100 Subject: [PATCH 391/485] AIRBOSS & BASE BASE: - Fixed bug that tracing is always ON AIRBOSS v1.1.0 - Added support for P-3C Orion and C-2A Greyhound (AI) from MAM --- Moose Development/Moose/Core/Base.lua | 14 ++++++++++++-- Moose Development/Moose/Ops/Airboss.lua | 12 +++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index c042904a1..307047859 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -925,7 +925,13 @@ end -- -- Switch the tracing Off -- BASE:TraceOnOff( false ) function BASE:TraceOnOff( TraceOnOff ) - _TraceOnOff = TraceOnOff or true + if TraceOnOff==false then + self:I( "Tracing in MOOSE is OFF" ) + _TraceOnOff = false + else + self:I( "Tracing in MOOSE is ON" ) + _TraceOnOff = true + end end @@ -954,7 +960,11 @@ end -- @param #boolean TraceAll true = trace all methods in MOOSE. function BASE:TraceAll( TraceAll ) - _TraceAll = TraceAll or true + if TraceAll==false then + _TraceAll=false + else + _TraceAll = true + end if _TraceAll then self:I( "Tracing all methods in MOOSE " ) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 5a3c3a6e5..a583bbc2e 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -39,6 +39,8 @@ -- * F-14A Tomcat (AI) -- * E-2D Hawkeye (AI) -- * S-3B Viking & tanker version (AI) +-- * [C-2A Greyhound](https://forums.eagle.ru/showthread.php?t=255641) (AI) +-- * [P-3C Orion](https://forums.eagle.ru/showthread.php?t=255641) (AI) -- -- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) and A-4E community mod as aircraft and the USS John C. Stennis as carrier. -- @@ -1255,6 +1257,8 @@ AIRBOSS = { -- @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 C2A Grumman C-2A Greyhound from Military Aircraft Mod. +-- @field #string P3C Lockheed P-3C Orion from Military Aircraft Mod. AIRBOSS.AircraftCarrier={ AV8B="AV8BNA", HORNET="FA-18C_hornet", @@ -1266,6 +1270,8 @@ AIRBOSS.AircraftCarrier={ S3B="S-3B", S3BTANKER="S-3B Tanker", E2D="E-2C", + C2A="C2A_Greyhound", + P3C="P3C_Orion" } --- Carrier types. @@ -1688,7 +1694,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.0.9" +AIRBOSS.version="1.1.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -15544,7 +15550,7 @@ end function AIRBOSS:_MarshalCallRecoveryStart(case) -- Marshal radial. - local radial=self:GetRadial(case, true, true, true) + local radial=self:GetRadial(case, true, true, false) -- Debug output. local text=string.format("Starting aircraft recovery Case %d ops.", case) @@ -17013,7 +17019,7 @@ function AIRBOSS:_DisplayCarrierInfo(_unitname) if self.case==1 then text=text..string.format("Case %d recovery ops\n", self.case) else - local radial=self:GetRadial(self.case, true, true, true) + local radial=self:GetRadial(self.case, true, true, false) text=text..string.format("Case %d recovery ops\nMarshal radial %03d°\n", self.case, radial) end text=text..string.format("BRC %03d° - FB %03d°\n", self:GetBRC(), self:GetFinalBearing(true)) From 60042e14dc4a54c128a11879e8551f11e864724d Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 23 Nov 2019 21:02:02 +0100 Subject: [PATCH 392/485] Update Airboss.lua - Removed Orion --- Moose Development/Moose/Ops/Airboss.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index a583bbc2e..91bf8a91a 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -40,7 +40,6 @@ -- * E-2D Hawkeye (AI) -- * S-3B Viking & tanker version (AI) -- * [C-2A Greyhound](https://forums.eagle.ru/showthread.php?t=255641) (AI) --- * [P-3C Orion](https://forums.eagle.ru/showthread.php?t=255641) (AI) -- -- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) and A-4E community mod as aircraft and the USS John C. Stennis as carrier. -- @@ -1258,7 +1257,6 @@ AIRBOSS = { -- @field #string S3BTANKER Lockheed S-3B Viking tanker. -- @field #string E2D Grumman E-2D Hawkeye AWACS. -- @field #string C2A Grumman C-2A Greyhound from Military Aircraft Mod. --- @field #string P3C Lockheed P-3C Orion from Military Aircraft Mod. AIRBOSS.AircraftCarrier={ AV8B="AV8BNA", HORNET="FA-18C_hornet", @@ -1271,7 +1269,6 @@ AIRBOSS.AircraftCarrier={ S3BTANKER="S-3B Tanker", E2D="E-2C", C2A="C2A_Greyhound", - P3C="P3C_Orion" } --- Carrier types. From 169c5a674c4c3a66ce9541d2a67b26ad73e00d76 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 25 Nov 2019 11:50:07 +0100 Subject: [PATCH 393/485] Fixes AI_PATROL: - Target unit is nil. Issue #1234 DETECTION: - ReportFriendliesNearBy ForEachPlayer added nil check. CONTROLLABLE: - Added IsHelicopter() - Added OptionRestrictBurner() AI_A2A_Cap - AtteckUnit not nil check. AI_A2A_Dispatcher: - DefenderGroup not nil and alive check #1228 AI_A2A_GCICAP: - AttackCoordinate nil check --- Moose Development/Moose/AI/AI_A2A_Cap.lua | 3 +- .../Moose/AI/AI_A2A_Dispatcher.lua | 132 ++++++++++-------- Moose Development/Moose/AI/AI_Patrol.lua | 26 ++-- .../Moose/Functional/Detection.lua | 42 +++--- .../Moose/Wrapper/Controllable.lua | 45 ++++++ 5 files changed, 160 insertions(+), 88 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Cap.lua b/Moose Development/Moose/AI/AI_A2A_Cap.lua index d44d9763c..ae4171ae5 100644 --- a/Moose Development/Moose/AI/AI_A2A_Cap.lua +++ b/Moose Development/Moose/AI/AI_A2A_Cap.lua @@ -200,8 +200,7 @@ function AI_A2A_CAP:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageA 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 and AttackUnit:IsAlive() and AttackUnit:IsAir() then -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition() -- Maybe the detected set also contains self:T( { "Attacking Task:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index d25dc2ce9..26da1d54c 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -3231,65 +3231,73 @@ do -- AI_A2A_DISPATCHER self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", AI_A2A_Fsm ) function AI_A2A_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) - self:F({"CAP Takeoff", DefenderGroup:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - - local DefenderName = DefenderGroup:GetCallsign() - local Dispatcher = AI_A2A_Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - - if Squadron then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. " Wheels up.", DefenderGroup ) - AI_A2A_Fsm:__Patrol( 2 ) -- Start Patrolling + -- Issue GetCallsign() returns nil, see https://github.com/FlightControl-Master/MOOSE/issues/1228 + if DefenderGroup and DefenderGroup:IsAlive() then + self:F({"CAP Takeoff", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2A_Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " Wheels up.", DefenderGroup ) + AI_A2A_Fsm:__Patrol( 2 ) -- Start Patrolling + end end end function AI_A2A_Fsm:onafterPatrolRoute( DefenderGroup, From, Event, To ) - self:F({"CAP PatrolRoute", DefenderGroup:GetName()}) - self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To ) - - local DefenderName = DefenderGroup:GetCallsign() - local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - if Squadron then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup ) + if DefenderGroup and DefenderGroup:IsAlive() then + self:F({"CAP PatrolRoute", DefenderGroup:GetName()}) + self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if Squadron then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup ) + end + + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end - - Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end function AI_A2A_Fsm:onafterRTB( DefenderGroup, From, Event, To ) - - self:F({"CAP RTB", DefenderGroup:GetName()}) - - self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) - - local DefenderName = DefenderGroup:GetCallsign() - local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - if Squadron then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) + if DefenderGroup and DefenderGroup:IsAlive() then + self:F({"CAP RTB", DefenderGroup:GetName()}) + + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if Squadron then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) + end + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end - Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end --- @param #AI_A2A_DISPATCHER self function AI_A2A_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_A2A_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_A2A_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - Dispatcher:ParkDefender( Squadron ) + if Defender and Defender:IsAlive() then + self:F({"CAP Home", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local Dispatcher = self:GetDispatcher() -- #AI_A2A_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_A2A_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + Dispatcher:ParkDefender( Squadron ) + end end end end @@ -3636,22 +3644,31 @@ do -- AI_A2A_DISPATCHER --- Assigns A2G AI Tasks in relation to the detected items. -- @param #AI_A2G_DISPATCHER self function AI_A2A_DISPATCHER:Order( DetectedItem ) - local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local detection=self.Detection -- Functional.Detection#DETECTION_AREAS local ShortestDistance = 999999999 + + -- Get coordinate (or nil). + local AttackCoordinate = detection:GetDetectedItemCoordinate( DetectedItem ) + + -- Issue https://github.com/FlightControl-Master/MOOSE/issues/1232 + if AttackCoordinate then - for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do - - self:I( { DefenderSquadron = DefenderSquadron.Name } ) - - local Airbase = DefenderSquadron.Airbase - local AirbaseCoordinate = Airbase:GetCoordinate() - - local EvaluateDistance = AttackCoordinate:Get2DDistance( AirbaseCoordinate ) + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do - if EvaluateDistance <= ShortestDistance then - ShortestDistance = EvaluateDistance + self:I( { DefenderSquadron = DefenderSquadron.Name } ) + + local Airbase = DefenderSquadron.Airbase + local AirbaseCoordinate = Airbase:GetCoordinate() + + local EvaluateDistance = AttackCoordinate:Get2DDistance( AirbaseCoordinate ) + + if EvaluateDistance <= ShortestDistance then + ShortestDistance = EvaluateDistance + end end + end return ShortestDistance @@ -3660,6 +3677,7 @@ do -- AI_A2A_DISPATCHER --- Shows the tactical display. -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. function AI_A2A_DISPATCHER:ShowTacticalDisplay( Detection ) local AreaMsg = {} diff --git a/Moose Development/Moose/AI/AI_Patrol.lua b/Moose Development/Moose/AI/AI_Patrol.lua index 6e2b6ff67..031d9aae8 100644 --- a/Moose Development/Moose/AI/AI_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Patrol.lua @@ -667,21 +667,27 @@ function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) if TargetObject and TargetObject:isExist() and TargetObject.id_ < 50000000 then local TargetUnit = UNIT:Find( TargetObject ) - local TargetUnitName = TargetUnit:GetName() - if self.DetectionZone then - if TargetUnit:IsInZone( self.DetectionZone ) then - self:T( {"Detected ", TargetUnit } ) + -- Check that target is alive due to issue https://github.com/FlightControl-Master/MOOSE/issues/1234 + if TargetUnit and TargetUnit:IsAlive() then + + local TargetUnitName = TargetUnit:GetName() + + if self.DetectionZone then + if TargetUnit:IsInZone( self.DetectionZone ) then + self:T( {"Detected ", TargetUnit } ) + if self.DetectedUnits[TargetUnit] == nil then + self.DetectedUnits[TargetUnit] = true + end + Detected = true + end + else if self.DetectedUnits[TargetUnit] == nil then self.DetectedUnits[TargetUnit] = true end - Detected = true + Detected = true end - else - if self.DetectedUnits[TargetUnit] == nil then - self.DetectedUnits[TargetUnit] = true - end - Detected = true + end end end diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 2c9c5d792..eb9b2e982 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1438,27 +1438,31 @@ do -- DETECTION_BASE function( PlayerUnitName ) local PlayerUnit = UNIT:FindByName( PlayerUnitName ) - if PlayerUnit and PlayerUnit:GetCoordinate():IsInRadius( DetectedUnitCoord, self.FriendliesRange ) then - --if PlayerUnit and PlayerUnit:IsInZone(DetectionZone) then + -- Fix for issue https://github.com/FlightControl-Master/MOOSE/issues/1225 + if PlayerUnit and PlayerUnit:IsAlive() then + local coord=PlayerUnit:GetCoordinate() + + if coord and coord:IsInRadius( DetectedUnitCoord, self.FriendliesRange ) then - local PlayerUnitCategory = PlayerUnit:GetDesc().category - - if ( not self.FriendliesCategory ) or ( self.FriendliesCategory and ( self.FriendliesCategory == PlayerUnitCategory ) ) then - - local PlayerUnitName = PlayerUnit:GetName() + local PlayerUnitCategory = PlayerUnit:GetDesc().category - DetectedItem.PlayersNearBy = DetectedItem.PlayersNearBy or {} - DetectedItem.PlayersNearBy[PlayerUnitName] = PlayerUnit - - -- Friendlies are sorted per unit category. - DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {} - DetectedItem.FriendliesNearBy[PlayerUnitCategory] = DetectedItem.FriendliesNearBy[PlayerUnitCategory] or {} - DetectedItem.FriendliesNearBy[PlayerUnitCategory][PlayerUnitName] = PlayerUnit - - local Distance = DetectedUnitCoord:Get2DDistance( PlayerUnit:GetCoordinate() ) - DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {} - DetectedItem.FriendliesDistance[Distance] = PlayerUnit - + if ( not self.FriendliesCategory ) or ( self.FriendliesCategory and ( self.FriendliesCategory == PlayerUnitCategory ) ) then + + local PlayerUnitName = PlayerUnit:GetName() + + DetectedItem.PlayersNearBy = DetectedItem.PlayersNearBy or {} + DetectedItem.PlayersNearBy[PlayerUnitName] = PlayerUnit + + -- Friendlies are sorted per unit category. + DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {} + DetectedItem.FriendliesNearBy[PlayerUnitCategory] = DetectedItem.FriendliesNearBy[PlayerUnitCategory] or {} + DetectedItem.FriendliesNearBy[PlayerUnitCategory][PlayerUnitName] = PlayerUnit + + local Distance = DetectedUnitCoord:Get2DDistance( PlayerUnit:GetCoordinate() ) + DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {} + DetectedItem.FriendliesDistance[Distance] = PlayerUnit + + end end end end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index b5d773b98..b42085516 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -3480,3 +3480,48 @@ function CONTROLLABLE:IsAirPlane() return nil end + +--- Returns if the Controllable contains Helicopters. +-- @param #CONTROLLABLE self +-- @return #boolean true if Controllable contains Helicopters. +function CONTROLLABLE:IsHelicopter() + self:F2() + + local DCSObject = self:GetDCSObject() + + if DCSObject then + local Category = DCSObject:getDesc().category + return Category == Unit.Category.HELICOPTER + end + + return nil +end + +--- Sets Controllable Option for Restriction of Afterburner. +-- @param #CONTROLLABLE self +-- @param #boolean RestrictBurner If true, restrict burner. If false or nil, allow (unrestrict) burner. +function CONTROLLABLE:OptionRestrictBurner(RestrictBurner) + self:F2({self.ControllableName}) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local Controller = self:_GetController() + + if Controller then + + -- Issue https://github.com/FlightControl-Master/MOOSE/issues/1216 + if RestrictBurner == true then + if self:IsAir() then + Controller:setOption(16, true) + end + else + if self:IsAir() then + Controller:setOption(16, false) + end + end + + end + end + +end From e16e2851c1dc0f6373cfdabf5f42993594bf34e4 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 25 Nov 2019 11:58:29 +0100 Subject: [PATCH 394/485] ARTY v1.1.6 - Check that arty unit is not dead when addressed via marker --- Moose Development/Moose/Functional/Artillery.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 87d4d0a69..d1742c271 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -693,7 +693,7 @@ ARTY.db={ --- Arty script version. -- @field #string version -ARTY.version="1.1.5" +ARTY.version="1.1.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2555,6 +2555,13 @@ function ARTY:_OnEventMarkChange(Event) if not _assigned then self:T3(self.lid..string.format("INFO: ARTY group %s was not addressed! Mark text:\n%s", self.groupname, Event.text)) return + else + if self.Controllable and self.Controllable:IsAlive() then + + else + self:T3(self.lid..string.format("INFO: ARTY group %s was addressed but is NOT alive! Mark text:\n%s", self.groupname, Event.text)) + return + end end -- Coordinate was given in text, e.g. as lat, long. From e4473010ebe70c3168dc85bbe8468aafaed57c7f Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 2 Dec 2019 16:33:03 +0100 Subject: [PATCH 395/485] Docs HelicopterUsable --- Moose Development/Moose/Functional/RAT.lua | 2 +- Moose Development/Moose/Wrapper/Airbase.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 322c3ea86..21af54d2e 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -242,7 +242,7 @@ -- * AIRBASE.TerminalType.OpenMed: Open/Shelter air airplane only. -- * AIRBASE.TerminalType.OpenBig: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there. -- * AIRBASE.TerminalType.OpenMedOrBig: Combines OpenMed and OpenBig spots. --- * AIRBASE.TerminalType.HelicopterUnsable: Combines HelicopterOnly, OpenMed and OpenBig. +-- * AIRBASE.TerminalType.HelicopterUsable: Combines HelicopterOnly, OpenMed and OpenBig. -- * AIRBASE.TerminalType.FighterAircraft: Combines Shelter, OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. -- -- So for example diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index e960bf8be..e3effd07c 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -305,7 +305,7 @@ AIRBASE.PersianGulf = { -- * AIRBASE.TerminalType.OpenMed = 72: Open/Shelter air airplane only. -- * AIRBASE.TerminalType.OpenBig = 104: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there. -- * AIRBASE.TerminalType.OpenMedOrBig = 176: Combines OpenMed and OpenBig spots. --- * AIRBASE.TerminalType.HelicopterUnsable = 216: Combines HelicopterOnly, OpenMed and OpenBig. +-- * AIRBASE.TerminalType.HelicopterUsable = 216: Combines HelicopterOnly, OpenMed and OpenBig. -- * AIRBASE.TerminalType.FighterAircraft = 244: Combines Shelter. OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. -- -- @type AIRBASE.TerminalType From 4b6612e11418466aebbc796af88d3b561ea6f3d0 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 4 Dec 2019 22:17:51 +0100 Subject: [PATCH 396/485] Cargo fixes --- Moose Development/Moose/AI/AI_Cargo.lua | 17 +++- .../Moose/AI/AI_Cargo_Airplane.lua | 88 ++++++++++++------- 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Cargo.lua b/Moose Development/Moose/AI/AI_Cargo.lua index ed76eca7b..586c0ab51 100644 --- a/Moose Development/Moose/AI/AI_Cargo.lua +++ b/Moose Development/Moose/AI/AI_Cargo.lua @@ -141,6 +141,17 @@ function AI_CARGO:New( Carrier, CargoSet ) -- @param #string From -- @param #string Event -- @param #string To + + --- On after Deployed event. + -- @function [parent=#AI_CARGO] OnAfterDeployed + -- @param #AI_CARGO self + -- @param Wrapper.Group#GROUP Carrier + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + -- @param #boolean Defend Defend for APCs. + for _, CarrierUnit in pairs( Carrier:GetUnits() ) do local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT @@ -256,9 +267,10 @@ function AI_CARGO:onbeforeLoad( Carrier, From, Event, To, PickupZone ) self:F( { "In radius", CarrierUnit:GetName() } ) local CargoWeight = Cargo:GetWeight() + local CarrierSpace=Carrier_Weight[CarrierUnit] -- Only when there is space within the bay to load the next cargo item! - if Carrier_Weight[CarrierUnit] > CargoWeight then --and CargoBayFreeVolume > CargoVolume then + if CarrierSpace > CargoWeight then Carrier:RouteStop() --Cargo:Ungroup() Cargo:__Board( -LoadDelay, CarrierUnit ) @@ -275,6 +287,8 @@ function AI_CARGO:onbeforeLoad( Carrier, From, Event, To, PickupZone ) -- Ok, we loaded a cargo, now we can stop the loop. break + else + self:T(string.format("WARNING: Cargo too heavy for carrier %s. Cargo=%.1f > %.1f free space", tostring(CarrierUnit:GetName()), CargoWeight, CarrierSpace)) end end end @@ -554,6 +568,7 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- @param #boolean Defend Defend for APCs. function AI_CARGO:onafterDeployed( Carrier, From, Event, To, DeployZone, Defend ) self:F( { Carrier, From, Event, To, DeployZone = DeployZone, Defend = Defend } ) diff --git a/Moose Development/Moose/AI/AI_Cargo_Airplane.lua b/Moose Development/Moose/AI/AI_Cargo_Airplane.lua index 947d7ec5b..2b3277bde 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Airplane.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Airplane.lua @@ -76,25 +76,31 @@ function AI_CARGO_AIRPLANE:New( Airplane, CargoSet ) --- Pickup Handler OnAfter for AI_CARGO_AIRPLANE -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterPickup -- @param #AI_CARGO_AIRPLANE self - -- @param Wrapper.Group#GROUP Airplane Cargo plane. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where troops are picked up. - -- @param #number Speed in km/h for travelling to pickup base. + -- @param Wrapper.Group#GROUP Airplane Cargo transport plane. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff. + -- @param #number Speed Speed in km/h for travelling to pickup base. + -- @param #number Height Height in meters to move to the pickup coordinate. + -- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up. --- Pickup Trigger for AI_CARGO_AIRPLANE -- @function [parent=#AI_CARGO_AIRPLANE] Pickup -- @param #AI_CARGO_AIRPLANE self - -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where troops are picked up. - -- @param #number Speed in km/h for travelling to pickup base. + -- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff. + -- @param #number Speed Speed in km/h for travelling to pickup base. + -- @param #number Height Height in meters to move to the pickup coordinate. + -- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up. --- Pickup Asynchronous Trigger for AI_CARGO_AIRPLANE -- @function [parent=#AI_CARGO_AIRPLANE] __Pickup -- @param #AI_CARGO_AIRPLANE self -- @param #number Delay Delay in seconds. - -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where troops are picked up. - -- @param #number Speed in km/h for travelling to pickup base. + -- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff. + -- @param #number Speed Speed in km/h for travelling to pickup base. + -- @param #number Height Height in meters to move to the pickup coordinate. + -- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up. --- Deploy Handler OnBefore for AI_CARGO_AIRPLANE -- @function [parent=#AI_CARGO_AIRPLANE] OnBeforeDeploy @@ -111,24 +117,30 @@ function AI_CARGO_AIRPLANE:New( Airplane, CargoSet ) -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterDeploy -- @param #AI_CARGO_AIRPLANE self -- @param Wrapper.Group#GROUP Airplane Cargo plane. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase where troops are deployed. - -- @param #number Speed Speed in km/h for travelling to deploy base. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff. + -- @param #number Speed Speed in km/h for travelling to the deploy base. + -- @param #number Height Height in meters to move to the home coordinate. + -- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. --- Deploy Trigger for AI_CARGO_AIRPLANE -- @function [parent=#AI_CARGO_AIRPLANE] Deploy -- @param #AI_CARGO_AIRPLANE self - -- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase where troops are deployed. - -- @param #number Speed Speed in km/h for travelling to deploy base. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff. + -- @param #number Speed Speed in km/h for travelling to the deploy base. + -- @param #number Height Height in meters to move to the home coordinate. + -- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. --- Deploy Asynchronous Trigger for AI_CARGO_AIRPLANE -- @function [parent=#AI_CARGO_AIRPLANE] __Deploy -- @param #AI_CARGO_AIRPLANE self -- @param #number Delay Delay in seconds. - -- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase where troops are deployed. - -- @param #number Speed Speed in km/h for travelling to deploy base. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff. + -- @param #number Speed Speed in km/h for travelling to the deploy base. + -- @param #number Height Height in meters to move to the home coordinate. + -- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. --- On after Loaded event, i.e. triggered when the cargo is inside the carrier. -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterLoaded @@ -137,6 +149,16 @@ function AI_CARGO_AIRPLANE:New( Airplane, CargoSet ) -- @param From -- @param Event -- @param To + + + --- On after Deployed event. + -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterDeployed + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo plane. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. -- Set carrier. self:SetCarrier( Airplane ) @@ -259,15 +281,17 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Core.Point#COORDINATE Coordinate --- @param #number Speed in km/h for travelling to pickup base. +-- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff. +-- @param #number Speed Speed in km/h for travelling to pickup base. -- @param #number Height Height in meters to move to the pickup coordinate. --- @param Core.Zone#ZONE_AIRBASE (optional) PickupZone The zone where the cargo will be picked up. +-- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up. function AI_CARGO_AIRPLANE:onafterPickup( Airplane, From, Event, To, Coordinate, Speed, Height, PickupZone ) if Airplane and Airplane:IsAlive() then - self.PickupZone = PickupZone + local airbasepickup=Coordinate:GetClosestAirbase() + + self.PickupZone = PickupZone or ZONE_AIRBASE:New(airbasepickup:GetName()) -- Get closest airbase of current position. local ClosestAirbase, DistToAirbase=Airplane:GetCoordinate():GetClosestAirbase() @@ -280,11 +304,10 @@ function AI_CARGO_AIRPLANE:onafterPickup( Airplane, From, Event, To, Coordinate, end -- Set pickup airbase. - local Airbase = PickupZone:GetAirbase() + local Airbase = self.PickupZone:GetAirbase() -- Distance from closest to pickup airbase ==> we need to know if we are already at the pickup airbase. local Dist = Airbase:GetCoordinate():Get2DDistance(ClosestAirbase:GetCoordinate()) - --env.info("Distance closest to pickup airbase = "..Dist) if Airplane:InAir() or Dist>500 then @@ -305,7 +328,7 @@ function AI_CARGO_AIRPLANE:onafterPickup( Airplane, From, Event, To, Coordinate, end - self:GetParent( self, AI_CARGO_AIRPLANE ).onafterPickup( self, Airplane, From, Event, To, Coordinate, Speed, Height, PickupZone ) + self:GetParent( self, AI_CARGO_AIRPLANE ).onafterPickup( self, Airplane, From, Event, To, Coordinate, Speed, Height, self.PickupZone ) end @@ -318,15 +341,19 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Core.Point#COORDINATE Coordinate --- @param #number Speed in km/h for travelling to pickup base. +-- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff. +-- @param #number Speed Speed in km/h for travelling to the deploy base. -- @param #number Height Height in meters to move to the home coordinate. --- @param Core.Zone#ZONE_AIRBASE DeployZone The zone where the cargo will be deployed. +-- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. function AI_CARGO_AIRPLANE:onafterDeploy( Airplane, From, Event, To, Coordinate, Speed, Height, DeployZone ) if Airplane and Airplane:IsAlive()~=nil then - local Airbase = DeployZone:GetAirbase() + local Airbase = Coordinate:GetClosestAirbase() + + if DeployZone then + Airbase=DeployZone:GetAirbase() + end -- Activate uncontrolled airplane. if Airplane:IsAlive()==false then @@ -354,6 +381,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. +-- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. function AI_CARGO_AIRPLANE:onafterUnload( Airplane, From, Event, To, DeployZone ) local UnboardInterval = 10 From 1b5e9df5869ee90549fdbac6db1f2f4616dd82e5 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 8 Dec 2019 00:27:20 +0100 Subject: [PATCH 397/485] SCHEDULER fixes --- Moose Development/Moose/Core/Base.lua | 7 +- Moose Development/Moose/Core/Event.lua | 5 +- .../Moose/Core/ScheduleDispatcher.lua | 101 ++++++++++--- Moose Development/Moose/Core/Scheduler.lua | 136 ++++++++---------- Moose Development/Moose/Globals.lua | 2 +- 5 files changed, 149 insertions(+), 102 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index 307047859..d20f068ee 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -764,9 +764,7 @@ do -- Scheduling if not self.Scheduler then self.Scheduler = SCHEDULER:New( self ) end - - self.Scheduler.SchedulerObject = self.Scheduler - + local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( self, SchedulerFunction, @@ -804,9 +802,6 @@ do -- Scheduling self.Scheduler = SCHEDULER:New( self ) end - self.Scheduler.SchedulerObject = self.Scheduler - --self.MasterObject = self - local ScheduleID = self.Scheduler:Schedule( self, SchedulerFunction, diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 4a6b96448..4ef56c8ea 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -488,6 +488,9 @@ local _EVENTMETA = { -- @type EVENT.Events -- @field #number IniUnit +--- Create new event handler. +-- @param #EVENT self +-- @return #EVENT self function EVENT:New() local self = BASE:Inherit( self, BASE:New() ) self:F2() @@ -1223,7 +1226,7 @@ EVENTHANDLER = { --- The EVENTHANDLER constructor -- @param #EVENTHANDLER self --- @return #EVENTHANDLER +-- @return #EVENTHANDLER self function EVENTHANDLER:New() self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER return self diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 2fb596fd8..c85943d75 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -4,7 +4,7 @@ -- -- Takes care of the creation and dispatching of scheduled functions for SCHEDULER objects. -- --- This class is tricky and needs some thorought explanation. +-- This class is tricky and needs some thorough explanation. -- SCHEDULE classes are used to schedule functions for objects, or as persistent objects. -- The SCHEDULEDISPATCHER class ensures that: -- @@ -13,9 +13,10 @@ -- - Scheduled functions are automatically removed when the schedule is finished, according the SCHEDULER object parameters. -- -- The SCHEDULEDISPATCHER class will manage SCHEDULER object in memory during garbage collection: --- - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER --- object is _persistent_ within memory. +-- +-- - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER object is _persistent_ within memory. -- - When a SCHEDULER object *is* attached to another object, then the SCHEDULER object is _not persistent_ within memory after a garbage collection! +-- -- The none persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collectged, when the parent object is also desroyed or nillified and garbage collected. -- Even when there are pending timer scheduled functions to be executed for the SCHEDULER object, -- these will not be executed anymore when the SCHEDULER object has been destroyed. @@ -33,13 +34,28 @@ -- @module Core.ScheduleDispatcher -- @image Core_Schedule_Dispatcher.JPG +--- SCHEDULEDISPATCHER class. +-- @type SCHEDULEDISPATCHER +-- @field #string ClassName Name of the class. +-- @field #number CallID Call ID counter. +-- @field #table PersistentSchedulers Persistant schedulers. +-- @field #table ObjectSchedulers Schedulers that only exist as long as the master object exists. +-- @field #table Schedule Meta table setmetatable( {}, { __mode = "k" } ). +-- @extends Core.Base#BASE + --- The SCHEDULEDISPATCHER structure -- @type SCHEDULEDISPATCHER SCHEDULEDISPATCHER = { - ClassName = "SCHEDULEDISPATCHER", - CallID = 0, + ClassName = "SCHEDULEDISPATCHER", + CallID = 0, + PersistentSchedulers = {}, + ObjectSchedulers = {}, + Schedule = nil, } +--- Create a new schedule dispatcher object. +-- @param #SCHEDULEDISPATCHER self +-- @return #SCHEDULEDISPATCHER self function SCHEDULEDISPATCHER:New() local self = BASE:Inherit( self, BASE:New() ) self:F3() @@ -51,11 +67,23 @@ end -- It is constructed as such that a garbage collection is executed on the weak tables, when the Scheduler is nillified. -- Nothing of this code should be modified without testing it thoroughly. -- @param #SCHEDULEDISPATCHER self --- @param Core.Scheduler#SCHEDULER Scheduler +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #function ScheduleFunction Scheduler function. +-- @param #table ScheduleArguments Table of arguments passed to the ScheduleFunction. +-- @param #number Start Start time in seconds. +-- @param #number Repeat Repeat interval in seconds. +-- @param #number Randomize Radomization factor [0,1]. +-- @param #number Stop Stop time in seconds. +-- @param #number TraceLevel Trace level [0,3]. +-- @param Core.Fsm#FSM Fsm Finite state model. +-- @return #table Call ID or nil. function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm ) - self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel } ) + self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm } ) + -- Increase counter. self.CallID = self.CallID + 1 + + -- Create ID. local CallID = self.CallID .. "#" .. ( Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID() or "" ) or "" -- Initialize the ObjectSchedulers array, which is a weakly coupled table. @@ -118,7 +146,6 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr if name_fsm then Info.name = name_fsm end - --env.info( debug.traceback() ) end self:T3( self.Schedule[Scheduler][CallID] ) @@ -126,7 +153,7 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr self.Schedule[Scheduler][CallID].CallHandler = function( Params ) local CallID = Params.CallID - local Info = Params.Info + local Info = Params.Info or {} local Source = Info.source or "?" local Line = Info.currentline or "?" local Name = Info.name or "?" @@ -153,10 +180,9 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr --self:T3( { Schedule = Schedule } ) - local SchedulerObject = Scheduler.SchedulerObject - --local ScheduleObjectName = Scheduler.SchedulerObject:GetNameAndClassID() + local SchedulerObject = Scheduler.MasterObject --Scheduler.SchedulerObject Now is this the Maste or Scheduler object? local ScheduleFunction = Schedule.Function - local ScheduleArguments = Schedule.Arguments + local ScheduleArguments = Schedule.Arguments or {} local Start = Schedule.Start local Repeat = Schedule.Repeat or 0 local Randomize = Schedule.Randomize or 0 @@ -201,7 +227,7 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr - ( Randomize * Repeat / 2 ), ( Randomize * Repeat / 2 ) ) + - 0.01 + 0.0001 -- Accuracy --self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) return ScheduleTime -- returns the next time the function needs to be called. else @@ -222,6 +248,10 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr return CallID end +--- Remove schedule. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #table CallID Call ID. function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID ) self:F2( { Remove = CallID, Scheduler = Scheduler } ) @@ -231,11 +261,19 @@ function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID ) end end +--- Start dispatcher. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #table CallID (Optional) Call ID. +-- @param #table CallID Call ID. +-- @param #string Info (Optional) Debug info. function SCHEDULEDISPATCHER:Start( Scheduler, CallID, Info ) self:F2( { Start = CallID, Scheduler = Scheduler } ) - + if CallID then + local Schedule = self.Schedule[Scheduler] + -- Only start when there is no ScheduleID defined! -- This prevents to "Start" the scheduler twice with the same CallID... if not Schedule[CallID].ScheduleID then @@ -246,31 +284,46 @@ function SCHEDULEDISPATCHER:Start( Scheduler, CallID, Info ) timer.getTime() + Schedule[CallID].Start ) end + else + + -- Recursive. for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do self:Start( Scheduler, CallID, Info ) -- Recursive end + end end +--- Stop dispatcher. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #table CallID Call ID. function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) self:F2( { Stop = CallID, Scheduler = Scheduler } ) if CallID then + local Schedule = self.Schedule[Scheduler] - -- Only stop when there is a ScheduleID defined for the CallID. - -- So, when the scheduler was stopped before, do nothing. + + -- Only stop when there is a ScheduleID defined for the CallID. So, when the scheduler was stopped before, do nothing. if Schedule[CallID].ScheduleID then timer.removeFunction( Schedule[CallID].ScheduleID ) Schedule[CallID].ScheduleID = nil end + else + for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do self:Stop( Scheduler, CallID ) -- Recursive end + end end +--- Clear all schedules by stopping all dispatchers. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. function SCHEDULEDISPATCHER:Clear( Scheduler ) self:F2( { Scheduler = Scheduler } ) @@ -279,9 +332,19 @@ function SCHEDULEDISPATCHER:Clear( Scheduler ) end end -function SCHEDULEDISPATCHER:NoTrace( Scheduler ) +--- Shopw tracing info. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +function SCHEDULEDISPATCHER:ShowTrace( Scheduler ) self:F2( { Scheduler = Scheduler } ) - - Scheduler.ShowTrace = nil + Scheduler.ShowTrace = true +end + +--- No tracing info. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +function SCHEDULEDISPATCHER:NoTrace( Scheduler ) + self:F2( { Scheduler = Scheduler } ) + Scheduler.ShowTrace = false end diff --git a/Moose Development/Moose/Core/Scheduler.lua b/Moose Development/Moose/Core/Scheduler.lua index c5f796185..e5ef5960f 100644 --- a/Moose Development/Moose/Core/Scheduler.lua +++ b/Moose Development/Moose/Core/Scheduler.lua @@ -43,7 +43,9 @@ --- The SCHEDULER class -- @type SCHEDULER --- @field #number ScheduleID the ID of the scheduler. +-- @field #table Schedules Table of schedules. +-- @field #table MasterObject Master object. +-- @field #boolean ShowTrace Trace info if true. -- @extends Core.Base#BASE @@ -69,53 +71,53 @@ -- -- * @{#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection. -- --- SchedulerObject = SCHEDULER:New() --- SchedulerID = SchedulerObject:Schedule( nil, ScheduleFunction, {} ) +-- MasterObject = SCHEDULER:New() +-- SchedulerID = MasterObject:Schedule( nil, ScheduleFunction, {} ) -- --- The above example creates a new SchedulerObject, but does not schedule anything. --- A separate schedule is created by using the SchedulerObject using the method :Schedule..., which returns a ScheduleID +-- The above example creates a new MasterObject, but does not schedule anything. +-- A separate schedule is created by using the MasterObject using the method :Schedule..., which returns a ScheduleID -- -- ### Construct a SCHEDULER object without a volatile schedule, but volatile to the Object existence... -- -- * @{#SCHEDULER.New}( Object ): Setup a new SCHEDULER object, which is linked to the Object. When the Object is nillified or destroyed, the SCHEDULER object will also be destroyed and stopped after garbage collection. -- -- ZoneObject = ZONE:New( "ZoneName" ) --- SchedulerObject = SCHEDULER:New( ZoneObject ) --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {} ) +-- MasterObject = SCHEDULER:New( ZoneObject ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} ) -- ... -- ZoneObject = nil -- garbagecollect() -- --- The above example creates a new SchedulerObject, but does not schedule anything, and is bound to the existence of ZoneObject, which is a ZONE. --- A separate schedule is created by using the SchedulerObject using the method :Schedule()..., which returns a ScheduleID +-- The above example creates a new MasterObject, but does not schedule anything, and is bound to the existence of ZoneObject, which is a ZONE. +-- A separate schedule is created by using the MasterObject using the method :Schedule()..., which returns a ScheduleID -- Later in the logic, the ZoneObject is put to nil, and garbage is collected. --- As a result, the ScheduleObject will cancel any planned schedule. +-- As a result, the MasterObject will cancel any planned schedule. -- -- ### Construct a SCHEDULER object with a persistent schedule. -- -- * @{#SCHEDULER.New}( nil, Function, FunctionArguments, Start, ... ): Setup a new persistent SCHEDULER object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. -- --- SchedulerObject, SchedulerID = SCHEDULER:New( nil, ScheduleFunction, {} ) +-- MasterObject, SchedulerID = SCHEDULER:New( nil, ScheduleFunction, {} ) -- --- The above example creates a new SchedulerObject, and does schedule the first schedule as part of the call. --- Note that 2 variables are returned here: SchedulerObject, ScheduleID... +-- The above example creates a new MasterObject, and does schedule the first schedule as part of the call. +-- Note that 2 variables are returned here: MasterObject, ScheduleID... -- -- ### Construct a SCHEDULER object without a schedule, but volatile to the Object existence... -- -- * @{#SCHEDULER.New}( Object, Function, FunctionArguments, Start, ... ): Setup a new SCHEDULER object, linked to Object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. -- -- ZoneObject = ZONE:New( "ZoneName" ) --- SchedulerObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} ) --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {} ) +-- MasterObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} ) -- ... -- ZoneObject = nil -- garbagecollect() -- --- The above example creates a new SchedulerObject, and schedules a method call (ScheduleFunction), +-- The above example creates a new MasterObject, and schedules a method call (ScheduleFunction), -- and is bound to the existence of ZoneObject, which is a ZONE object (ZoneObject). --- Both a ScheduleObject and a SchedulerID variable are returned. +-- Both a MasterObject and a SchedulerID variable are returned. -- Later in the logic, the ZoneObject is put to nil, and garbage is collected. --- As a result, the ScheduleObject will cancel the planned schedule. +-- As a result, the MasterObject will cancel the planned schedule. -- -- ## SCHEDULER timer stopping and (re-)starting. -- @@ -125,15 +127,15 @@ -- * @{#SCHEDULER.Stop}(): Stop the schedules within the SCHEDULER object. If a CallID is provided to :Stop(), then only the schedule referenced by CallID will be stopped. -- -- ZoneObject = ZONE:New( "ZoneName" ) --- SchedulerObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} ) --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 10 ) +-- MasterObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 10 ) -- ... --- SchedulerObject:Stop( SchedulerID ) +-- MasterObject:Stop( SchedulerID ) -- ... --- SchedulerObject:Start( SchedulerID ) +-- MasterObject:Start( SchedulerID ) -- --- The above example creates a new SchedulerObject, and does schedule the first schedule as part of the call. --- Note that 2 variables are returned here: SchedulerObject, ScheduleID... +-- The above example creates a new MasterObject, and does schedule the first schedule as part of the call. +-- Note that 2 variables are returned here: MasterObject, ScheduleID... -- Later in the logic, the repeating schedule with SchedulerID is stopped. -- A bit later, the repeating schedule with SchedulerId is (re)-started. -- @@ -145,32 +147,32 @@ -- Consider the following code fragment of the SCHEDULER object creation. -- -- ZoneObject = ZONE:New( "ZoneName" ) --- SchedulerObject = SCHEDULER:New( ZoneObject ) +-- MasterObject = SCHEDULER:New( ZoneObject ) -- -- Several parameters can be specified that influence the behaviour of a Schedule. -- -- ### A single schedule, immediately executed -- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {} ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} ) -- -- The above example schedules a new ScheduleFunction call to be executed asynchronously, within milleseconds ... -- -- ### A single schedule, planned over time -- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10 ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10 ) -- -- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds ... -- -- ### A schedule with a repeating time interval, planned over time -- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60 ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60 ) -- -- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, -- and repeating 60 every seconds ... -- -- ### A schedule with a repeating time interval, planned over time, with time interval randomization -- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5 ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5 ) -- -- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, -- and repeating 60 seconds, with a 50% time interval randomization ... @@ -180,7 +182,7 @@ -- -- ### A schedule with a repeating time interval, planned over time, with time interval randomization, and stop after a time interval -- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5, 300 ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5, 300 ) -- -- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, -- The schedule will repeat every 60 seconds. @@ -191,13 +193,15 @@ -- -- @field #SCHEDULER SCHEDULER = { - ClassName = "SCHEDULER", - Schedules = {}, + ClassName = "SCHEDULER", + Schedules = {}, + MasterObject = nil, + ShowTrace = nil, } --- SCHEDULER constructor. -- @param #SCHEDULER self --- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. +-- @param #table MasterObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. -- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. @@ -205,50 +209,46 @@ SCHEDULER = { -- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. -- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. -- @return #SCHEDULER self. --- @return #number The ScheduleID of the planned schedule. -function SCHEDULER:New( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) +-- @return #table The ScheduleID of the planned schedule. +function SCHEDULER:New( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) local self = BASE:Inherit( self, BASE:New() ) -- #SCHEDULER self:F2( { Start, Repeat, RandomizeFactor, Stop } ) local ScheduleID = nil - self.MasterObject = SchedulerObject - self.ShowTrace = true + self.MasterObject = MasterObject + self.ShowTrace = false if SchedulerFunction then - ScheduleID = self:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, 4 ) + ScheduleID = self:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, 4 ) end return self, ScheduleID end ---function SCHEDULER:_Destructor() --- --self:E("_Destructor") --- --- _SCHEDULEDISPATCHER:RemoveSchedule( self.CallID ) ---end - --- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also. -- @param #SCHEDULER self --- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. +-- @param #table MasterObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. -- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. --- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. +-- @param #number Repeat Specifies the time interval in seconds when the scheduler will call the event function. -- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. --- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. --- @return #number The ScheduleID of the planned schedule. -function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, TraceLevel, Fsm ) +-- @param #number Stop Time interval in seconds after which the scheduler will be stoppe. +-- @param #number TraceLevel Trace level [0,3]. Default 3. +-- @param Core.Fsm#FSM Fsm Finite state model. +-- @return #table The ScheduleID of the planned schedule. +function SCHEDULER:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, TraceLevel, Fsm ) self:F2( { Start, Repeat, RandomizeFactor, Stop } ) self:T3( { SchedulerArguments } ) local ObjectName = "-" - if SchedulerObject and SchedulerObject.ClassName and SchedulerObject.ClassID then - ObjectName = SchedulerObject.ClassName .. SchedulerObject.ClassID + if MasterObject and MasterObject.ClassName and MasterObject.ClassID then + ObjectName = MasterObject.ClassName .. MasterObject.ClassID end - self:F3( { "Schedule :", ObjectName, tostring( SchedulerObject ), Start, Repeat, RandomizeFactor, Stop } ) - self.SchedulerObject = SchedulerObject + self:F3( { "Schedule :", ObjectName, tostring( MasterObject ), Start, Repeat, RandomizeFactor, Stop } ) + self.MasterObject = MasterObject local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( self, @@ -269,19 +269,17 @@ end --- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided. -- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +-- @param #table ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. function SCHEDULER:Start( ScheduleID ) self:F3( { ScheduleID } ) - _SCHEDULEDISPATCHER:Start( self, ScheduleID ) end --- Stops the schedules or a specific schedule if a valid ScheduleID is provided. -- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +-- @param #table ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. function SCHEDULER:Stop( ScheduleID ) self:F3( { ScheduleID } ) - _SCHEDULEDISPATCHER:Stop( self, ScheduleID ) end @@ -290,7 +288,6 @@ end -- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. function SCHEDULER:Remove( ScheduleID ) self:F3( { ScheduleID } ) - _SCHEDULEDISPATCHER:Remove( self, ScheduleID ) end @@ -298,28 +295,17 @@ end -- @param #SCHEDULER self function SCHEDULER:Clear() self:F3( ) - _SCHEDULEDISPATCHER:Clear( self ) end +--- Show tracing for this scheduler. +-- @param #SCHEDULER self +function SCHEDULER:ShowTrace() + _SCHEDULEDISPATCHER:ShowTrace( self ) +end + --- No tracing for this scheduler. -- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. function SCHEDULER:NoTrace() - _SCHEDULEDISPATCHER:NoTrace( self ) end - - - - - - - - - - - - - - diff --git a/Moose Development/Moose/Globals.lua b/Moose Development/Moose/Globals.lua index 05f3b8cf3..bdde44b6d 100644 --- a/Moose Development/Moose/Globals.lua +++ b/Moose Development/Moose/Globals.lua @@ -5,7 +5,7 @@ _EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT --- Declare the timer dispatcher based on the SCHEDULEDISPATCHER class -_SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.Timer#SCHEDULEDISPATCHER +_SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.ScheduleDispatcher#SCHEDULEDISPATCHER --- Declare the main database object, which is used internally by the MOOSE classes. _DATABASE = DATABASE:New() -- Core.Database#DATABASE From 1c99e474b22f55603494982fcd1abee9614ba140 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 10 Dec 2019 20:11:49 +0100 Subject: [PATCH 398/485] SPAWN AI_Balancer SPAWN - Added :InitAirbase() AI_BALANCER - Fixed bug in Return function. --- Moose Development/Moose/AI/AI_Balancer.lua | 9 +++++++- Moose Development/Moose/Core/Spawn.lua | 26 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Balancer.lua b/Moose Development/Moose/AI/AI_Balancer.lua index 1b18bf79d..9c3be56e7 100644 --- a/Moose Development/Moose/AI/AI_Balancer.lua +++ b/Moose Development/Moose/AI/AI_Balancer.lua @@ -196,8 +196,12 @@ function AI_BALANCER:onenterDestroying( SetGroup, From, Event, To, ClientName, A SetGroup:Flush( self ) end ---- @param #AI_BALANCER self +--- RTB +-- @param #AI_BALANCER self -- @param Core.Set#SET_GROUP SetGroup +-- @param #string From +-- @param #string Event +-- @param #string To -- @param Wrapper.Group#GROUP AIGroup function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) @@ -213,10 +217,13 @@ function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) local PointVec2 = POINT_VEC2:New( AIGroup:GetVec2().x, AIGroup:GetVec2().y ) local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 ) self:T( ClosestAirbase.AirbaseName ) + --[[ AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 ) local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase ) AIGroupTemplate.route = RTBRoute AIGroup:Respawn( AIGroupTemplate ) + ]] + AIGroup:RouteRTB(ClosestAirbase) end end diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 0d49bfb5b..4865754ca 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -326,6 +326,7 @@ function SPAWN:New( SpawnTemplatePrefix ) self.SpawnInitModu = nil -- No special modulation. self.SpawnInitRadio = nil -- No radio comms setting. self.SpawnInitModex = nil + self.SpawnInitAirbase = nil self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -378,6 +379,7 @@ function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) self.SpawnInitModu = nil -- No special modulation. self.SpawnInitRadio = nil -- No radio comms setting. self.SpawnInitModex = nil + self.SpawnInitAirbase = nil self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -433,6 +435,7 @@ function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPr self.SpawnInitModu = nil -- No special modulation. self.SpawnInitRadio = nil -- No radio comms setting. self.SpawnInitModex = nil + self.SpawnInitAirbase = nil self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -503,6 +506,21 @@ function SPAWN:InitLateActivated( LateActivated ) return self end +--- Set spawns to happen at a particular airbase. Only for aircraft, of course. +-- @param #SPAWN self +-- @param #string AirbaseName Name of the airbase. +-- @param #number Takeoff (Optional) Takeoff type. Can be SPAWN.Takeoff.Hot (default), SPAWN.Takeoff.Cold or SPAWN.Takeoff.Runway. +-- @return #SPAWN self +function SPAWN:InitAirbase( AirbaseName, Takeoff ) + self:F( ) + + self.SpawnInitAirbase=AIRBASE:FindByName(AirbaseName) + + self.SpawnInitTakeoff=Takeoff or SPAWN.Takeoff.Hot + + return self +end + --- Defines the Heading for the new spawned units. -- The heading can be given as one fixed degree, or can be randomized between minimum and maximum degrees. @@ -1115,7 +1133,12 @@ end -- Delay methods function SPAWN:Spawn() self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, self.AliveUnits } ) - return self:SpawnWithIndex( self.SpawnIndex + 1 ) + if self.SpawnInitAirbase then + return self:SpawnAtAirbase(self.SpawnInitAirbase, self.SpawnInitTakeoff) + else + return self:SpawnWithIndex( self.SpawnIndex + 1 ) + end + end --- Will re-spawn a group based on a given index. @@ -3239,6 +3262,7 @@ end --- This function is called automatically by the Spawning scheduler. -- It is the internal worker method SPAWNing new Groups on the defined time intervals. +-- @param #SPAWN self function SPAWN:_Scheduler() self:F2( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) From 0d57ad558448d33c360e72c87ff3f38b7afd01f2 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 11 Dec 2019 21:10:23 +0100 Subject: [PATCH 399/485] ATIS v0.6.0 - Added altimeter QNH calculation --- Moose Development/Moose/AI/AI_Air.lua | 2 +- .../Moose/AI/AI_Air_Squadron.lua | 4 +- .../Moose/AI/AI_Escort_Dispatcher.lua | 4 +- .../Moose/AI/AI_Escort_Dispatcher_Request.lua | 4 +- Moose Development/Moose/DCS.lua | 7 +- Moose Development/Moose/Ops/ATIS.lua | 65 +++++++++++++++++-- .../Moose/Tasking/Task_Capture_Dispatcher.lua | 2 +- 7 files changed, 70 insertions(+), 18 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 3477a3d2a..eeb9053b7 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -1,4 +1,4 @@ ---- **AI** -- Models the process of AI air operations. +--- **AI** - Models the process of AI air operations. -- -- === -- diff --git a/Moose Development/Moose/AI/AI_Air_Squadron.lua b/Moose Development/Moose/AI/AI_Air_Squadron.lua index d55e65ded..69e660877 100644 --- a/Moose Development/Moose/AI/AI_Air_Squadron.lua +++ b/Moose Development/Moose/AI/AI_Air_Squadron.lua @@ -1,4 +1,4 @@ ---- **AI** -- Models squadrons for airplanes and helicopters. +--- **AI** - Models squadrons for airplanes and helicopters. -- -- This is a class used in the @{AI_Air_Dispatcher} and derived dispatcher classes. -- @@ -9,7 +9,7 @@ -- === -- -- @module AI.AI_Air_Squadron --- @image AI_Air_To_Air_Engage.JPG +-- @image MOOSE.JPG diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua index 7b85afe07..26e89a728 100644 --- a/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua @@ -1,4 +1,4 @@ ---- **AI** -- (R2.5) - Models the automatic assignment of AI escorts to player flights. +--- **AI** - Models the automatic assignment of AI escorts to player flights. -- -- ## Features: -- -- @@ -12,7 +12,7 @@ -- === -- -- @module AI.AI_Escort_Dispatcher --- @image AI_Escort_Dispatcher.JPG +-- @image MOOSE.JPG --- @type AI_ESCORT_DISPATCHER diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua index 8735f7064..33c614d4a 100644 --- a/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua @@ -1,4 +1,4 @@ ---- **AI** -- (R2.5) - Models the assignment of AI escorts to player flights upon request using the radio menu. +--- **AI** - Models the assignment of AI escorts to player flights upon request using the radio menu. -- -- ## Features: -- @@ -12,7 +12,7 @@ -- === -- -- @module AI.AI_ESCORT_DISPATCHER_REQUEST --- @image AI_ESCORT_DISPATCHER_REQUEST.JPG +-- @image MOOSE.JPG --- @type AI_ESCORT_DISPATCHER_REQUEST diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index 0d508793c..b330bf43c 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -1,6 +1,7 @@ ---- DCS API prototypes --- See [https://wiki.hoggitworld.com/view/Simulator_Scripting_Engine_Documentation](https://wiki.hoggitworld.com/view/Simulator_Scripting_Engine_Documentation) --- for further explanation and examples. +--- **DCS API** Prototypes +-- +-- See the [Simulator Scripting Engine Documentation](https://wiki.hoggitworld.com/view/Simulator_Scripting_Engine_Documentation) on Hoggit for further explanation and examples. +-- -- @module DCS -- @image MOOSE.JPG diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 38fcb3401..6d7a939bd 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -1,4 +1,4 @@ ---- **Ops** - (R2.5) - Automatic Terminal Information Service (ATIS). +--- **Ops** - Automatic Terminal Information Service (ATIS). -- -- === -- @@ -81,6 +81,7 @@ -- @field #table runwaymag Table of magnetic runway headings. -- @field #number runwaym2t Optional correction for magnetic to true runway heading conversion (and vice versa) in degrees. -- @field #boolean windtrue Report true (from) heading of wind. Default is magnetic. +-- @field #boolean altimeterQNH Report altimeter QNH. -- @extends Core.Fsm#FSM --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde @@ -307,6 +308,7 @@ ATIS = { runwaymag = {}, runwaym2t = nil, windtrue = nil, + altimeterQNH = nil, } --- NATO alphabet. @@ -513,7 +515,7 @@ _ATIS={} --- ATIS class version. -- @field #string version -ATIS.version="0.5.0" +ATIS.version="0.6.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -567,6 +569,7 @@ function ATIS:New(airbasename, frequency, modulation) self:SetMagneticDeclination() self:SetRunwayCorrectionMagnetic2True() self:SetRadioPower() + self:SetAltimeterQNH(true) -- Start State. self:SetStartState("Stopped") @@ -775,6 +778,21 @@ function ATIS:SetTemperatureFahrenheit() return self end +--- Report altimeter QNH. +-- @param #ATIS self +-- @param #boolean switch If true or nil, report altimeter QHN. If false, report QFF. +-- @return #ATIS self +function ATIS:SetAltimeterQNH(switch) + + if switch==true or switch==nil then + self.altimeterQNH=true + else + self.altimeterQNH=false + end + + return self +end + --- Set magnetic declination/variation at the airport. -- -- Default is per map: @@ -1013,7 +1031,7 @@ function ATIS:onafterBroadcast(From, Event, To) local coord=self.airbase:GetCoordinate() -- Get elevation. - local height=coord:GetLandHeight()+10 + local height=coord:GetLandHeight() ---------------- --- Pressure --- @@ -1022,6 +1040,32 @@ function ATIS:onafterBroadcast(From, Event, To) -- Pressure in hPa. local qfe=coord:GetPressure(height) local qnh=coord:GetPressure(0) + + if self.altimeterQNH then + + -- Some constants. + local L=-0.0065 --[K/m] + local R= 8.31446 --[J/mol/K] + local g= 9.80665 --[m/s^2] + local M= 0.0289644 --[kg/mol] + local T0=coord:GetTemperature(0)+273.15 --[K] Temp at sea level. + local TS=288.15 -- Standard Temperature assumed by Altimeter is 15°C + local q=qnh*100 + + -- Calculate Pressure. + local P=q*(1+L*height/T0)^(-g*M/(R*L)) -- Pressure at sea level + local Q=P/(1+L*height/TS)^(-g*M/(R*L)) -- Altimeter QNH + local A=(T0/L)*((P/q)^(((-R*L)/(g*M)))-1) -- Altitude check + + + -- Debug aoutput + self:T2(self.lid..string.format("height=%.1f, A=%.1f, T0=%.1f, QFE=%.1f, QNH=%.1f, P=%.1f, Q=%.1f hPa = %.2f", height, A, T0-273.15, qfe, qnh, P/100, Q/100, UTILS.hPa2inHg(Q/100))) + + -- Set QNH value in hPa. + qnh=Q/100 + + end + -- Convert to inHg. if self.PmmHg then @@ -1052,7 +1096,7 @@ function ATIS:onafterBroadcast(From, Event, To) ------------ -- Get wind direction and speed in m/s. - local windFrom, windSpeed=coord:GetWind(height) + local windFrom, windSpeed=coord:GetWind(height+10) -- Wind in magnetic or true. local magvar=self.magvar @@ -1134,7 +1178,7 @@ function ATIS:onafterBroadcast(From, Event, To) ------------------- -- Temperature in °C (or °F). - local temperature=coord:GetTemperature(height) + local temperature=coord:GetTemperature(height+5) -- Convert to °F. if self.TDegF then @@ -1151,12 +1195,12 @@ function ATIS:onafterBroadcast(From, Event, To) local clouds, visibility, turbulence, fog, dust, static=self:GetMissionWeather() -- Check that fog is actually "thick" enough to reach the airport. If an airport is in the mountains, fog might not affect it as it is measured from sea level. - if fog and fog.thicknessUTILS.FeetToMeters(1500) then + if dust and height+25>UTILS.FeetToMeters(1500) then dust=nil end @@ -1655,6 +1699,13 @@ end -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get runway from user supplied magnetic heading. +-- @param #ATIS self +-- @return #string Runway magnetic heading divided by ten (and rounded). Eg, "13" for 130°. +function ATIS:GetActiveRunway() + +end + --- Get runway from user supplied magnetic heading. -- @param #ATIS self -- @param #number windfrom Wind direction (from) in degrees. diff --git a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua index 7f1fa7d08..0c05ba18b 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua @@ -55,7 +55,7 @@ -- === -- -- @module Tasking.Task_Zone_Capture_Dispatcher --- @image Task_Zone_Capture_Dispatcher.JPG +-- @image MOOSE.JPG do -- TASK_CAPTURE_DISPATCHER From 59c46f9b4b6f40d3999fd8e8d7b0989cd4440f69 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 11 Dec 2019 21:38:53 +0100 Subject: [PATCH 400/485] Push --- Moose Development/Moose/Ops/Airboss.lua | 2 +- Moose Development/Moose/Ops/RecoveryTanker.lua | 2 +- Moose Development/Moose/Ops/RescueHelo.lua | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 91bf8a91a..da53af408 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1,4 +1,4 @@ ---- **Ops** - (R2.5) - Manages aircraft CASE X recoveries for carrier operations (X=I, II, III). +--- **Ops** - Manages aircraft CASE X recoveries for carrier operations (X=I, II, III). -- -- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. -- diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index a3af32cb9..dae5f2ab4 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -1,4 +1,4 @@ ---- **Ops** - (R2.5) - Recovery tanker for carrier operations. +--- **Ops** - Recovery tanker for carrier operations. -- -- 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 0a040f32e..b3b0fc69f 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -1,4 +1,4 @@ ---- **Ops** - (R2.5) - Rescue helicopter for carrier operations. +--- **Ops** - Rescue helicopter for carrier operations. -- -- Recue helicopter for carrier operations. -- From ca49e8f8aeaf3ba787d2c9a47473b9fa8d0e7179 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 15 Dec 2019 22:30:05 +0100 Subject: [PATCH 401/485] FOX v0.6.1 - Added Stop event. --- Moose Development/Moose/Functional/Fox.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index 47faf90bf..1963afd83 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -198,7 +198,7 @@ FOX.MenuF10Root=nil --- FOX class version. -- @field #string version -FOX.version="0.6.0" +FOX.version="0.6.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -243,6 +243,7 @@ function FOX:New() self:AddTransition("*", "MissileDestroyed", "*") -- Missile was destroyed before impact. self:AddTransition("*", "EnterSafeZone", "*") -- Player enters a safe zone. self:AddTransition("*", "ExitSafeZone", "*") -- Player exists a safe zone. + self:AddTransition("Running", "Stop", "Stopped") -- Stop FOX script. ------------------------ --- Pseudo Functions --- From c3c984381e1025866d8a7730d4fb6972bd6419ab Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 16 Dec 2019 00:59:23 +0100 Subject: [PATCH 402/485] SUPPRESSION --- .../Moose/Functional/Suppression.lua | 316 +++++++++++++----- Moose Development/Moose/Wrapper/Group.lua | 39 +++ Moose Development/Moose/Wrapper/Unit.lua | 88 +++++ 3 files changed, 353 insertions(+), 90 deletions(-) diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua index 3df26be6d..1c8c56092 100644 --- a/Moose Development/Moose/Functional/Suppression.lua +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -5,6 +5,10 @@ -- ## Features: -- -- * Hold fire of attacked units when being fired upon. +-- * Retreat to a user defined zone. +-- * Fall back on hits. +-- * Take cover on hits. +-- * Gaussian distribution of suppression time. -- -- === -- @@ -44,6 +48,7 @@ -- @type SUPPRESSION -- @field #string ClassName Name of the class. -- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. +-- @field #string lid String for DCS log file. -- @field #boolean flare Flare units when they get hit or die. -- @field #boolean smoke Smoke places to which the group retreats, falls back or hides. -- @field #list DCSdesc Table containing all DCS descriptors of the group. @@ -222,41 +227,42 @@ -- -- @field #SUPPRESSION SUPPRESSION={ - ClassName = "SUPPRESSION", - Debug = false, - flare = false, - smoke = false, - DCSdesc = nil, - Type = nil, - IsInfantry=nil, - SpeedMax = nil, - Tsuppress_ave = 15, - Tsuppress_min = 5, - Tsuppress_max = 25, - TsuppressOver = nil, - IniGroupStrength = nil, - Nhit = 0, - Formation = "Off road", - Speed = 4, - MenuON = false, - FallbackON = false, - FallbackWait = 60, - FallbackDist = 100, - FallbackHeading = nil, - TakecoverON = false, - TakecoverWait = 120, - TakecoverRange = 300, - hideout = nil, - PminFlee = 10, - PmaxFlee = 90, - RetreatZone = nil, - RetreatDamage = nil, - RetreatWait = 7200, + ClassName = "SUPPRESSION", + Debug = false, + lid = nil, + flare = false, + smoke = false, + DCSdesc = nil, + Type = nil, + IsInfantry = nil, + SpeedMax = nil, + Tsuppress_ave = 15, + Tsuppress_min = 5, + Tsuppress_max = 25, + TsuppressOver = nil, + IniGroupStrength = nil, + Nhit = 0, + Formation = "Off road", + Speed = 4, + MenuON = false, + FallbackON = false, + FallbackWait = 60, + FallbackDist = 100, + FallbackHeading = nil, + TakecoverON = false, + TakecoverWait = 120, + TakecoverRange = 300, + hideout = nil, + PminFlee = 10, + PmaxFlee = 90, + RetreatZone = nil, + RetreatDamage = nil, + RetreatWait = 7200, CurrentAlarmState = "unknown", - CurrentROE = "unknown", + CurrentROE = "unknown", DefaultAlarmState = "Auto", - DefaultROE = "Weapon Free", - eventmoose = true, + DefaultROE = "Weapon Free", + eventmoose = true, } --- Enumerator of possible rules of engagement. @@ -279,13 +285,9 @@ SUPPRESSION.AlarmState={ -- @field #string MenuF10 SUPPRESSION.MenuF10=nil ---- Some ID to identify who we are in output of the DCS.log file. --- @field #string id -SUPPRESSION.id="SUPPRESSION | " - --- PSEUDOATC version. -- @field #number version -SUPPRESSION.version="0.9.0" +SUPPRESSION.version="0.9.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -302,22 +304,22 @@ SUPPRESSION.version="0.9.0" -- @return #SUPPRESSION SUPPRESSION object. -- @return nil If group does not exist or is not a ground group. function SUPPRESSION:New(group) - BASE:F2(group) -- Inherits from FSM_CONTROLLABLE local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #SUPPRESSION -- Check that group is present. if group then - self:T(SUPPRESSION.id..string.format("SUPPRESSION version %s. Activating suppressive fire for group %s", SUPPRESSION.version, group:GetName())) + self.lid=string.format("SUPPRESSION %s | ", tostring(group:GetName())) + self:T(self.lid..string.format("SUPPRESSION version %s. Activating suppressive fire for group %s", SUPPRESSION.version, group:GetName())) else - self:E(SUPPRESSION.id.."Suppressive fire: Requested group does not exist! (Has to be a MOOSE group.)") + self:E(self.lid.."SUPPRESSION | Requested group does not exist! (Has to be a MOOSE group.)") return nil end -- Check that we actually have a GROUND group. if group:IsGround()==false then - self:E(SUPPRESSION.id..string.format("SUPPRESSION fire group %s has to be a GROUND group!", group:GetName())) + self:E(self.lid..string.format("SUPPRESSION fire group %s has to be a GROUND group!", group:GetName())) return nil end @@ -350,6 +352,7 @@ function SUPPRESSION:New(group) -- Transitions self:AddTransition("*", "Start", "CombatReady") + self:AddTransition("*", "Status", "*") self:AddTransition("CombatReady", "Hit", "Suppressed") self:AddTransition("Suppressed", "Hit", "Suppressed") self:AddTransition("Suppressed", "Recovered", "CombatReady") @@ -364,6 +367,38 @@ function SUPPRESSION:New(group) self:AddTransition("TakingCover", "Hit", "TakingCover") self:AddTransition("FallingBack", "Hit", "FallingBack") + + --- Trigger "Status" event. + -- @function [parent=#SUPPRESSION] Status + -- @param #SUPPRESSION self + + --- Trigger "Status" event after a delay. + -- @function [parent=#SUPPRESSION] __Status + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + + --- User function for OnAfter "Status" event. + -- @function [parent=#SUPPRESSION] OnAfterStatus + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Trigger "Hit" event. + -- @function [parent=#SUPPRESSION] Hit + -- @param #SUPPRESSION self + -- @param Wrapper.Unit#UNIT Unit Unit that was hit. + -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + + --- Trigger "Hit" event after a delay. + -- @function [parent=#SUPPRESSION] __Hit + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + -- @param Wrapper.Unit#UNIT Unit Unit that was hit. + -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + --- User function for OnBefore "Hit" event. -- @function [parent=#SUPPRESSION] OnBeforeHit -- @param #SUPPRESSION self @@ -375,7 +410,7 @@ function SUPPRESSION:New(group) -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. -- @return #boolean - --- User function for OnAfer "Hit" event. + --- User function for OnAfter "Hit" event. -- @function [parent=#SUPPRESSION] OnAfterHit -- @param #SUPPRESSION self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -384,7 +419,16 @@ function SUPPRESSION:New(group) -- @param #string To To state. -- @param Wrapper.Unit#UNIT Unit Unit that was hit. -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + + --- Trigger "Recovered" event. + -- @function [parent=#SUPPRESSION] Recovered + -- @param #SUPPRESSION self + + --- Trigger "Recovered" event after a delay. + -- @function [parent=#SUPPRESSION] Recovered + -- @param #number Delay Delay in seconds. + -- @param #SUPPRESSION self --- User function for OnBefore "Recovered" event. -- @function [parent=#SUPPRESSION] OnBeforeRecovered @@ -404,6 +448,17 @@ function SUPPRESSION:New(group) -- @param #string To To state. + --- Trigger "TakeCover" event. + -- @function [parent=#SUPPRESSION] TakeCover + -- @param #SUPPRESSION self + -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + + --- Trigger "TakeCover" event after a delay. + -- @function [parent=#SUPPRESSION] __TakeCover + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + --- User function for OnBefore "TakeCover" event. -- @function [parent=#SUPPRESSION] OnBeforeTakeCover -- @param #SUPPRESSION self @@ -424,6 +479,17 @@ function SUPPRESSION:New(group) -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + --- Trigger "FallBack" event. + -- @function [parent=#SUPPRESSION] FallBack + -- @param #SUPPRESSION self + -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + + --- Trigger "FallBack" event after a delay. + -- @function [parent=#SUPPRESSION] __FallBack + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + --- User function for OnBefore "FallBack" event. -- @function [parent=#SUPPRESSION] OnBeforeFallBack -- @param #SUPPRESSION self @@ -444,6 +510,15 @@ function SUPPRESSION:New(group) -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + --- Trigger "Retreat" event. + -- @function [parent=#SUPPRESSION] Retreat + -- @param #SUPPRESSION self + + --- Trigger "Retreat" event after a delay. + -- @function [parent=#SUPPRESSION] __Retreat + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + --- User function for OnBefore "Retreat" event. -- @function [parent=#SUPPRESSION] OnBeforeRetreat -- @param #SUPPRESSION self @@ -462,6 +537,15 @@ function SUPPRESSION:New(group) -- @param #string To To state. + --- Trigger "Retreated" event. + -- @function [parent=#SUPPRESSION] Retreated + -- @param #SUPPRESSION self + + --- Trigger "Retreated" event after a delay. + -- @function [parent=#SUPPRESSION] __Retreated + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + --- User function for OnBefore "Retreated" event. -- @function [parent=#SUPPRESSION] OnBeforeRetreated -- @param #SUPPRESSION self @@ -480,6 +564,15 @@ function SUPPRESSION:New(group) -- @param #string To To state. + --- Trigger "FightBack" event. + -- @function [parent=#SUPPRESSION] FightBack + -- @param #SUPPRESSION self + + --- Trigger "FightBack" event after a delay. + -- @function [parent=#SUPPRESSION] __FightBack + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + --- User function for OnBefore "FlightBack" event. -- @function [parent=#SUPPRESSION] OnBeforeFightBack -- @param #SUPPRESSION self @@ -498,6 +591,23 @@ function SUPPRESSION:New(group) -- @param #string To To state. + --- Trigger "Dead" event. + -- @function [parent=#SUPPRESSION] Dead + -- @param #SUPPRESSION self + + --- Trigger "Dead" event after a delay. + -- @function [parent=#SUPPRESSION] __Dead + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + + --- User function for OnAfter "Dead" event. + -- @function [parent=#SUPPRESSION] OnAfterDead + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + return self end @@ -524,9 +634,9 @@ function SUPPRESSION:SetSuppressionTime(Tave, Tmin, Tmax) self.Tsuppress_ave=math.max(self.Tsuppress_min) self.Tsuppress_ave=math.min(self.Tsuppress_max) - self:T(SUPPRESSION.id..string.format("Set ave suppression time to %d seconds.", self.Tsuppress_ave)) - self:T(SUPPRESSION.id..string.format("Set min suppression time to %d seconds.", self.Tsuppress_min)) - self:T(SUPPRESSION.id..string.format("Set max suppression time to %d seconds.", self.Tsuppress_max)) + self:T(self.lid..string.format("Set ave suppression time to %d seconds.", self.Tsuppress_ave)) + self:T(self.lid..string.format("Set min suppression time to %d seconds.", self.Tsuppress_min)) + self:T(self.lid..string.format("Set max suppression time to %d seconds.", self.Tsuppress_max)) end --- Set the zone to which a group retreats after being damaged too much. @@ -756,13 +866,14 @@ end --- Status of group. Current ROE, alarm state, life. -- @param #SUPPRESSION self -- @param #boolean message Send message to all players. -function SUPPRESSION:Status(message) +function SUPPRESSION:StatusReport(message) local name=self.Controllable:GetName() local nunits=#self.Controllable:GetUnits() local roe=self.CurrentROE local state=self.CurrentAlarmState local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife() + local at=self.Controllable:GetAmmunition() local text=string.format("Status of group %s\n", name) text=text..string.format("Number of units: %d of %d\n", nunits, self.IniGroupStrength) @@ -774,10 +885,11 @@ function SUPPRESSION:Status(message) text=text..string.format("Life max: %3.0f\n", life_max) text=text..string.format("Life ave: %3.0f\n", life_ave) text=text..string.format("Life ave0: %3.0f\n", life_ave0) + text=text..string.format("Ammo tot: %d\n", at) text=text..string.format("Group strength: %3.0f", groupstrength) MESSAGE:New(text, 10):ToAllIf(message or self.Debug) - self:T(SUPPRESSION.id.."\n"..text) + self:I(self.lid.."\n"..text) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -794,6 +906,7 @@ function SUPPRESSION:onafterStart(Controllable, From, Event, To) self:_EventFromTo("onafterStart", Event, From, To) local text=string.format("Started SUPPRESSION for group %s.", Controllable:GetName()) + self:I(self.lid..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) local rzone="not defined" @@ -854,7 +967,7 @@ function SUPPRESSION:onafterStart(Controllable, From, Event, To) text=text..string.format("Speed max = %5.1f km/h\n", self.SpeedMax) text=text..string.format("Formation = %s\n", self.Formation) text=text..string.format("******************************************************\n") - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) -- Add event handler. if self.eventmoose then @@ -863,9 +976,37 @@ function SUPPRESSION:onafterStart(Controllable, From, Event, To) else world.addEventHandler(self) end - + + + self:__Status(-1) end +--- After "Status" event. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterStatus(Controllable, From, Event, To) + + --local text=string.format("State=%s, ROE %d, Life=%.1f", Controllable:GetName()) + --MESSAGE:New(text, 10):ToAllIf(self.Debug) + + local group=self.Controllable --Wrapper.Group#GROUP + + local n=group:GetAmmunition() + + self:StatusReport(false) + + -- Retreat if completely out of ammo and retreat zone defined. + if n==0 and self.RetreatZone then + + self:Retreat() + + end + + self:__Status(-30) +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Before "Hit" event. (Of course, this is not really before the group got hit.) @@ -881,7 +1022,7 @@ function SUPPRESSION:onbeforeHit(Controllable, From, Event, To, Unit, AttackUnit self:_EventFromTo("onbeforeHit", Event, From, To) --local Tnow=timer.getTime() - --env.info(SUPPRESSION.id..string.format("Last hit = %s %s", tostring(self.LastHit), tostring(Tnow))) + --env.info(self.lid..string.format("Last hit = %s %s", tostring(self.LastHit), tostring(Tnow))) return true end @@ -925,7 +1066,7 @@ function SUPPRESSION:onafterHit(Controllable, From, Event, To, Unit, AttackUnit) text=string.format("\nGroup %s: Life min=%5.1f, max=%5.1f, ave=%5.1f, ave0=%5.1f group=%5.1f\n", Controllable:GetName(), life_min, life_max, life_ave, life_ave0, groupstrength) text=string.format("Group %s: Damage = %8.4f (%8.4f retreat threshold).\n", Controllable:GetName(), Damage, self.RetreatDamage) text=string.format("Group %s: P_Flee = %5.1f %5.1f=P_rand (P_Flee > Prand ==> Flee)\n", Controllable:GetName(), Pflee, P) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) -- Group is obviously destroyed. if Damage >= 99.9 then @@ -957,11 +1098,6 @@ function SUPPRESSION:onafterHit(Controllable, From, Event, To, Unit, AttackUnit) end end - -- Give info on current status. - if self.Debug then - self:Status() - end - end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -980,7 +1116,7 @@ function SUPPRESSION:onbeforeRecovered(Controllable, From, Event, To) local Tnow=timer.getTime() -- Debug info - self:T(SUPPRESSION.id..string.format("onbeforeRecovered: Time now: %d - Time over: %d", Tnow, self.TsuppressionOver)) + self:T(self.lid..string.format("onbeforeRecovered: Time now: %d - Time over: %d", Tnow, self.TsuppressionOver)) -- Recovery is only possible if enough time since the last hit has passed. if Tnow >= self.TsuppressionOver then @@ -1005,7 +1141,7 @@ function SUPPRESSION:onafterRecovered(Controllable, From, Event, To) -- Debug message. local text=string.format("Group %s has recovered!", Controllable:GetName()) MESSAGE:New(text, 10):ToAllIf(self.Debug) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) -- Set ROE back to default. self:_SetROE() @@ -1066,7 +1202,7 @@ function SUPPRESSION:onafterFallBack(Controllable, From, Event, To, AttackUnit) self:_EventFromTo("onafterFallback", Event, From, To) -- Debug info - self:T(SUPPRESSION.id..string.format("Group %s is falling back after %d hits.", Controllable:GetName(), self.Nhit)) + self:T(self.lid..string.format("Group %s is falling back after %d hits.", Controllable:GetName(), self.Nhit)) -- Coordinate of the attacker and attacked unit. local ACoord=AttackUnit:GetCoordinate() @@ -1174,8 +1310,8 @@ function SUPPRESSION:onbeforeRetreat(Controllable, From, Event, To) self:_EventFromTo("onbeforeRetreat", Event, From, To) if From=="Retreating" then - local text=string.format("Group %s is already retreating.") - self:T2(SUPPRESSION.id..text) + local text=string.format("Group %s is already retreating.", tostring(Controllable:GetName())) + self:T2(self.lid..text) return false else return true @@ -1195,7 +1331,7 @@ function SUPPRESSION:onafterRetreat(Controllable, From, Event, To) -- Route the group to a zone. local text=string.format("Group %s is retreating! Alarm state green.", Controllable:GetName()) MESSAGE:New(text, 10):ToAllIf(self.Debug) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) -- Get a random point in the retreat zone. local ZoneCoord=self.RetreatZone:GetRandomCoordinate() -- Core.Point#COORDINATE @@ -1272,11 +1408,11 @@ function SUPPRESSION:onafterDead(Controllable, From, Event, To) local text=string.format("Group %s: One of our units just died! %d units left.", self.Controllable:GetName(), nunits) MESSAGE:New(text, 10):ToAllIf(self.Debug) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) -- Go to stop state. if nunits==0 then - self:T(SUPPRESSION.id..string.format("Stopping SUPPRESSION for group %s.", Controllable:GetName())) + self:T(self.lid..string.format("Stopping SUPPRESSION for group %s.", Controllable:GetName())) self:Stop() if self.mooseevents then self:UnHandleEvent(EVENTS.Dead) @@ -1348,7 +1484,7 @@ function SUPPRESSION:_OnEventHit(EventData) -- Check that correct group was hit. if GroupNameTgt == GroupNameSelf then - self:T(SUPPRESSION.id..string.format("Hit event at t = %5.1f", timer.getTime())) + self:T(self.lid..string.format("Hit event at t = %5.1f", timer.getTime())) -- Flare unit that was hit. if self.flare or self.Debug then @@ -1359,11 +1495,11 @@ function SUPPRESSION:_OnEventHit(EventData) self.Nhit=self.Nhit+1 -- Info on hit times. - self:T(SUPPRESSION.id..string.format("Group %s has just been hit %d times.", self.Controllable:GetName(), self.Nhit)) + self:T(self.lid..string.format("Group %s has just been hit %d times.", self.Controllable:GetName(), self.Nhit)) --self:Status() local life=tgt:getLife()/(tgt:getLife0()+1)*100 - self:T2(SUPPRESSION.id..string.format("Target unit life = %5.1f", life)) + self:T2(self.lid..string.format("Target unit life = %5.1f", life)) -- FSM Hit event. self:__Hit(3, TgtUnit, IniUnit) @@ -1380,35 +1516,35 @@ function SUPPRESSION:_OnEventDead(EventData) local GroupNameIni=EventData.IniGroupName -- Check for correct group. - if GroupNameIni== GroupNameSelf then + if GroupNameIni==GroupNameSelf then -- Dead Unit. local IniUnit=EventData.IniUnit --Wrapper.Unit#UNIT local IniUnitName=EventData.IniUnitName if EventData.IniUnit then - self:T2(SUPPRESSION.id..string.format("Group %s: Dead MOOSE unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName)) + self:T2(self.lid..string.format("Group %s: Dead MOOSE unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName)) else - self:T2(SUPPRESSION.id..string.format("Group %s: Dead MOOSE unit DOES NOT not exist! Unit name %s.", GroupNameIni, IniUnitName)) + self:T2(self.lid..string.format("Group %s: Dead MOOSE unit DOES NOT not exist! Unit name %s.", GroupNameIni, IniUnitName)) end if EventData.IniDCSUnit then - self:T2(SUPPRESSION.id..string.format("Group %s: Dead DCS unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName)) + self:T2(self.lid..string.format("Group %s: Dead DCS unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName)) else - self:T2(SUPPRESSION.id..string.format("Group %s: Dead DCS unit DOES NOT exist! Unit name %s.", GroupNameIni, IniUnitName)) + self:T2(self.lid..string.format("Group %s: Dead DCS unit DOES NOT exist! Unit name %s.", GroupNameIni, IniUnitName)) end -- Flare unit that died. if IniUnit and (self.flare or self.Debug) then IniUnit:FlareWhite() - self:T(SUPPRESSION.id..string.format("Flare Dead MOOSE unit.")) + self:T(self.lid..string.format("Flare Dead MOOSE unit.")) end -- Flare unit that died. if EventData.IniDCSUnit and (self.flare or self.Debug) then local p=EventData.IniDCSUnit:getPosition().p trigger.action.signalFlare(p, trigger.flareColor.Yellow , 0) - self:T(SUPPRESSION.id..string.format("Flare Dead DCS unit.")) + self:T(self.lid..string.format("Flare Dead DCS unit.")) end -- Get status. @@ -1462,7 +1598,7 @@ function SUPPRESSION:_Suppress() -- Debug message. local text=string.format("Group %s is suppressed for %d seconds. Suppression ends at %d:%02d.", Controllable:GetName(), Tsuppress, self.TsuppressionOver/60, self.TsuppressionOver%60) MESSAGE:New(text, 10):ToAllIf(self.Debug) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) end @@ -1520,7 +1656,7 @@ function SUPPRESSION:_Run(fin, speed, formation, wait) local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName())) end - self:T2(SUPPRESSION.id..string.format("Number of waypoints %d", nx)) + self:T2(self.lid..string.format("Number of waypoints %d", nx)) for i=1,nx-2 do local x=dx*i @@ -1529,13 +1665,13 @@ function SUPPRESSION:_Run(fin, speed, formation, wait) wp[#wp+1]=coord:WaypointGround(speed, formation) tasks[#tasks+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, false) - self:T2(SUPPRESSION.id..string.format("%d x = %4.1f", i, x)) + self:T2(self.lid..string.format("%d x = %4.1f", i, x)) if self.Debug then local MarkerID=coord:MarkToAll(string.format("Waypoing %d of group %s", #wp, self.Controllable:GetName())) end end - self:T2(SUPPRESSION.id..string.format("Total distance: %4.1f", dist)) + self:T2(self.lid..string.format("Total distance: %4.1f", dist)) -- Final waypoint. wp[#wp+1]=fin:WaypointGround(speed, formation) @@ -1584,7 +1720,7 @@ function SUPPRESSION._Passing_Waypoint(group, Fsm, i, final) local text=string.format("Group %s passing waypoint %d (final=%s)", group:GetName(), i, tostring(final)) MESSAGE:New(text,10):ToAllIf(Fsm.Debug) if Fsm.Debug then - env.info(SUPPRESSION.id..text) + env.info(self.lid..text) end if final then @@ -1629,7 +1765,7 @@ function SUPPRESSION:_SearchHideout() -- Place markers on every possible scenery object. local MarkerID=SceneryObject:GetCoordinate():MarkToAll(string.format("%s scenery object %s", self.Controllable:GetName(),SceneryObject:GetTypeName())) local text=string.format("%s scenery: %s, Coord %s", self.Controllable:GetName(), SceneryObject:GetTypeName(), SceneryObject:GetCoordinate():ToStringLLDMS()) - self:T2(SUPPRESSION.id..text) + self:T2(self.lid..text) end -- Add to table. @@ -1642,7 +1778,7 @@ function SUPPRESSION:_SearchHideout() if #hideouts>0 then -- Debug info. - self:T(SUPPRESSION.id.."Number of hideouts "..#hideouts) + self:T(self.lid.."Number of hideouts "..#hideouts) -- Sort results table wrt number of hits. local _sort = function(a,b) return a.distance < b.distance end @@ -1655,7 +1791,7 @@ function SUPPRESSION:_SearchHideout() Hideout=hideouts[1].object:GetCoordinate() else - self:E(SUPPRESSION.id.."No hideouts found!") + self:E(self.lid.."No hideouts found!") end return Hideout @@ -1685,7 +1821,7 @@ function SUPPRESSION:_GetLife() local groupstrength=#units/self.IniGroupStrength*100 - self.T2(SUPPRESSION.id..string.format("Group %s _GetLife nunits = %d", self.Controllable:GetName(), #units)) + self.T2(self.lid..string.format("Group %s _GetLife nunits = %d", self.Controllable:GetName(), #units)) for _,unit in pairs(units) do @@ -1702,7 +1838,7 @@ function SUPPRESSION:_GetLife() life_ave=life_ave+life if self.Debug then local text=string.format("n=%02d: Life = %3.1f, Life0 = %3.1f, min=%3.1f, max=%3.1f, ave=%3.1f, group=%3.1f", n, unit:GetLife(), unit:GetLife0(), life_min, life_max, life_ave/n,groupstrength) - self:T2(SUPPRESSION.id..text) + self:T2(self.lid..text) end end @@ -1795,13 +1931,13 @@ function SUPPRESSION:_SetROE(roe) elseif roe==SUPPRESSION.ROE.Return then group:OptionROEReturnFire() else - self:E(SUPPRESSION.id.."Unknown ROE requested: "..tostring(roe)) + self:E(self.lid.."Unknown ROE requested: "..tostring(roe)) group:OptionROEOpenFire() self.CurrentROE=SUPPRESSION.ROE.Free end local text=string.format("Group %s now has ROE %s.", self.Controllable:GetName(), self.CurrentROE) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) end --- Sets the alarm state of the group and updates the current alarm state variable. @@ -1824,13 +1960,13 @@ function SUPPRESSION:_SetAlarmState(state) elseif state==SUPPRESSION.AlarmState.Red then group:OptionAlarmStateRed() else - self:E(SUPPRESSION.id.."Unknown alarm state requested: "..tostring(state)) + self:E(self.lid.."Unknown alarm state requested: "..tostring(state)) group:OptionAlarmStateAuto() self.CurrentAlarmState=SUPPRESSION.AlarmState.Auto end local text=string.format("Group %s now has Alarm State %s.", self.Controllable:GetName(), self.CurrentAlarmState) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) end --- Print event-from-to string to DCS log file. @@ -1841,7 +1977,7 @@ end -- @param #string To To state. function SUPPRESSION:_EventFromTo(BA, Event, From, To) local text=string.format("\n%s: %s EVENT %s: %s --> %s", BA, self.Controllable:GetName(), Event, From, To) - self:T2(SUPPRESSION.id..text) + self:T2(self.lid..text) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 8c855be45..b85d519b2 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1046,6 +1046,45 @@ function GROUP:GetFuel() end +--- Get the number of shells, rockets, bombs and missiles the whole group currently has. +-- @param #GROUP self +-- @return #number Total amount of ammo the group has left. This is the sum of shells, rockets, bombs and missiles of all units. +-- @return #number Number of shells left. +-- @return #number Number of rockets left. +-- @return #number Number of bombs left. +-- @return #number Number of missiles left. +function GROUP:GetAmmunition() + self:F( self.ControllableName ) + + local DCSControllable = self:GetDCSObject() + + local Ntot=0 + local Nshells=0 + local Nrockets=0 + local Nmissiles=0 + + if DCSControllable then + + -- Loop over units. + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + + -- Get ammo of the unit + local ntot, nshells, nrockets, nmissiles = Unit:GetAmmunition() + + Ntot=Ntot+ntot + Nshells=Nshells+nshells + Nrockets=Nrockets+nrockets + Nmissiles=Nmissiles+nmissiles + + end + + end + + return Ntot, Nshells, Nrockets, Nmissiles +end + + do -- Is Zone methods --- Returns true if all units of the group are within a @{Zone}. diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 6987e5ad5..7f974be19 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -493,6 +493,94 @@ function UNIT:GetAmmo() return nil end +--- Get the number of ammunition and in particular the number of shells, rockets, bombs and missiles a unit currently has. +-- @param #UNIT self +-- @return #number Total amount of ammo the unit has left. This is the sum of shells, rockets, bombs and missiles. +-- @return #number Number of shells left. +-- @return #number Number of rockets left. +-- @return #number Number of bombs left. +-- @return #number Number of missiles left. +function UNIT:GetAmmunition() + + -- Init counter. + local nammo=0 + local nshells=0 + local nrockets=0 + local nmissiles=0 + local nbombs=0 + + local unit=self + + -- Get ammo table. + local ammotable=unit:GetAmmo() + + if ammotable then + + local weapons=#ammotable + + -- Loop over all weapons. + for w=1,weapons do + + -- Number of current weapon. + local Nammo=ammotable[w]["count"] + + -- Type name of current weapon. + local Tammo=ammotable[w]["desc"]["typeName"] + + local _weaponString = UTILS.Split(Tammo,"%.") + local _weaponName = _weaponString[#_weaponString] + + -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3 + local Category=ammotable[w].desc.category + + -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6 + local MissileCategory=nil + if Category==Weapon.Category.MISSILE then + MissileCategory=ammotable[w].desc.missileCategory + end + + -- We are specifically looking for shells or rockets here. + if Category==Weapon.Category.SHELL then + + -- Add up all shells. + nshells=nshells+Nammo + + elseif Category==Weapon.Category.ROCKET then + + -- Add up all rockets. + nrockets=nrockets+Nammo + + elseif Category==Weapon.Category.BOMB then + + -- Add up all rockets. + nbombs=nbombs+Nammo + + elseif Category==Weapon.Category.MISSILE then + + -- Add up all cruise missiles (category 5) + if MissileCategory==Weapon.MissileCategory.AAM then + nmissiles=nmissiles+Nammo + elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then + nmissiles=nmissiles+Nammo + elseif MissileCategory==Weapon.MissileCategory.BM then + nmissiles=nmissiles+Nammo + elseif MissileCategory==Weapon.MissileCategory.OTHER then + nmissiles=nmissiles+Nammo + end + + end + + end + end + + -- Total amount of ammunition. + nammo=nshells+nrockets+nmissiles+nbombs + + return nammo, nshells, nrockets, nbombs, nmissiles +end + + + --- Returns the unit sensors. -- @param #UNIT self -- @return DCS#Unit.Sensors From 5b9978e390c67ab3dbe8e2ac93bc2caf36758a8b Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 17 Dec 2019 00:09:24 +0100 Subject: [PATCH 403/485] A2A Dispatcher fixes - Increased engage distance. --- .../Moose/AI/AI_A2A_Dispatcher.lua | 19 ++-- Moose Development/Moose/AI/AI_Air_Engage.lua | 91 ++++++++----------- Moose Development/Moose/AI/AI_CAP.lua | 2 +- 3 files changed, 47 insertions(+), 65 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 26da1d54c..b780591fe 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -3045,8 +3045,7 @@ do -- AI_A2A_DISPATCHER 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 + if DefenderTask.Fsm:Is( "Returning" ) or DefenderTask.Fsm:Is( "Patrolling" ) then Friendlies = Friendlies or {} Friendlies[Friendly] = Friendly DefenderCount = DefenderCount + Friendly:GetSize() @@ -3315,19 +3314,20 @@ do -- AI_A2A_DISPATCHER -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. -- @param #table Defenders Defenders table. function AI_A2A_DISPATCHER:onafterENGAGE( From, Event, To, AttackerDetection, Defenders ) - - self:F("ENGAGING "..tostring(AttackerDetection.Name)) + self:F("ENGAGING Detection ID="..tostring(AttackerDetection.ID)) if Defenders then for DefenderID, Defender in pairs( Defenders ) do local Fsm = self:GetDefenderTaskFsm( Defender ) + Fsm:EngageRoute( AttackerDetection.Set ) -- Engage on the TargetSetUnit - + self:SetDefenderTaskTarget( Defender, AttackerDetection ) end + end end @@ -3341,7 +3341,7 @@ do -- AI_A2A_DISPATCHER -- @param #table DefenderFriendlies Friendly defenders. function AI_A2A_DISPATCHER:onafterGCI( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies ) - self:F("GCI "..tostring(AttackerDetection.Name)) + self:F("GCI Detection ID="..tostring(AttackerDetection.ID)) self:F( { From, Event, To, AttackerDetection.Index, DefendersMissing, DefenderFriendlies } ) @@ -3606,8 +3606,7 @@ do -- AI_A2A_DISPATCHER -- 2. There is sufficient fuel -- 3. There is sufficient ammo -- 4. The plane is not damaged - if DefenderGroups and DetectedItem.IsDetected == true then - + if DefenderGroups and DetectedItem.IsDetected == true then return DefenderGroups end @@ -3686,13 +3685,13 @@ do -- AI_A2A_DISPATCHER local TaskReport = REPORT:New() - local Report = REPORT:New( "Tactical Overviews" ) + local Report = REPORT:New( "Tactical Overview:" ) local DefenderGroupCount = 0 -- Now that all obsolete tasks are removed, loop through the detected targets. --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT diff --git a/Moose Development/Moose/AI/AI_Air_Engage.lua b/Moose Development/Moose/AI/AI_Air_Engage.lua index bbfd82497..806376f83 100644 --- a/Moose Development/Moose/AI/AI_Air_Engage.lua +++ b/Moose Development/Moose/AI/AI_Air_Engage.lua @@ -334,9 +334,8 @@ end -- @param #string Event The Event string. -- @param #string To The To State string. function AI_AIR_ENGAGE:onafterEngage( AIGroup, From, Event, To ) - + -- TODO: This function is overwritten below! self:HandleEvent( EVENTS.Dead ) - end -- todo: need to fix this global function @@ -349,10 +348,10 @@ end -- @param #string Event The Event string. -- @param #string To The To State string. function AI_AIR_ENGAGE:onbeforeEngage( AIGroup, From, Event, To ) - if self.Accomplished == true then return false - end + end + return true end --- onafter event handler for Abort event. @@ -405,14 +404,10 @@ end --- @param Wrapper.Group#GROUP AIControllable function AI_AIR_ENGAGE.___EngageRoute( AIGroup, Fsm, AttackSetUnit ) - - Fsm:I( { "AI_AIR_ENGAGE.___EngageRoute:", AIGroup:GetName() } ) + Fsm:I(string.format("AI_AIR_ENGAGE.___EngageRoute: %s", tostring(AIGroup:GetName()))) - if AIGroup:IsAlive() then - Fsm:__EngageRoute( Fsm.TaskDelay, AttackSetUnit ) - - --local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) - --AIGroup:SetTask( Task ) + if AIGroup and AIGroup:IsAlive() then + Fsm:__EngageRoute( Fsm.TaskDelay or 0.1, AttackSetUnit ) end end @@ -423,7 +418,6 @@ end -- @param #string Event The Event string. -- @param #string To The To State string. function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) - self:I( { DefenderGroup, From, Event, To, AttackSetUnit } ) local DefenderGroupName = DefenderGroup:GetName() @@ -450,39 +444,33 @@ function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, Attac TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) - local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) - if TargetDistance <= EngageDistance * 3 then + -- TODO: A factor of * 3 is way too close. This causes the AI not to engange until merged sometimes! + if TargetDistance <= EngageDistance * 9 then + self:I(string.format("AI_AIR_ENGAGE onafterEngageRoute ==> __Engage - target distance = %.1f km", TargetDistance/1000)) self:__Engage( 0.1, AttackSetUnit ) else + + self:I(string.format("FF AI_AIR_ENGAGE onafterEngageRoute ==> Routing - target distance = %.1f km", TargetDistance/1000)) local EngageRoute = {} local AttackTasks = {} --- Calculate the target route point. - local FromWP = DefenderCoord:WaypointAir( - self.PatrolAltType or "RADIO", - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - EngageSpeed, - true - ) + 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 = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) - local ToWP = DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( - self.PatrolAltType or "RADIO", - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - EngageSpeed, - true - ) + local ToCoord=DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ) + + local ToWP = ToCoord:WaypointAir(self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, EngageSpeed, true) EngageRoute[#EngageRoute+1] = ToWP @@ -492,11 +480,12 @@ function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, Attac DefenderGroup:OptionROEReturnFire() DefenderGroup:OptionROTEvadeFire() - DefenderGroup:Route( EngageRoute, self.TaskDelay ) + DefenderGroup:Route( EngageRoute, self.TaskDelay or 0.1 ) end end else + -- TODO: This will make an A2A Dispatcher CAP flight to return rather than going back to patrolling! self:I( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() end @@ -506,10 +495,11 @@ end --- @param Wrapper.Group#GROUP AIControllable function AI_AIR_ENGAGE.___Engage( AIGroup, Fsm, AttackSetUnit ) - Fsm:I( { "AI_AIR_ENGAGE.___Engage:", AIGroup:GetName() } ) + Fsm:I(string.format("AI_AIR_ENGAGE.___Engage: %s", tostring(AIGroup:GetName()))) - if AIGroup:IsAlive() then - Fsm:__Engage( Fsm.TaskDelay, AttackSetUnit ) + if AIGroup and AIGroup:IsAlive() then + local delay=Fsm.TaskDelay or 0.1 + Fsm:__Engage(delay, AttackSetUnit) end end @@ -520,7 +510,6 @@ end -- @param #string Event The Event string. -- @param #string To The To State string. function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) - self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) local DefenderGroupName = DefenderGroup:GetName() @@ -532,7 +521,7 @@ function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetU if AttackCount > 0 then - if DefenderGroup:IsAlive() then + if DefenderGroup and DefenderGroup:IsAlive() then local EngageAltitude = math.random( self.EngageFloorAltitude or 500, self.EngageCeilingAltitude or 1000 ) local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) @@ -544,33 +533,25 @@ function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetU TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) local EngageRoute = {} local AttackTasks = {} - local FromWP = DefenderCoord:WaypointAir( - self.EngageAltType or "RADIO", - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - EngageSpeed, - true - ) + local FromWP = DefenderCoord:WaypointAir(self.EngageAltType 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 = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) - local ToWP = DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ):WaypointAir( - self.EngageAltType or "RADIO", - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - EngageSpeed, - true - ) + local ToCoord=DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ) + + local ToWP = ToCoord:WaypointAir(self.EngageAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, EngageSpeed, true) EngageRoute[#EngageRoute+1] = ToWP - if TargetDistance <= EngageDistance * 3 then + -- TODO: A factor of * 3 this way too low. This causes the AI NOT to engage until very close or even merged sometimes. Some A2A missiles have a much longer range! Needs more frequent updates of the task! + if TargetDistance <= EngageDistance * 9 then local AttackUnitTasks = self:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) -- Polymorphic @@ -579,7 +560,8 @@ function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetU self:Return() return else - self:I( DefenderGroupName .. ": Engaging targets " ) + local text=string.format("%s: Engaging targets at distance %.2f NM", DefenderGroupName, UTILS.MetersToNM(TargetDistance)) + self:I(text) DefenderGroup:OptionROEOpenFire() DefenderGroup:OptionROTEvadeFire() DefenderGroup:OptionKeepWeaponsOnThreat() @@ -591,10 +573,11 @@ function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetU AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_AIR_ENGAGE.___Engage", self, AttackSetUnit ) EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) - DefenderGroup:Route( EngageRoute, self.TaskDelay ) + DefenderGroup:Route( EngageRoute, self.TaskDelay or 0.1 ) end else + -- TODO: This will make an A2A Dispatcher CAP flight to return rather than going back to patrolling! self:I( DefenderGroupName .. ": No targets found -> returning.") self:Return() return @@ -605,9 +588,9 @@ end function AI_AIR_ENGAGE.Resume( AIEngage, Fsm ) AIEngage:F( { "Resume:", AIEngage:GetName() } ) - if AIEngage:IsAlive() then - Fsm:__Reset( Fsm.TaskDelay ) - Fsm:__EngageRoute( Fsm.TaskDelay, Fsm.AttackSetUnit ) + if AIEngage and AIEngage:IsAlive() then + Fsm:__Reset( Fsm.TaskDelay or 0.1 ) + Fsm:__EngageRoute( Fsm.TaskDelay or 0.2, Fsm.AttackSetUnit ) end end diff --git a/Moose Development/Moose/AI/AI_CAP.lua b/Moose Development/Moose/AI/AI_CAP.lua index 1ffdbc23b..5366db4b4 100644 --- a/Moose Development/Moose/AI/AI_CAP.lua +++ b/Moose Development/Moose/AI/AI_CAP.lua @@ -421,7 +421,7 @@ end -- @param #string To The To State string. function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) - if Controllable:IsAlive() then + if Controllable and Controllable:IsAlive() then local EngageRoute = {} From 07edcd7e73a7fa341d6abbf483972fdd315d2b21 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 18 Dec 2019 23:00:42 +0100 Subject: [PATCH 404/485] SCHEDULER Clean up --- .../Moose/Core/ScheduleDispatcher.lua | 101 +++++++++++------- Moose Development/Moose/Core/Scheduler.lua | 18 +++- 2 files changed, 77 insertions(+), 42 deletions(-) diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index c85943d75..01b8a7000 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -53,6 +53,19 @@ SCHEDULEDISPATCHER = { Schedule = nil, } +--- Player data table holding all important parameters of each player. +-- @type SCHEDULEDISPATCHER.ScheduleData +-- @field #function Function The schedule function to be called. +-- @field #table Arguments Schedule function arguments. +-- @field #number Start Start time in seconds. +-- @field #number Repeat Repeat time intervall in seconds. +-- @field #number Randomize Randomization factor [0,1]. +-- @field #number Stop Stop time in seconds. +-- @field #number StartTime Time in seconds when the scheduler is created. +-- @field #number ScheduleID Schedule ID. +-- @field #function CallHandler Function to be passed to the DCS timer.scheduleFunction(). +-- @field #boolean ShowTrace If true, show tracing info. + --- Create a new schedule dispatcher object. -- @param #SCHEDULEDISPATCHER self -- @return #SCHEDULEDISPATCHER self @@ -76,7 +89,7 @@ end -- @param #number Stop Stop time in seconds. -- @param #number TraceLevel Trace level [0,3]. -- @param Core.Fsm#FSM Fsm Finite state model. --- @return #table Call ID or nil. +-- @return #string Call ID or nil. function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm ) self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm } ) @@ -85,9 +98,10 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr -- Create ID. local CallID = self.CallID .. "#" .. ( Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID() or "" ) or "" + + self:T2(string.format("Adding schedule #%d CallID=%s", self.CallID, CallID)) - -- Initialize the ObjectSchedulers array, which is a weakly coupled table. - -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. + -- Initialize PersistentSchedulers self.PersistentSchedulers = self.PersistentSchedulers or {} -- Initialize the ObjectSchedulers array, which is a weakly coupled table. @@ -104,11 +118,11 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr self.Schedule = self.Schedule or setmetatable( {}, { __mode = "k" } ) self.Schedule[Scheduler] = self.Schedule[Scheduler] or {} - self.Schedule[Scheduler][CallID] = {} + self.Schedule[Scheduler][CallID] = {} --#SCHEDULEDISPATCHER.ScheduleData self.Schedule[Scheduler][CallID].Function = ScheduleFunction self.Schedule[Scheduler][CallID].Arguments = ScheduleArguments self.Schedule[Scheduler][CallID].StartTime = timer.getTime() + ( Start or 0 ) - self.Schedule[Scheduler][CallID].Start = Start + .1 + self.Schedule[Scheduler][CallID].Start = Start + 0.1 self.Schedule[Scheduler][CallID].Repeat = Repeat or 0 self.Schedule[Scheduler][CallID].Randomize = Randomize or 0 self.Schedule[Scheduler][CallID].Stop = Stop @@ -150,6 +164,7 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr self:T3( self.Schedule[Scheduler][CallID] ) + --- Function passed to the DCS timer.scheduleFunction() self.Schedule[Scheduler][CallID].CallHandler = function( Params ) local CallID = Params.CallID @@ -166,7 +181,8 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr return errmsg end - local Scheduler = self.ObjectSchedulers[CallID] + -- Get object or persistant scheduler object. + local Scheduler = self.ObjectSchedulers[CallID] --Core.Scheduler#SCHEDULER if not Scheduler then Scheduler = self.PersistentSchedulers[CallID] end @@ -175,20 +191,24 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr if Scheduler then - local MasterObject = tostring(Scheduler.MasterObject) - local Schedule = self.Schedule[Scheduler][CallID] + local MasterObject = tostring(Scheduler.MasterObject) + + -- Schedule object. + local Schedule = self.Schedule[Scheduler][CallID] --#SCHEDULEDISPATCHER.ScheduleData --self:T3( { Schedule = Schedule } ) local SchedulerObject = Scheduler.MasterObject --Scheduler.SchedulerObject Now is this the Maste or Scheduler object? - local ScheduleFunction = Schedule.Function + local ShowTrace = Scheduler.ShowTrace + + local ScheduleFunction = Schedule.Function local ScheduleArguments = Schedule.Arguments or {} - local Start = Schedule.Start - local Repeat = Schedule.Repeat or 0 - local Randomize = Schedule.Randomize or 0 - local Stop = Schedule.Stop or 0 - local ScheduleID = Schedule.ScheduleID - local ShowTrace = Scheduler.ShowTrace + local Start = Schedule.Start + local Repeat = Schedule.Repeat or 0 + local Randomize = Schedule.Randomize or 0 + local Stop = Schedule.Stop or 0 + local ScheduleID = Schedule.ScheduleID + local Prefix = ( Repeat == 0 ) and "--->" or "+++>" @@ -215,24 +235,20 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr local CurrentTime = timer.getTime() local StartTime = Schedule.StartTime - self:F3( { Master = MasterObject, CurrentTime = CurrentTime, StartTime = StartTime, Start = Start, Repeat = Repeat, Randomize = Randomize, Stop = Stop } ) + -- Debug info. + self:F3( { CallID=CallID, ScheduleID=ScheduleID, Master = MasterObject, CurrentTime = CurrentTime, StartTime = StartTime, Start = Start, Repeat = Repeat, Randomize = Randomize, Stop = Stop } ) if Status and (( Result == nil ) or ( Result and Result ~= false ) ) then + if Repeat ~= 0 and ( ( Stop == 0 ) or ( Stop ~= 0 and CurrentTime <= StartTime + Stop ) ) then - local ScheduleTime = - CurrentTime + - Repeat + - math.random( - - ( Randomize * Repeat / 2 ), - ( Randomize * Repeat / 2 ) - ) + - 0.0001 -- Accuracy + local ScheduleTime = CurrentTime + Repeat + math.random(- ( Randomize * Repeat / 2 ), ( Randomize * Repeat / 2 )) + 0.0001 -- Accuracy --self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) return ScheduleTime -- returns the next time the function needs to be called. else self:Stop( Scheduler, CallID ) end + else self:Stop( Scheduler, CallID ) end @@ -265,24 +281,27 @@ end -- @param #SCHEDULEDISPATCHER self -- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. -- @param #table CallID (Optional) Call ID. --- @param #table CallID Call ID. -- @param #string Info (Optional) Debug info. function SCHEDULEDISPATCHER:Start( Scheduler, CallID, Info ) self:F2( { Start = CallID, Scheduler = Scheduler } ) if CallID then - local Schedule = self.Schedule[Scheduler] + local Schedule = self.Schedule[Scheduler][CallID] --#SCHEDULEDISPATCHER.ScheduleData -- Only start when there is no ScheduleID defined! -- This prevents to "Start" the scheduler twice with the same CallID... - if not Schedule[CallID].ScheduleID then - Schedule[CallID].StartTime = timer.getTime() -- Set the StartTime field to indicate when the scheduler started. - Schedule[CallID].ScheduleID = timer.scheduleFunction( - Schedule[CallID].CallHandler, - { CallID = CallID, Info = Info }, - timer.getTime() + Schedule[CallID].Start - ) + if not Schedule.ScheduleID then + + -- Current time in seconds. + local Tnow=timer.getTime() + + Schedule.StartTime = Tnow -- Set the StartTime field to indicate when the scheduler started. + + -- Start DCS schedule function https://wiki.hoggitworld.com/view/DCS_func_scheduleFunction + Schedule.ScheduleID = timer.scheduleFunction(Schedule.CallHandler, { CallID = CallID, Info = Info }, Tnow + Schedule.Start) + + self:T(string.format("Starting scheduledispatcher Call ID=%s ==> Schedule ID=%s", tostring(CallID), tostring(Schedule.ScheduleID))) end else @@ -304,12 +323,20 @@ function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) if CallID then - local Schedule = self.Schedule[Scheduler] + local Schedule = self.Schedule[Scheduler][CallID] --#SCHEDULEDISPATCHER.ScheduleData -- Only stop when there is a ScheduleID defined for the CallID. So, when the scheduler was stopped before, do nothing. - if Schedule[CallID].ScheduleID then - timer.removeFunction( Schedule[CallID].ScheduleID ) - Schedule[CallID].ScheduleID = nil + if Schedule.ScheduleID then + + self:T(string.format("scheduledispatcher stopping scheduler CallID=%s, ScheduleID=%s", tostring(CallID), tostring(Schedule.ScheduleID))) + + -- Remove schedule function https://wiki.hoggitworld.com/view/DCS_func_removeFunction + timer.removeFunction(Schedule.ScheduleID) + + Schedule.ScheduleID = nil + + else + self:E(string.format("Error no ScheduleID for CallID=%s", tostring(CallID))) end else diff --git a/Moose Development/Moose/Core/Scheduler.lua b/Moose Development/Moose/Core/Scheduler.lua index e5ef5960f..781b90ebe 100644 --- a/Moose Development/Moose/Core/Scheduler.lua +++ b/Moose Development/Moose/Core/Scheduler.lua @@ -221,7 +221,7 @@ function SCHEDULER:New( MasterObject, SchedulerFunction, SchedulerArguments, Sta self.ShowTrace = false if SchedulerFunction then - ScheduleID = self:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, 4 ) + ScheduleID = self:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, 3 ) end return self, ScheduleID @@ -243,13 +243,17 @@ function SCHEDULER:Schedule( MasterObject, SchedulerFunction, SchedulerArguments self:F2( { Start, Repeat, RandomizeFactor, Stop } ) self:T3( { SchedulerArguments } ) + -- Debug info. local ObjectName = "-" if MasterObject and MasterObject.ClassName and MasterObject.ClassID then ObjectName = MasterObject.ClassName .. MasterObject.ClassID end self:F3( { "Schedule :", ObjectName, tostring( MasterObject ), Start, Repeat, RandomizeFactor, Stop } ) + + -- Set master object. self.MasterObject = MasterObject + -- Add schedule. local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( self, SchedulerFunction, @@ -269,32 +273,36 @@ end --- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided. -- @param #SCHEDULER self --- @param #table ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. +-- @param #string ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. function SCHEDULER:Start( ScheduleID ) self:F3( { ScheduleID } ) + self:T(string.format("Starting scheduler ID=%s", tostring(ScheduleID))) _SCHEDULEDISPATCHER:Start( self, ScheduleID ) end --- Stops the schedules or a specific schedule if a valid ScheduleID is provided. -- @param #SCHEDULER self --- @param #table ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. +-- @param #string ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. function SCHEDULER:Stop( ScheduleID ) self:F3( { ScheduleID } ) + self:T(string.format("Stopping scheduler ID=%s", tostring(ScheduleID))) _SCHEDULEDISPATCHER:Stop( self, ScheduleID ) end --- Removes a specific schedule if a valid ScheduleID is provided. -- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +-- @param #string ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. function SCHEDULER:Remove( ScheduleID ) self:F3( { ScheduleID } ) - _SCHEDULEDISPATCHER:Remove( self, ScheduleID ) + self:T(string.format("Removing scheduler ID=%s", tostring(ScheduleID))) + _SCHEDULEDISPATCHER:RemoveSchedule( self, ScheduleID ) end --- Clears all pending schedules. -- @param #SCHEDULER self function SCHEDULER:Clear() self:F3( ) + self:T(string.format("Clearing scheduler")) _SCHEDULEDISPATCHER:Clear( self ) end From f48f53fb376d6473ff5298981fd27eb4a536bfec Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 19 Dec 2019 13:11:58 +0100 Subject: [PATCH 405/485] Update RAT.lua Fixed bug with uncontrolled spawning. --- Moose Development/Moose/Functional/RAT.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 21af54d2e..74c9d8155 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -5173,6 +5173,7 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take if self.uncontrolled then -- This is used in the SPAWN:SpawnWithIndex() function. Some values are overwritten there! self.SpawnUnControlled=true + SpawnTemplate.uncontrolled=true end -- Number of units in the group. With grouping this can actually differ from the template group size! From b9fd6c0e3136dfe33699c256edb3b7f2898a2825 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 19 Dec 2019 22:59:37 +0100 Subject: [PATCH 406/485] Update RAT.lua --- Moose Development/Moose/Functional/RAT.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 74c9d8155..7492a5d4d 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -550,7 +550,7 @@ RAT.id="RAT | " --- RAT version. -- @list version RAT.version={ - version = "2.3.8", + version = "2.3.9", print = true, } From 389931fbf0c99d60a0b705724e9b9b4033e58d97 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 21 Dec 2019 01:09:59 +0100 Subject: [PATCH 407/485] AI_PATROL - output fix --- Moose Development/Moose/AI/AI_Patrol.lua | 6 +++--- Moose Development/Moose/Functional/Suppression.lua | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Patrol.lua b/Moose Development/Moose/AI/AI_Patrol.lua index 031d9aae8..dcb94b99d 100644 --- a/Moose Development/Moose/AI/AI_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Patrol.lua @@ -636,7 +636,7 @@ function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To ) self.Controllable:OnReSpawn( function( PatrolGroup ) - self:E( "ReSpawn" ) + self:T( "ReSpawn" ) self:__Reset( 1 ) self:__Route( 5 ) end @@ -741,7 +741,7 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) -- This will make the plane fly immediately to the patrol zone. if self.Controllable:InAir() == false then - self:E( "Not in the air, finding route path within PatrolZone" ) + self:T( "Not in the air, finding route path within PatrolZone" ) local CurrentVec2 = self.Controllable:GetVec2() --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() @@ -756,7 +756,7 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) ) PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint else - self:E( "In the air, finding route path within PatrolZone" ) + self:T( "In the air, finding route path within PatrolZone" ) local CurrentVec2 = self.Controllable:GetVec2() --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua index 1c8c56092..098efeb20 100644 --- a/Moose Development/Moose/Functional/Suppression.lua +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -868,13 +868,15 @@ end -- @param #boolean message Send message to all players. function SUPPRESSION:StatusReport(message) - local name=self.Controllable:GetName() - local nunits=#self.Controllable:GetUnits() + local group=self.Controllable --Wrapper.Group#GROUP + + local nunits=group:CountAliveUnits() local roe=self.CurrentROE local state=self.CurrentAlarmState local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife() - local at=self.Controllable:GetAmmunition() + local ammotot=self.Controllable:GetAmmunition() + --[[ local text=string.format("Status of group %s\n", name) text=text..string.format("Number of units: %d of %d\n", nunits, self.IniGroupStrength) text=text..string.format("Current state: %s\n", self:GetState()) @@ -887,6 +889,10 @@ function SUPPRESSION:StatusReport(message) text=text..string.format("Life ave0: %3.0f\n", life_ave0) text=text..string.format("Ammo tot: %d\n", at) text=text..string.format("Group strength: %3.0f", groupstrength) + ]] + + local text=string.format("State %s, Units=%d/%d, ROE=%s Alarm State=%s, Hits=%d, Life=%d/%d/%d/%d, Ammo=%d", + self:GetState(), nunits, self.IniGroupStrength, self.CurrentROE, self.CurrentAlarmState, self.Nhit, life_min, life_max, life_ave, life_ave0, ammotot) MESSAGE:New(text, 10):ToAllIf(message or self.Debug) self:I(self.lid.."\n"..text) From 833705830691999ce3fe3b07a610a78140a1b8b0 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 26 Dec 2019 23:20:55 +0100 Subject: [PATCH 408/485] CAPTURE ZONE --- Moose Development/Moose/Core/Zone.lua | 69 +++- .../Moose/Functional/ZoneCaptureCoalition.lua | 355 +++++++++--------- .../Moose/Functional/ZoneGoal.lua | 9 +- .../Moose/Functional/ZoneGoalCoalition.lua | 3 + 4 files changed, 248 insertions(+), 188 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 9dedcb35a..07c93f7b9 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -685,30 +685,44 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) local function EvaluateZone( ZoneObject ) --if ZoneObject:isExist() then --FF: isExist always returns false for SCENERY objects since DCS 2.2 and still in DCS 2.5 - if ZoneObject then + if ZoneObject then + local ObjectCategory = ZoneObject:getCategory() - if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or - (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then + + 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 + -- Anythink found is included. Include = true else + -- Check if found object is in specified categories. 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() + + -- This coalition is inside the zone. 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() local SceneryName = ZoneObject:getName() @@ -716,21 +730,29 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) self.ScanData.Scenery[SceneryType][SceneryName] = SCENERY:Register( SceneryName, ZoneObject ) self:F2( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } ) end + end + return true end + -- Search objects. world.searchObjects( ObjectCategories, SphereSearch, EvaluateZone ) end - +--- Count the number of different coalitions inside the zone. +-- @param #ZONE_RADIUS self +-- @return #table Table of DCS units and DCS statics inside the zone. function ZONE_RADIUS:GetScannedUnits() return self.ScanData.Units end +--- Count the number of different coalitions inside the zone. +-- @param #ZONE_RADIUS self +-- @return Core.Set#SET_UNIT Set of units and statics inside the zone. function ZONE_RADIUS:GetScannedSetUnit() local SetUnit = SET_UNIT:New() @@ -756,6 +778,9 @@ function ZONE_RADIUS:GetScannedSetUnit() end +--- Count the number of different coalitions inside the zone. +-- @param #ZONE_RADIUS self +-- @return #number Counted coalitions. function ZONE_RADIUS:CountScannedCoalitions() local Count = 0 @@ -763,14 +788,25 @@ function ZONE_RADIUS:CountScannedCoalitions() for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do Count = Count + 1 end + return Count end +--- Check if a certain coalition is inside a scanned zone. +-- @param #ZONE_RADIUS self +-- @param #number Coalition The coalition id, e.g. coalition.side.BLUE. +-- @return #boolean If true, the coalition is inside the zone. +function ZONE_RADIUS:CheckScannedCoalition( Coalition ) + if Coalition then + return self.ScanData.Coalitions[Coalition] + end + return nil +end --- Get Coalitions of the units in the Zone, or Check if there are units of the given Coalition in the Zone. --- Returns nil if there are none ot two Coalitions in the zone! +-- Returns nil if there are none to two Coalitions in the zone! -- Returns one Coalition if there are only Units of one Coalition in the Zone. --- Returns the Coalition for the given Coalition if there are units of the Coalition in the Zone +-- Returns the Coalition for the given Coalition if there are units of the Coalition in the Zone. -- @param #ZONE_RADIUS self -- @return #table function ZONE_RADIUS:GetScannedCoalition( Coalition ) @@ -795,20 +831,27 @@ function ZONE_RADIUS:GetScannedCoalition( Coalition ) end +--- Get scanned scenery type +-- @param #ZONE_RADIUS self +-- @return #table Table of DCS scenery type objects. function ZONE_RADIUS:GetScannedSceneryType( SceneryType ) return self.ScanData.Scenery[SceneryType] end +--- Get scanned scenery table +-- @param #ZONE_RADIUS self +-- @return #table Table of DCS scenery objects. function ZONE_RADIUS:GetScannedScenery() return self.ScanData.Scenery end --- Is All in Zone of Coalition? +-- Check if only the specifed coalition is inside the zone and noone else. -- @param #ZONE_RADIUS self --- @param Coalition --- @return #boolean +-- @param #number Coalition Coalition ID of the coalition which is checked to be the only one in the zone. +-- @return #boolean True, if **only** that coalition is inside the zone and no one else. -- @usage -- self.Zone:Scan() -- local IsGuarded = self.Zone:IsAllInZoneOfCoalition( self.Coalition ) @@ -820,11 +863,12 @@ end --- Is All in Zone of Other Coalition? +-- Check if only one coalition is inside the zone and the specified coalition is not the one. -- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated! -- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. -- @param #ZONE_RADIUS self --- @param Coalition --- @return #boolean +-- @param #number Coalition Coalition ID of the coalition which is not supposed to be in the zone. +-- @return #boolean True, if and only if only one coalition is inside the zone and the specified coalition is not it. -- @usage -- self.Zone:Scan() -- local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition ) @@ -836,11 +880,12 @@ end --- Is Some in Zone of Coalition? +-- Check if more than one coaltion is inside the zone and the specifed coalition is one of them. -- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated! -- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. -- @param #ZONE_RADIUS self --- @param Coalition --- @return #boolean +-- @param #number Coalition ID of the coaliton which is checked to be inside the zone. +-- @return #boolean True if more than one coalition is inside the zone and the specified coalition is one of them. -- @usage -- self.Zone:Scan() -- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 4ee91c93f..1615c70ff 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -197,8 +197,7 @@ do -- ZONE_CAPTURE_COALITION -- -- ### IMPORTANT -- - -- **Each capture zone object must have the monitoring process started specifically. - -- The monitoring process is NOT started by default!!!** + -- **Each capture zone object must have the monitoring process started specifically. The monitoring process is NOT started by default!** -- -- -- # Full Example @@ -554,175 +553,7 @@ do -- ZONE_CAPTURE_COALITION return self end - - --- @param #ZONE_CAPTURE_COALITION self - function ZONE_CAPTURE_COALITION:onenterCaptured() - - self:F({"hello"}) - - self:GetParent( self, ZONE_CAPTURE_COALITION ).onenterCaptured( self ) - - self.Goal:Achieved() - end - - - function ZONE_CAPTURE_COALITION:IsGuarded() - - local IsGuarded = self:IsAllInZoneOfCoalition( self.Coalition ) - self:F( { IsGuarded = IsGuarded } ) - return IsGuarded - end - - - function ZONE_CAPTURE_COALITION:IsEmpty() - - local IsEmpty = self:IsNoneInZone() - self:F( { IsEmpty = IsEmpty } ) - return IsEmpty - end - - - function ZONE_CAPTURE_COALITION:IsCaptured() - - local IsCaptured = self:IsAllInZoneOfOtherCoalition( self.Coalition ) - self:F( { IsCaptured = IsCaptured } ) - return IsCaptured - end - - - function ZONE_CAPTURE_COALITION:IsAttacked() - - local IsAttacked = self:IsSomeInZoneOfCoalition( self.Coalition ) - self:F( { IsAttacked = IsAttacked } ) - return IsAttacked - end - - - - --- Mark. - -- @param #ZONE_CAPTURE_COALITION self - function ZONE_CAPTURE_COALITION:Mark() - - local Coord = self:GetCoordinate() - local ZoneName = self:GetZoneName() - local State = self:GetState() - - if self.MarkRed and self.MarkBlue then - self:F( { MarkRed = self.MarkRed, MarkBlue = self.MarkBlue } ) - Coord:RemoveMark( self.MarkRed ) - Coord:RemoveMark( self.MarkBlue ) - end - - if self.Coalition == coalition.side.BLUE then - self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Blue\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) - self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Blue\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) - else - self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Red\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) - self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Red\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) - end - end - - --- Bound. - -- @param #ZONE_CAPTURE_COALITION self - function ZONE_CAPTURE_COALITION:onenterGuarded() - - --self:GetParent( self ):onenterGuarded() - - if self.Coalition == coalition.side.BLUE then - --elf.ProtectZone:BoundZone( 12, country.id.USA ) - else - --self.ProtectZone:BoundZone( 12, country.id.RUSSIA ) - end - - self:Mark() - - end - - function ZONE_CAPTURE_COALITION:onenterCaptured() - - --self:GetParent( self ):onenterCaptured() - - local NewCoalition = self:GetScannedCoalition() - self:F( { NewCoalition = NewCoalition } ) - self:SetCoalition( NewCoalition ) - - self:Mark() - self.Goal:Achieved() - end - - - function ZONE_CAPTURE_COALITION:onenterEmpty() - - --self:GetParent( self ):onenterEmpty() - - self:Mark() - end - - - function ZONE_CAPTURE_COALITION:onenterAttacked() - - --self:GetParent( self ):onenterAttacked() - - self:Mark() - end - - - --- When started, check the Coalition status. - -- @param #ZONE_CAPTURE_COALITION self - function ZONE_CAPTURE_COALITION:onafterGuard() - - --self:F({BASE:GetParent( self )}) - --BASE:GetParent( self ).onafterGuard( self ) - - if not self.SmokeScheduler then - self.SmokeScheduler = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 0.1, nil, self.StatusSmoke, self ) - end - end - - - function ZONE_CAPTURE_COALITION:IsCaptured() - - local IsCaptured = self:IsAllInZoneOfOtherCoalition( self.Coalition ) - self:F( { IsCaptured = IsCaptured } ) - return IsCaptured - end - - - function ZONE_CAPTURE_COALITION:IsAttacked() - - local IsAttacked = self:IsSomeInZoneOfCoalition( self.Coalition ) - self:F( { IsAttacked = IsAttacked } ) - return IsAttacked - end - - - --- Check status Coalition ownership. - -- @param #ZONE_CAPTURE_COALITION self - function ZONE_CAPTURE_COALITION:StatusZone() - - local State = self:GetState() - self:F( { State = self:GetState() } ) - - self:GetParent( self, ZONE_CAPTURE_COALITION ).StatusZone( self ) - - if State ~= "Guarded" and self:IsGuarded() then - self:Guard() - end - - if State ~= "Empty" and self:IsEmpty() then - self:Empty() - end - - if State ~= "Attacked" and self:IsAttacked() then - self:Attack() - end - - if State ~= "Captured" and self:IsCaptured() then - self:Capture() - end - - end --- Starts the zone capturing monitoring process. -- This process can be CPU intensive, ensure that you specify reasonable time intervals for the monitoring process. @@ -753,6 +584,7 @@ do -- ZONE_CAPTURE_COALITION if self.ScheduleStatusZone then self:ScheduleStop( self.ScheduleStatusZone ) end + self.ScheduleStatusZone = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 1.5, nil, self.StatusZone, self ) end @@ -812,7 +644,184 @@ do -- ZONE_CAPTURE_COALITION end end - - -end + + --- On after "Guard" event. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onafterGuard() + + if not self.SmokeScheduler then + self.SmokeScheduler = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 0.1, nil, self.StatusSmoke, self ) + end + + end + + --- On enter "Guarded" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onenterGuarded() + self:Mark() + end + + --- On enter "Captured" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onenterCaptured() + + -- Get new coalition. + local NewCoalition = self:GetScannedCoalition() + self:F( { NewCoalition = NewCoalition } ) + + -- Set new owner of zone. + self:SetCoalition( NewCoalition ) + + -- Update mark. + self:Mark() + + -- Goal achieved. + self.Goal:Achieved() + end + + --- On enter "Empty" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onenterEmpty() + self:Mark() + end + + --- On enter "Attacked" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onenterAttacked() + self:Mark() + end + + + + --- Check if zone is "Guarded" + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsAllInZoneOfCoalition( self.Coalition ) + function ZONE_CAPTURE_COALITION:IsGuarded() + + local IsGuarded = self:IsAllInZoneOfCoalition( self.Coalition ) + self:F( { IsGuarded = IsGuarded } ) + return IsGuarded + end + + --- Check if zone is "Empty" + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsNoneInZone() + function ZONE_CAPTURE_COALITION:IsEmpty() + + local IsEmpty = self:IsNoneInZone() + self:F( { IsEmpty = IsEmpty } ) + return IsEmpty + end + + --- Check if zone is "Captured" + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsAllInZoneOfOtherCoalition( self.Coalition ) + function ZONE_CAPTURE_COALITION:IsCaptured() + + local IsCaptured = self:IsAllInZoneOfOtherCoalition( self.Coalition ) + self:F( { IsCaptured = IsCaptured } ) + return IsCaptured + end + + --- Check if zone is "Attacked" + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsSomeInZoneOfCoalition( self.Coalition ) + function ZONE_CAPTURE_COALITION:IsAttacked() + + local IsAttacked = self:IsSomeInZoneOfCoalition( self.Coalition ) + self:F( { IsAttacked = IsAttacked } ) + return IsAttacked + end + + + --- Check if zone is captured. + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsAllInZoneOfOtherCoalition( self.Coalition ) + function ZONE_CAPTURE_COALITION:IsCaptured() + + local IsCaptured = self:IsAllInZoneOfOtherCoalition( self.Coalition ) + self:F( { IsCaptured = IsCaptured } ) + return IsCaptured + end + + --- Check if zone is attacked. + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsSomeInZoneOfCoalition( self.Coalition ) + function ZONE_CAPTURE_COALITION:IsAttacked() + + local IsAttacked = self:IsSomeInZoneOfCoalition( self.Coalition ) + self:F( { IsAttacked = IsAttacked } ) + return IsAttacked + end + + + --- Check status Coalition ownership. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:StatusZone() + + -- Get FSM state. + local State = self:GetState() + + -- Scan zone in parent class ZONE_GOAL_COALITION + self:GetParent( self, ZONE_CAPTURE_COALITION ).StatusZone( self ) + + -- Check if zone is guarded. + if State ~= "Guarded" and self:IsGuarded() then + self:Guard() + end + + -- Check if zone is empty. + if State ~= "Empty" and self:IsEmpty() then + self:Empty() + end + + -- Check if zone is attacked. + if State ~= "Attacked" and self:IsAttacked() then + self:Attack() + end + + -- Check if zone is captured. + if State ~= "Captured" and self:IsCaptured() then + self:Capture() + end + + -- + local text=string.format("CAPTURE ZONE %s: Status %s", self:GetZoneName(), State) + + local NewState = self:GetState() + if NewState~=State then + text=text..string.format(" --> %s", NewState) + end + self:I(text) + + end + + --- Update Mark on F10 map. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:Mark() + + local Coord = self:GetCoordinate() + local ZoneName = self:GetZoneName() + local State = self:GetState() + + if self.MarkRed and self.MarkBlue then + self:F( { MarkRed = self.MarkRed, MarkBlue = self.MarkBlue } ) + Coord:RemoveMark( self.MarkRed ) + Coord:RemoveMark( self.MarkBlue ) + end + + if self.Coalition == coalition.side.BLUE then + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Blue\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Blue\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + elseif self.Coalition == coalition.side.RED then + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Red\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Red\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + else + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + end + end + + +end diff --git a/Moose Development/Moose/Functional/ZoneGoal.lua b/Moose Development/Moose/Functional/ZoneGoal.lua index 67f6a872b..5a7e85397 100644 --- a/Moose Development/Moose/Functional/ZoneGoal.lua +++ b/Moose Development/Moose/Functional/ZoneGoal.lua @@ -17,10 +17,10 @@ do -- Zone --- @type ZONE_GOAL - -- @extends Core.Fsm#FSM + -- @extends Core.Zone#ZONE_RADIUS - -- Models processes that have a Goal with a defined achievement involving a Zone. + --- Models processes that have a Goal with a defined achievement involving a Zone. -- Derived classes implement the ways how the achievements can be realized. -- -- ## 1. ZONE_GOAL constructor @@ -39,7 +39,10 @@ do -- Zone -- -- @field #ZONE_GOAL ZONE_GOAL = { - ClassName = "ZONE_GOAL", + ClassName = "ZONE_GOAL", + Goal = nil, + SmokeTime = nil, + SmokeScheduler = nil, } --- ZONE_GOAL Constructor. diff --git a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua index 1de5218f3..0625aaae1 100644 --- a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua @@ -38,6 +38,8 @@ do -- ZoneGoal -- @field #ZONE_GOAL_COALITION ZONE_GOAL_COALITION = { ClassName = "ZONE_GOAL_COALITION", + Coalition = nil, + } --- @field #table ZONE_GOAL_COALITION.States @@ -104,6 +106,7 @@ do -- ZoneGoal local State = self:GetState() self:F( { State = self:GetState() } ) + env.info("scanning") self:Scan( { Object.Category.UNIT, Object.Category.STATIC } ) end From d3a3d96436d64f7c0c13f21dced64543241b4235 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 27 Dec 2019 23:03:19 +0100 Subject: [PATCH 409/485] ZONE CAP COAL --- Moose Development/Moose/Core/Database.lua | 26 +++++------ .../Moose/Functional/ZoneCaptureCoalition.lua | 45 ++++++++++++++++++- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 1a2af6d62..fa12803fc 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -187,7 +187,7 @@ end function DATABASE:AddUnit( DCSUnitName ) if not self.UNITS[DCSUnitName] then - self:I( { "Add UNIT:", DCSUnitName } ) + self:T( { "Add UNIT:", DCSUnitName } ) local UnitRegister = UNIT:Register( DCSUnitName ) self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) @@ -509,7 +509,7 @@ end function DATABASE:AddGroup( GroupName ) if not self.GROUPS[GroupName] then - self:I( { "Add GROUP:", GroupName } ) + self:T( { "Add GROUP:", GroupName } ) self.GROUPS[GroupName] = GROUP:Register( GroupName ) end @@ -521,7 +521,7 @@ end function DATABASE:AddPlayer( UnitName, PlayerName ) if PlayerName then - self:I( { "Add player for unit:", UnitName, PlayerName } ) + self:T( { "Add player for unit:", UnitName, PlayerName } ) self.PLAYERS[PlayerName] = UnitName self.PLAYERUNITS[PlayerName] = self:FindUnit( UnitName ) self.PLAYERSJOINED[PlayerName] = PlayerName @@ -533,7 +533,7 @@ end function DATABASE:DeletePlayer( UnitName, PlayerName ) if PlayerName then - self:I( { "Clean player:", PlayerName } ) + self:T( { "Clean player:", PlayerName } ) self.PLAYERS[PlayerName] = nil self.PLAYERUNITS[PlayerName] = nil end @@ -698,11 +698,11 @@ function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, Category UnitNames[#UnitNames+1] = self.Templates.Units[UnitTemplate.name].UnitName end - self:I( { Group = self.Templates.Groups[GroupTemplateName].GroupName, + self:T( { Group = self.Templates.Groups[GroupTemplateName].GroupName, Coalition = self.Templates.Groups[GroupTemplateName].CoalitionID, - Category = self.Templates.Groups[GroupTemplateName].CategoryID, - Country = self.Templates.Groups[GroupTemplateName].CountryID, - Units = UnitNames + Category = self.Templates.Groups[GroupTemplateName].CategoryID, + Country = self.Templates.Groups[GroupTemplateName].CountryID, + Units = UnitNames } ) end @@ -847,9 +847,9 @@ function DATABASE:_RegisterGroupsAndUnits() end end - self:I("Groups:") + self:T("Groups:") for GroupName, Group in pairs( self.GROUPS ) do - self:I( { "Group:", GroupName } ) + self:T( { "Group:", GroupName } ) end return self @@ -861,7 +861,7 @@ end function DATABASE:_RegisterClients() for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do - self:I( { "Register Client:", ClientName } ) + self:T( { "Register Client:", ClientName } ) self:AddClient( ClientName ) end @@ -879,7 +879,7 @@ function DATABASE:_RegisterStatics() if DCSStatic:isExist() then local DCSStaticName = DCSStatic:getName() - self:I( { "Register Static:", DCSStaticName } ) + self:T( { "Register Static:", DCSStaticName } ) self:AddStatic( DCSStaticName ) else self:E( { "Static does not exist: ", DCSStatic } ) @@ -899,7 +899,7 @@ function DATABASE:_RegisterAirbases() local DCSAirbaseName = DCSAirbase:getName() - self:I( { "Register Airbase:", DCSAirbaseName, DCSAirbase:getID() } ) + self:T( { "Register Airbase:", DCSAirbaseName, DCSAirbase:getID() } ) self:AddAirbase( DCSAirbaseName ) end end diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 1615c70ff..2cd1f7ac2 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -544,6 +544,41 @@ do -- ZONE_CAPTURE_COALITION -- @param #ZONE_CAPTURE_COALITION self -- @param #number Delay + self:AddTransition( "*", "ChangeCoalition", "*" ) + + --- ChangeCoalition Handler OnBefore for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeChangeCoalition + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number NewCoalition New coalition ID, i.e. after the change. + -- @param #number OldCoalition Old coalition ID, i.e. before the change. + -- @return #boolean + + --- ChangeCoalition Handler OnAfter for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterChangeCoalition + -- @param #ZONE_CAPTURE_COALITION self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #number NewCoalition New coalition ID, i.e. after the change. + -- @param #number OldCoalition Old coalition ID, i.e. before the change. + + --- ChangeCoalition Trigger for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] ChangeCoalition + -- @param #ZONE_CAPTURE_COALITION self + -- @param #number NewCoalition New coalition ID, i.e. after the change. + -- @param #number OldCoalition Old coalition ID, i.e. before the change. + + --- ChangeCoalition Asynchronous Trigger for ZONE_CAPTURE_COALITION + -- @function [parent=#ZONE_CAPTURE_COALITION] __ChangeCoalition + -- @param #ZONE_CAPTURE_COALITION self + -- @param #number Delay + -- @param #number NewCoalition New coalition ID, i.e. after the change. + -- @param #number OldCoalition Old coalition ID, i.e. before the change. + + -- 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 ) @@ -586,6 +621,8 @@ do -- ZONE_CAPTURE_COALITION end self.ScheduleStatusZone = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 1.5, nil, self.StatusZone, self ) + + self:StatusZone() end @@ -671,7 +708,7 @@ do -- ZONE_CAPTURE_COALITION self:F( { NewCoalition = NewCoalition } ) -- Set new owner of zone. - self:SetCoalition( NewCoalition ) + self:ChangeCoalition(NewCoalition, self.Coalition) -- Update mark. self:Mark() @@ -680,6 +717,12 @@ do -- ZONE_CAPTURE_COALITION self.Goal:Achieved() end + --- On after "ChangeCoalition" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onafterChangeCoalition(From, Event, To, NewCoalition, OldCoalition) + self:SetCoalition(NewCoalition) + end + --- On enter "Empty" state. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:onenterEmpty() From 6dd73dba44167ed9d68eed3f8f680537ca5af4d3 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 28 Dec 2019 19:53:02 +0100 Subject: [PATCH 410/485] AI_AIR Fixed task in onafterPatrolRoute function --- Moose Development/Moose/AI/AI_A2A_Patrol.lua | 14 +++++--- Moose Development/Moose/AI/AI_Air_Engage.lua | 3 +- Moose Development/Moose/AI/AI_Air_Patrol.lua | 36 +++++++------------- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index 9eedd29cc..164a5edeb 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -287,16 +287,20 @@ function AI_A2A_PATROL:onafterPatrol( AIPatrol, From, Event, To ) end - ---- @param Wrapper.Group#GROUP AIPatrol --- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol. +--- 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. +-- @param Wrapper.Group#GROUP AIPatrol The AI group. +-- @param #AI_A2A_PATROL Fsm The FSM. function AI_A2A_PATROL.PatrolRoute( AIPatrol, Fsm ) AIPatrol:F( { "AI_A2A_PATROL.PatrolRoute:", AIPatrol:GetName() } ) - if AIPatrol:IsAlive() then - Fsm:Route() + if AIPatrol and AIPatrol:IsAlive() then + if Fsm then + Fsm:Route() + else + AIPatrol:E("WARNING: FSM in AI_A2A_PATROL.PatrolRoute is nil!") + end end end diff --git a/Moose Development/Moose/AI/AI_Air_Engage.lua b/Moose Development/Moose/AI/AI_Air_Engage.lua index 806376f83..50b2e0988 100644 --- a/Moose Development/Moose/AI/AI_Air_Engage.lua +++ b/Moose Development/Moose/AI/AI_Air_Engage.lua @@ -80,7 +80,8 @@ AI_AIR_ENGAGE = { --- Creates a new AI_AIR_ENGAGE object -- @param #AI_AIR_ENGAGE self --- @param Wrapper.Group#GROUP AIGroup +-- @param AI.AI_Air#AI_AIR AI_Air The AI_AIR FSM. +-- @param Wrapper.Group#GROUP AIGroup The AI group. -- @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. diff --git a/Moose Development/Moose/AI/AI_Air_Patrol.lua b/Moose Development/Moose/AI/AI_Air_Patrol.lua index 3f133f8fa..6458c80d1 100644 --- a/Moose Development/Moose/AI/AI_Air_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Air_Patrol.lua @@ -99,7 +99,8 @@ AI_AIR_PATROL = { --- Creates a new AI_AIR_PATROL object -- @param #AI_AIR_PATROL self --- @param Wrapper.Group#GROUP AIGroup +-- @param AI.AI_Air#AI_AIR AI_Air The AI_AIR FSM. +-- @param Wrapper.Group#GROUP AIGroup The AI group. -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. -- @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. @@ -270,14 +271,15 @@ function AI_AIR_PATROL:onafterPatrol( AIPatrol, From, Event, To ) ) end ---- @param Wrapper.Group#GROUP AIPatrol --- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol. +--- 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. +-- @param Wrapper.Group#GROUP AIPatrol The AI group. +-- @param #AI_AIR_PATROL Fsm The FSM. function AI_AIR_PATROL.___PatrolRoute( AIPatrol, Fsm ) AIPatrol:F( { "AI_AIR_PATROL.___PatrolRoute:", AIPatrol:GetName() } ) - if AIPatrol:IsAlive() then + if AIPatrol and AIPatrol:IsAlive() then Fsm:PatrolRoute() end @@ -299,7 +301,7 @@ function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) end - if AIPatrol:IsAlive() then + if AIPatrol and AIPatrol:IsAlive() then local PatrolRoute = {} @@ -316,13 +318,7 @@ function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) local speedkmh=ToTargetSpeed - local FromWP = CurrentCoord:WaypointAir( - self.PatrolAltType or "RADIO", - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) + local FromWP = CurrentCoord:WaypointAir(self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, true) PatrolRoute[#PatrolRoute+1] = FromWP if self.racetrack then @@ -360,7 +356,8 @@ function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) local taskOrbit=AIPatrol:TaskOrbit(c1, altitude, UTILS.KmphToMps(speedkmh), c2) -- Task function to redo the patrol at other random position. - local taskPatrol=AIPatrol:TaskFunction("AI_A2A_PATROL.PatrolRoute", self) + --local taskPatrol=AIPatrol:TaskFunction("AI_A2A_PATROL.PatrolRoute", self) + local taskPatrol=AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute", self) -- Controlled task with task condition. local taskCond=AIPatrol:TaskCondition(nil, nil, nil, nil, duration, nil) @@ -372,18 +369,11 @@ function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) else --- Create a route point of type air. - local ToWP = ToTargetCoord:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - + local ToWP = ToTargetCoord:WaypointAir(self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, true) PatrolRoute[#PatrolRoute+1] = ToWP local Tasks = {} - Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_AIR_PATROL.___PatrolRoute", self ) + Tasks[#Tasks+1] = AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute", self) PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) end @@ -401,7 +391,7 @@ end function AI_AIR_PATROL.Resume( AIPatrol, Fsm ) AIPatrol:F( { "AI_AIR_PATROL.Resume:", AIPatrol:GetName() } ) - if AIPatrol:IsAlive() then + if AIPatrol and AIPatrol:IsAlive() then Fsm:__Reset( Fsm.TaskDelay ) Fsm:__PatrolRoute( Fsm.TaskDelay ) end From 8b506a2f20dcf9cadc77e1f84d3c5bc1ea2f1b2c Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 28 Dec 2019 23:59:39 +0100 Subject: [PATCH 411/485] ZONE_CAPTURE_COALITON & RANGE --- Moose Development/Moose/Core/Zone.lua | 2 +- Moose Development/Moose/Functional/Range.lua | 13 ++- .../Moose/Functional/ZoneCaptureCoalition.lua | 20 ++-- .../Moose/Functional/ZoneGoal.lua | 100 ++++++++++++------ .../Moose/Functional/ZoneGoalCoalition.lua | 76 ++++++++++--- 5 files changed, 159 insertions(+), 52 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 07c93f7b9..a1db93d70 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -56,7 +56,7 @@ --- @type ZONE_BASE -- @field #string ZoneName Name of the zone. -- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. --- @extends Core.Base#BASE +-- @extends Core.Fsm#FSM --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index d3fc4d1aa..d181ebbec 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -516,7 +516,7 @@ RANGE.MenuF10Root=nil --- Range script version. -- @field #string version -RANGE.version="2.2.0" +RANGE.version="2.2.1" --TODO list: --TODO: Verbosity level for messages. @@ -2521,10 +2521,17 @@ function RANGE:_DisplayBombTargets(_unitname) local coord=self:_GetBombTargetCoordinate(bombtarget) if coord then + + -- Get elevation + local elevation=coord:GetLandHeight() + local eltxt=string.format("%d m", elevation) + if _settings:IsImperial() then + elevation=UTILS.MetersToFeet(elevation) + eltxt=string.format("%d ft", elevation) + end local ca2g=coord:ToStringA2G(_unit,_settings) - --local lldms=coord:ToStringLLDMS(_settings) - _text=_text..string.format("\n- %s:\n%s", bombtarget.name or "unknown", ca2g) + _text=_text..string.format("\n- %s:\n%s @ %s", bombtarget.name or "unknown", ca2g, eltxt) end end diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 2cd1f7ac2..fbca12fae 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -347,6 +347,8 @@ do -- ZONE_CAPTURE_COALITION -- @param #ZONE_CAPTURE_COALITION self -- @param Core.Zone#ZONE Zone A @{Zone} object with the goal to be achieved. -- @param DCSCoalition.DCSCoalition#coalition Coalition The initial coalition owning the zone. + -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}. + -- @param #table ObjectCategories Table of unit categories. See [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object). Default {Object.Category.UNIT, Object.Category.STATIC}, i.e. all UNITS and STATICS. -- @return #ZONE_CAPTURE_COALITION -- @usage -- @@ -355,11 +357,13 @@ do -- ZONE_CAPTURE_COALITION -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( AttackZone, coalition.side.RED ) -- Create a new ZONE_CAPTURE_COALITION object of zone AttackZone with ownership RED coalition. -- ZoneCaptureCoalition:__Guard( 1 ) -- Start the Guarding of the AttackZone. -- - function ZONE_CAPTURE_COALITION:New( Zone, Coalition, UnitCategories ) + function ZONE_CAPTURE_COALITION:New( Zone, Coalition, UnitCategories, ObjectCategories ) local self = BASE:Inherit( self, ZONE_GOAL_COALITION:New( Zone, Coalition, UnitCategories ) ) -- #ZONE_CAPTURE_COALITION - self:F( { Zone = Zone, Coalition = Coalition, UnitCategories = UnitCategories } ) + self:F( { Zone = Zone, Coalition = Coalition, UnitCategories = UnitCategories, ObjectCategories = ObjectCategories } ) + + self:SetObjectCategories(ObjectCategories) do @@ -613,16 +617,15 @@ do -- ZONE_CAPTURE_COALITION -- function ZONE_CAPTURE_COALITION:Start( StartInterval, RepeatInterval ) - self.StartInterval = StartInterval or 15 + self.StartInterval = StartInterval or 1 self.RepeatInterval = RepeatInterval or 15 if self.ScheduleStatusZone then self:ScheduleStop( self.ScheduleStatusZone ) end - self.ScheduleStatusZone = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 1.5, nil, self.StatusZone, self ) - - self:StatusZone() + -- Start Status scheduler. + self.ScheduleStatusZone = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 0.1, nil, self.StatusZone, self ) end @@ -696,12 +699,14 @@ do -- ZONE_CAPTURE_COALITION --- On enter "Guarded" state. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:onenterGuarded() + env.info("FF enter Guarded") self:Mark() end --- On enter "Captured" state. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:onenterCaptured() + env.info("FF enter Captured") -- Get new coalition. local NewCoalition = self:GetScannedCoalition() @@ -720,18 +725,21 @@ do -- ZONE_CAPTURE_COALITION --- On after "ChangeCoalition" state. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:onafterChangeCoalition(From, Event, To, NewCoalition, OldCoalition) + env.info("FF after ChangeCoalition") self:SetCoalition(NewCoalition) end --- On enter "Empty" state. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:onenterEmpty() + env.info("FF enter Empty") self:Mark() end --- On enter "Attacked" state. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:onenterAttacked() + env.info("FF enter Attacked") self:Mark() end diff --git a/Moose Development/Moose/Functional/ZoneGoal.lua b/Moose Development/Moose/Functional/ZoneGoal.lua index 5a7e85397..f3ba834dd 100644 --- a/Moose Development/Moose/Functional/ZoneGoal.lua +++ b/Moose Development/Moose/Functional/ZoneGoal.lua @@ -17,6 +17,12 @@ do -- Zone --- @type ZONE_GOAL + -- @field #string ClassName Name of the class. + -- @field Core.Goal#GOAL Goal The goal object. + -- @field #number SmokeTime Time stamp in seconds when the last smoke of the zone was triggered. + -- @field Core.Scheduler#SCHEDULER SmokeScheduler Scheduler responsible for smoking the zone. + -- @field #number SmokeColor Color of the smoke. + -- @field #boolean SmokeZone If true, smoke zone. -- @extends Core.Zone#ZONE_RADIUS @@ -43,6 +49,8 @@ do -- Zone Goal = nil, SmokeTime = nil, SmokeScheduler = nil, + SmokeColor = nil, + SmokeZone = nil, } --- ZONE_GOAL Constructor. @@ -54,11 +62,24 @@ do -- Zone local self = BASE:Inherit( self, ZONE_RADIUS:New( Zone:GetName(), Zone:GetVec2(), Zone:GetRadius() ) ) -- #ZONE_GOAL self:F( { Zone = Zone } ) + -- Goal object. self.Goal = GOAL:New() self.SmokeTime = nil + + -- Set smoke ON. + self:SetSmokeZone(true) self:AddTransition( "*", "DestroyedUnit", "*" ) + + --- DestroyedUnit event. + -- @function [parent=#ZONE_GOAL] DestroyedUnit + -- @param #ZONE_GOAL self + + --- DestroyedUnit delayed event + -- @function [parent=#ZONE_GOAL] __DestroyedUnit + -- @param #ZONE_GOAL self + -- @param #number delay Delay in seconds. --- DestroyedUnit Handler OnAfter for ZONE_GOAL -- @function [parent=#ZONE_GOAL] OnAfterDestroyedUnit @@ -69,19 +90,18 @@ do -- Zone -- @param Wrapper.Unit#UNIT DestroyedUnit The destroyed unit. -- @param #string PlayerName The name of the player. - return self end - --- Get the Zone + --- Get the Zone. -- @param #ZONE_GOAL self - -- @return Core.Zone#ZONE_BASE + -- @return #ZONE_GOAL function ZONE_GOAL:GetZone() return self end - --- Get the name of the ProtectZone + --- Get the name of the Zone. -- @param #ZONE_GOAL self -- @return #string function ZONE_GOAL:GetZoneName() @@ -89,35 +109,43 @@ do -- Zone end - --- Smoke the center of theh zone. + --- Activate smoking of zone with the color or the current owner. -- @param #ZONE_GOAL self - -- @param #SMOKECOLOR.Color SmokeColor - function ZONE_GOAL:Smoke( SmokeColor ) - + -- @param #boolean switch If *true* or *nil* activate smoke. If false, no smoke. + -- @return #ZONE_GOAL + function ZONE_GOAL:SetSmokeZone(switch) + if switch==nil or switch==true then + self.SmokeZone=true + else + self.SmokeZone=false + end + return self + end + + --- Set the smoke color. + -- @param #ZONE_GOAL self + -- @param DCS#SMOKECOLOR.Color SmokeColor + function ZONE_GOAL:Smoke( SmokeColor ) self:F( { SmokeColor = SmokeColor} ) self.SmokeColor = SmokeColor end - --- Flare the center of the zone. + --- Flare the zone boundary. -- @param #ZONE_GOAL self - -- @param #SMOKECOLOR.Color FlareColor + -- @param DCS#SMOKECOLOR.Color FlareColor function ZONE_GOAL:Flare( FlareColor ) - self:FlareZone( FlareColor, math.random( 1, 360 ) ) + self:FlareZone( FlareColor, 30) end --- When started, check the Smoke and the Zone status. -- @param #ZONE_GOAL self function ZONE_GOAL:onafterGuard() - - --self:GetParent( self ):onafterStart() - self:F("Guard") - - --self:ScheduleRepeat( 15, 15, 0.1, nil, self.StatusZone, self ) - if not self.SmokeScheduler then + + if self.SmokeZone and not self.SmokeScheduler then self.SmokeScheduler = self:ScheduleRepeat( 1, 1, 0.1, nil, self.StatusSmoke, self ) end end @@ -126,42 +154,54 @@ do -- Zone --- Check status Smoke. -- @param #ZONE_GOAL self function ZONE_GOAL:StatusSmoke() - self:F({self.SmokeTime, self.SmokeColor}) - local CurrentTime = timer.getTime() - - if self.SmokeTime == nil or self.SmokeTime + 300 <= CurrentTime then - if self.SmokeColor then - self:GetCoordinate():Smoke( self.SmokeColor ) - --self.SmokeColor = nil - self.SmokeTime = CurrentTime + if self.SmokeZone then + + -- Current time. + local CurrentTime = timer.getTime() + + -- Restart smoke every 5 min. + if self.SmokeTime == nil or self.SmokeTime + 300 <= CurrentTime then + if self.SmokeColor then + self:GetCoordinate():Smoke( self.SmokeColor ) + self.SmokeTime = CurrentTime + end end + end + end --- @param #ZONE_GOAL self - -- @param Core.Event#EVENTDATA EventData + -- @param Core.Event#EVENTDATA EventData Event data table. function ZONE_GOAL:__Destroyed( EventData ) self:F( { "EventDead", EventData } ) self:F( { EventData.IniUnit } ) - local Vec3 = EventData.IniDCSUnit:getPosition().p - self:F( { Vec3 = Vec3 } ) - if EventData.IniDCSUnit then - if self:IsVec3InZone(Vec3) then + + local Vec3 = EventData.IniDCSUnit:getPosition().p + self:F( { Vec3 = Vec3 } ) + + if Vec3 and self:IsVec3InZone(Vec3) then + local PlayerHits = _DATABASE.HITS[EventData.IniUnitName] + if PlayerHits then + for PlayerName, PlayerHit in pairs( PlayerHits.Players or {} ) do self.Goal:AddPlayerContribution( PlayerName ) self:DestroyedUnit( EventData.IniUnitName, PlayerName ) end + end + end end + end diff --git a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua index 0625aaae1..a25d4e534 100644 --- a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua @@ -17,6 +17,10 @@ do -- ZoneGoal --- @type ZONE_GOAL_COALITION + -- @field #string ClassName Name of the Class. + -- @field #number Coalition The current coalition ID of the zone owner. + -- @field #table UnitCategories Table of unit categories that are able to capture and hold the zone. Default is only GROUND units. + -- @field #table ObjectCategories Table of object categories that are able to hold a zone. Default is UNITS and STATICS. -- @extends Functional.ZoneGoal#ZONE_GOAL @@ -37,9 +41,10 @@ do -- ZoneGoal -- -- @field #ZONE_GOAL_COALITION ZONE_GOAL_COALITION = { - ClassName = "ZONE_GOAL_COALITION", - Coalition = nil, - + ClassName = "ZONE_GOAL_COALITION", + Coalition = nil, + UnitCategories = nil, + ObjectCategories = nil, } --- @field #table ZONE_GOAL_COALITION.States @@ -48,27 +53,69 @@ do -- ZoneGoal --- ZONE_GOAL_COALITION Constructor. -- @param #ZONE_GOAL_COALITION self -- @param Core.Zone#ZONE Zone A @{Zone} object with the goal to be achieved. - -- @param DCSCoalition.DCSCoalition#coalition Coalition The initial coalition owning the zone. + -- @param DCSCoalition.DCSCoalition#coalition Coalition The initial coalition owning the zone. Default coalition.side.NEUTRAL. + -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}. -- @return #ZONE_GOAL_COALITION - function ZONE_GOAL_COALITION:New( Zone, Coalition ) + function ZONE_GOAL_COALITION:New( Zone, Coalition, UnitCategories ) + if not Zone then + BASE:E("ERROR: No Zone specified in ZONE_GOAL_COALITON!") + return nil + end + + -- Inherit ZONE_GOAL. local self = BASE:Inherit( self, ZONE_GOAL:New( Zone ) ) -- #ZONE_GOAL_COALITION self:F( { Zone = Zone, Coalition = Coalition } ) - self:SetCoalition( Coalition ) - - + -- Set initial owner. + self:SetCoalition( Coalition or coalition.side.NEUTRAL) + + -- Set default unit and object categories for the zone scan. + self:SetUnitCategories(UnitCategories) + self:SetObjectCategories() + return self end --- Set the owning coalition of the zone. -- @param #ZONE_GOAL_COALITION self - -- @param DCSCoalition.DCSCoalition#coalition Coalition + -- @param DCSCoalition.DCSCoalition#coalition Coalition The coalition ID, e.g. *coalition.side.RED*. + -- @return #ZONE_GOAL_COALITION function ZONE_GOAL_COALITION:SetCoalition( Coalition ) self.Coalition = Coalition + return self + end + + --- Set the owning coalition of the zone. + -- @param #ZONE_GOAL_COALITION self + -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}. + -- @return #ZONE_GOAL_COALITION + function ZONE_GOAL_COALITION:SetUnitCategories( UnitCategories ) + + if UnitCategories and type(UnitCategories)~="table" then + UnitCategories={UnitCategories} + end + + self.UnitCategories=UnitCategories or {Unit.Category.GROUND_UNIT} + + return self end + --- Set the owning coalition of the zone. + -- @param #ZONE_GOAL_COALITION self + -- @param #table ObjectCategories Table of unit categories. See [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object). Default {Object.Category.UNIT, Object.Category.STATIC}, i.e. all UNITS and STATICS. + -- @return #ZONE_GOAL_COALITION + function ZONE_GOAL_COALITION:SetObjectCategories( ObjectCategories ) + + if ObjectCategories and type(ObjectCategories)~="table" then + ObjectCategories={ObjectCategories} + end + + self.ObjectCategories=ObjectCategories or {Object.Category.UNIT, Object.Category.STATIC} + + return self + end --- Get the owning coalition of the zone. -- @param #ZONE_GOAL_COALITION self @@ -95,20 +142,25 @@ do -- ZoneGoal return "Neutral" end - return "" + return "Unknown" end --- Check status Coalition ownership. -- @param #ZONE_GOAL_COALITION self + -- @return #ZONE_GOAL_COALITION function ZONE_GOAL_COALITION:StatusZone() local State = self:GetState() self:F( { State = self:GetState() } ) - env.info("scanning") - self:Scan( { Object.Category.UNIT, Object.Category.STATIC } ) + -- Debug text. + local text=string.format("Zone state=%s, Owner=%s, Scanning...", State, self:GetCoalitionName()) + env.info(text) + + self:Scan( self.ObjectCategories, self.UnitCategories ) + return self end end From 8915657e91793d7d6cca917e14d3b28a50676ee6 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 29 Dec 2019 23:08:23 +0100 Subject: [PATCH 412/485] ZONE_CAPTURE_COALITION --- Moose Development/Moose/Core/Goal.lua | 1 + .../Moose/Functional/ZoneCaptureCoalition.lua | 300 ++++++++++-------- .../Moose/Functional/ZoneGoal.lua | 9 +- .../Moose/Functional/ZoneGoalCoalition.lua | 32 +- .../Moose/Ops/RecoveryTanker.lua | 13 +- Moose Development/Moose/Ops/RescueHelo.lua | 4 +- Moose Development/Moose/Utilities/Utils.lua | 23 ++ .../Moose/Wrapper/Identifiable.lua | 13 +- 8 files changed, 235 insertions(+), 160 deletions(-) diff --git a/Moose Development/Moose/Core/Goal.lua b/Moose Development/Moose/Core/Goal.lua index ac1f616b9..c145d6de4 100644 --- a/Moose Development/Moose/Core/Goal.lua +++ b/Moose Development/Moose/Core/Goal.lua @@ -15,6 +15,7 @@ -- === -- -- ### Author: **FlightControl** +-- ### Contributions: **funkyfranky** -- -- === -- diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index fbca12fae..81c2aefd1 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -39,7 +39,7 @@ -- === -- -- ### Author: **FlightControl** --- ### Contributions: **Millertime** - Concept +-- ### Contributions: **Millertime** - Concept, **funkyfranky** -- -- === -- @@ -49,6 +49,15 @@ do -- ZONE_CAPTURE_COALITION --- @type ZONE_CAPTURE_COALITION + -- @field #string ClassName Name of the class. + -- @field #number MarkBlue ID of blue F10 mark. + -- @field #number MarkRed ID of red F10 mark. + -- @field #number StartInterval Time in seconds after the status monitor is started. + -- @field #number RepeatInterval Time in seconds after which the zone status is updated. + -- @field #boolean HitsOn If true, hit events are monitored and trigger the "Attack" event when a defending unit is hit. + -- @field #number HitTimeLast Time stamp in seconds when the last unit inside the zone was hit. + -- @field #number HitTimeAttackOver Time interval in seconds before the zone goes from "Attacked" to "Guarded" state after the last hit. + -- @field #boolean MarkOn If true, create marks of zone status on F10 map. -- @extends Functional.ZoneGoalCoalition#ZONE_GOAL_COALITION @@ -337,12 +346,21 @@ do -- ZONE_CAPTURE_COALITION -- -- @field #ZONE_CAPTURE_COALITION ZONE_CAPTURE_COALITION = { - ClassName = "ZONE_CAPTURE_COALITION", + ClassName = "ZONE_CAPTURE_COALITION", + MarkBlue = nil, + MarkRed = nil, + StartInterval = nil, + RepeatInterval = nil, + HitsOn = nil, + HitTimeLast = nil, + HitTimeAttackOver = nil, + MarkOn = nil, } - - --- @field #table ZONE_CAPTURE_COALITION.States - ZONE_CAPTURE_COALITION.States = {} - + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor and Start/Stop Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- ZONE_CAPTURE_COALITION Constructor. -- @param #ZONE_CAPTURE_COALITION self -- @param Core.Zone#ZONE Zone A @{Zone} object with the goal to be achieved. @@ -354,16 +372,22 @@ do -- ZONE_CAPTURE_COALITION -- -- AttackZone = ZONE:New( "AttackZone" ) -- - -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( AttackZone, coalition.side.RED ) -- Create a new ZONE_CAPTURE_COALITION object of zone AttackZone with ownership RED coalition. + -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( AttackZone, coalition.side.RED, {UNITS ) -- Create a new ZONE_CAPTURE_COALITION object of zone AttackZone with ownership RED coalition. -- ZoneCaptureCoalition:__Guard( 1 ) -- Start the Guarding of the AttackZone. -- function ZONE_CAPTURE_COALITION:New( Zone, Coalition, UnitCategories, ObjectCategories ) local self = BASE:Inherit( self, ZONE_GOAL_COALITION:New( Zone, Coalition, UnitCategories ) ) -- #ZONE_CAPTURE_COALITION - self:F( { Zone = Zone, Coalition = Coalition, UnitCategories = UnitCategories, ObjectCategories = ObjectCategories } ) self:SetObjectCategories(ObjectCategories) + + -- Default is no smoke. + self:SetSmokeZone(false) + -- Default is F10 marks ON. + self:SetMarkZone(true) + -- Start in state "Empty". + self:SetStartState("Empty") do @@ -548,47 +572,8 @@ do -- ZONE_CAPTURE_COALITION -- @param #ZONE_CAPTURE_COALITION self -- @param #number Delay - self:AddTransition( "*", "ChangeCoalition", "*" ) - - --- ChangeCoalition Handler OnBefore for ZONE_CAPTURE_COALITION - -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeChangeCoalition - -- @param #ZONE_CAPTURE_COALITION self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number NewCoalition New coalition ID, i.e. after the change. - -- @param #number OldCoalition Old coalition ID, i.e. before the change. - -- @return #boolean - - --- ChangeCoalition Handler OnAfter for ZONE_CAPTURE_COALITION - -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterChangeCoalition - -- @param #ZONE_CAPTURE_COALITION self - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param #number NewCoalition New coalition ID, i.e. after the change. - -- @param #number OldCoalition Old coalition ID, i.e. before the change. - - --- ChangeCoalition Trigger for ZONE_CAPTURE_COALITION - -- @function [parent=#ZONE_CAPTURE_COALITION] ChangeCoalition - -- @param #ZONE_CAPTURE_COALITION self - -- @param #number NewCoalition New coalition ID, i.e. after the change. - -- @param #number OldCoalition Old coalition ID, i.e. before the change. - - --- ChangeCoalition Asynchronous Trigger for ZONE_CAPTURE_COALITION - -- @function [parent=#ZONE_CAPTURE_COALITION] __ChangeCoalition - -- @param #ZONE_CAPTURE_COALITION self - -- @param #number Delay - -- @param #number NewCoalition New coalition ID, i.e. after the change. - -- @param #number OldCoalition Old coalition ID, i.e. before the change. - - - -- 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 ) - -- ZoneGoal objects are added to the _DATABASE.ZONES_GOAL and SET_ZONE_GOAL sets. - _EVENTDISPATCHER:CreateEventNewZoneGoal( self ) + _EVENTDISPATCHER:CreateEventNewZoneGoal(self) return self end @@ -603,6 +588,7 @@ do -- ZONE_CAPTURE_COALITION -- @param #ZONE_CAPTURE_COALITION self -- @param #number StartInterval (optional) Specifies the start time interval in seconds when the zone state will be checked for the first time. -- @param #number RepeatInterval (optional) Specifies the repeat time interval in seconds when the zone state will be checked repeatedly. + -- @return #ZONE_CAPTURE_COALITION self -- @usage -- -- -- Setup the zone. @@ -626,6 +612,14 @@ do -- ZONE_CAPTURE_COALITION -- Start Status scheduler. self.ScheduleStatusZone = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 0.1, nil, self.StatusZone, self ) + + -- 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) + + -- Create mark on F10 map. + self:Mark() + + return self end @@ -667,30 +661,88 @@ do -- ZONE_CAPTURE_COALITION function ZONE_CAPTURE_COALITION:Stop() if self.ScheduleStatusZone then - self:ScheduleStop( self.ScheduleStatusZone ) + self:ScheduleStop(self.ScheduleStatusZone) end + + if self.SmokeScheduler then + self:ScheduleStop(self.SmokeScheduler) + end + + self:UnHandleEvent(EVENTS.Hit) + end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + --- Set whether hit events of defending units are monitored and trigger "Attack" events. + -- @param #ZONE_CAPTURE_COALITION self + -- @param #boolean Switch If *true*, hit events are monitored. If *false* or *nil*, hit events are not monitored. + -- @param #number TimeAttackOver (Optional) Time in seconds after an attack is over after the last hit and the zone state goes to "Guarded". Default is 300 sec = 5 min. + -- @return #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:SetMonitorHits(Switch, TimeAttackOver) + self.HitsOn=Switch + self.HitTimeAttackOver=TimeAttackOver or 5*60 + return self + end + + --- Set whether marks on the F10 map are shown, which display the current zone status. + -- @param #ZONE_CAPTURE_COALITION self + -- @param #boolean Switch If *true* or *nil*, marks are shown. If *false*, marks are not displayed. + -- @return #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:SetMarkZone(Switch) + if Switch==nil or Switch==true then + self.MarkOn=true + else + self.MarkOn=false + end + return self + end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + - --- @param #ZONE_CAPTURE_COALITION self + --- Monitor hit events. + -- @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 ) then - self:Attack() + if self.HitsOn then + + local UnitHit = EventData.TgtUnit + + -- Check if unit is inside the capture zone and that it is of the defending coalition. + if UnitHit and UnitHit:IsInZone(self) and UnitHit:GetCoalition()==self.Coalition then + + -- Update last hit time. + self.HitTimeLast=timer.getTime() + + -- Only trigger attacked event if not already in state "Attacked". + if self:GetState()~="Attacked" then + self:F2("Hit ==> Attack") + self:Attack() + end + end + end end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- On after "Guard" event. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:onafterGuard() + self:F2("After Guard") - if not self.SmokeScheduler then + if self.SmokeZone and not self.SmokeScheduler then self.SmokeScheduler = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 0.1, nil, self.StatusSmoke, self ) end @@ -699,21 +751,21 @@ do -- ZONE_CAPTURE_COALITION --- On enter "Guarded" state. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:onenterGuarded() - env.info("FF enter Guarded") + self:F2("Enter Guarded") self:Mark() end --- On enter "Captured" state. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:onenterCaptured() - env.info("FF enter Captured") + self:F2("Enter Captured") -- Get new coalition. local NewCoalition = self:GetScannedCoalition() self:F( { NewCoalition = NewCoalition } ) -- Set new owner of zone. - self:ChangeCoalition(NewCoalition, self.Coalition) + self:SetCoalition(NewCoalition) -- Update mark. self:Mark() @@ -721,92 +773,70 @@ do -- ZONE_CAPTURE_COALITION -- Goal achieved. self.Goal:Achieved() end - - --- On after "ChangeCoalition" state. - -- @param #ZONE_CAPTURE_COALITION self - function ZONE_CAPTURE_COALITION:onafterChangeCoalition(From, Event, To, NewCoalition, OldCoalition) - env.info("FF after ChangeCoalition") - self:SetCoalition(NewCoalition) - end - + --- On enter "Empty" state. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:onenterEmpty() - env.info("FF enter Empty") + self:F2("Enter Empty") self:Mark() end --- On enter "Attacked" state. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:onenterAttacked() - env.info("FF enter Attacked") + self:F2("Enter Attacked") self:Mark() end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Check Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - --- Check if zone is "Guarded" - -- @param #ZONE_CAPTURE_COALITION self - -- @return #boolean self:IsAllInZoneOfCoalition( self.Coalition ) - function ZONE_CAPTURE_COALITION:IsGuarded() - - local IsGuarded = self:IsAllInZoneOfCoalition( self.Coalition ) - self:F( { IsGuarded = IsGuarded } ) - return IsGuarded - end - - --- Check if zone is "Empty" + --- Check if zone is "Empty". -- @param #ZONE_CAPTURE_COALITION self -- @return #boolean self:IsNoneInZone() function ZONE_CAPTURE_COALITION:IsEmpty() local IsEmpty = self:IsNoneInZone() self:F( { IsEmpty = IsEmpty } ) + return IsEmpty end - --- Check if zone is "Captured" + --- Check if zone is "Guarded", i.e. only one (the defending) coaliton is present inside the zone. + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsAllInZoneOfCoalition( self.Coalition ) + function ZONE_CAPTURE_COALITION:IsGuarded() + + local IsGuarded = self:IsAllInZoneOfCoalition( self.Coalition ) + self:F( { IsGuarded = IsGuarded } ) + + return IsGuarded + end + + --- Check if zone is "Captured", i.e. another coalition took control over the zone and is the only one present. -- @param #ZONE_CAPTURE_COALITION self -- @return #boolean self:IsAllInZoneOfOtherCoalition( self.Coalition ) function ZONE_CAPTURE_COALITION:IsCaptured() local IsCaptured = self:IsAllInZoneOfOtherCoalition( self.Coalition ) self:F( { IsCaptured = IsCaptured } ) + return IsCaptured end - --- Check if zone is "Attacked" + --- Check if zone is "Attacked", i.e. another coaliton entered the zone. -- @param #ZONE_CAPTURE_COALITION self -- @return #boolean self:IsSomeInZoneOfCoalition( self.Coalition ) function ZONE_CAPTURE_COALITION:IsAttacked() local IsAttacked = self:IsSomeInZoneOfCoalition( self.Coalition ) self:F( { IsAttacked = IsAttacked } ) + return IsAttacked end - - --- Check if zone is captured. - -- @param #ZONE_CAPTURE_COALITION self - -- @return #boolean self:IsAllInZoneOfOtherCoalition( self.Coalition ) - function ZONE_CAPTURE_COALITION:IsCaptured() - local IsCaptured = self:IsAllInZoneOfOtherCoalition( self.Coalition ) - self:F( { IsCaptured = IsCaptured } ) - return IsCaptured - end - - --- Check if zone is attacked. - -- @param #ZONE_CAPTURE_COALITION self - -- @return #boolean self:IsSomeInZoneOfCoalition( self.Coalition ) - function ZONE_CAPTURE_COALITION:IsAttacked() - - local IsAttacked = self:IsSomeInZoneOfCoalition( self.Coalition ) - self:F( { IsAttacked = IsAttacked } ) - return IsAttacked - end - - --- Check status Coalition ownership. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:StatusZone() @@ -817,9 +847,16 @@ do -- ZONE_CAPTURE_COALITION -- Scan zone in parent class ZONE_GOAL_COALITION self:GetParent( self, ZONE_CAPTURE_COALITION ).StatusZone( self ) + local Tnow=timer.getTime() + -- Check if zone is guarded. if State ~= "Guarded" and self:IsGuarded() then - self:Guard() + + -- Check that there was a sufficient amount of time after the last hit before going back to "Guarded". + if self.HitTimeLast==nil or Tnow>=self.HitTimeLast+self.HitTimeAttackOver then + self:Guard() + self.HitTimeLast=nil + end end -- Check if zone is empty. @@ -837,9 +874,8 @@ do -- ZONE_CAPTURE_COALITION self:Capture() end - -- - local text=string.format("CAPTURE ZONE %s: Status %s", self:GetZoneName(), State) - + -- Status text. + local text=string.format("CAPTURE ZONE %s: Owner=%s (Previous=%s): Status %s", self:GetZoneName(), self:GetCoalitionName(), UTILS.GetCoalitionName(self:GetPreviousCoalition()), State) local NewState = self:GetState() if NewState~=State then text=text..string.format(" --> %s", NewState) @@ -847,32 +883,44 @@ do -- ZONE_CAPTURE_COALITION self:I(text) end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Update Mark on F10 map. -- @param #ZONE_CAPTURE_COALITION self function ZONE_CAPTURE_COALITION:Mark() - local Coord = self:GetCoordinate() - local ZoneName = self:GetZoneName() - local State = self:GetState() + if self.MarkOn then - if self.MarkRed and self.MarkBlue then - self:F( { MarkRed = self.MarkRed, MarkBlue = self.MarkBlue } ) - Coord:RemoveMark( self.MarkRed ) - Coord:RemoveMark( self.MarkBlue ) + local Coord = self:GetCoordinate() + local ZoneName = self:GetZoneName() + local State = self:GetState() + + -- Remove marks. + if self.MarkRed then + Coord:RemoveMark(self.MarkRed) + end + if self.MarkBlue then + Coord:RemoveMark(self.MarkBlue) + end + + -- Create new marks for each coaliton. + if self.Coalition == coalition.side.BLUE then + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Blue\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Blue\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + elseif self.Coalition == coalition.side.RED then + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Red\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Red\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + else + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + end + end - if self.Coalition == coalition.side.BLUE then - self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Blue\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) - self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Blue\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) - elseif self.Coalition == coalition.side.RED then - self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Red\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) - self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Red\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) - else - self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) - self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) - end end - end diff --git a/Moose Development/Moose/Functional/ZoneGoal.lua b/Moose Development/Moose/Functional/ZoneGoal.lua index f3ba834dd..cd55b2603 100644 --- a/Moose Development/Moose/Functional/ZoneGoal.lua +++ b/Moose Development/Moose/Functional/ZoneGoal.lua @@ -8,6 +8,7 @@ -- === -- -- ### Author: **FlightControl** +-- ### Contributions: **funkyfranky** -- -- === -- @@ -111,14 +112,17 @@ do -- Zone --- Activate smoking of zone with the color or the current owner. -- @param #ZONE_GOAL self - -- @param #boolean switch If *true* or *nil* activate smoke. If false, no smoke. + -- @param #boolean switch If *true* or *nil* activate smoke. If *false* or *nil*, no smoke. -- @return #ZONE_GOAL function ZONE_GOAL:SetSmokeZone(switch) + self.SmokeZone=switch + --[[ if switch==nil or switch==true then self.SmokeZone=true else self.SmokeZone=false end + ]] return self end @@ -145,8 +149,9 @@ do -- Zone function ZONE_GOAL:onafterGuard() self:F("Guard") + -- Start smoke if self.SmokeZone and not self.SmokeScheduler then - self.SmokeScheduler = self:ScheduleRepeat( 1, 1, 0.1, nil, self.StatusSmoke, self ) + self.SmokeScheduler = self:ScheduleRepeat(1, 1, 0.1, nil, self.StatusSmoke, self) end end diff --git a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua index a25d4e534..6296b4835 100644 --- a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua @@ -19,6 +19,7 @@ do -- ZoneGoal --- @type ZONE_GOAL_COALITION -- @field #string ClassName Name of the Class. -- @field #number Coalition The current coalition ID of the zone owner. + -- @field #number PreviousCoalition The previous owner of the zone. -- @field #table UnitCategories Table of unit categories that are able to capture and hold the zone. Default is only GROUND units. -- @field #table ObjectCategories Table of object categories that are able to hold a zone. Default is UNITS and STATICS. -- @extends Functional.ZoneGoal#ZONE_GOAL @@ -43,6 +44,7 @@ do -- ZoneGoal ZONE_GOAL_COALITION = { ClassName = "ZONE_GOAL_COALITION", Coalition = nil, + PreviousCoaliton = nil, UnitCategories = nil, ObjectCategories = nil, } @@ -83,6 +85,7 @@ do -- ZoneGoal -- @param DCSCoalition.DCSCoalition#coalition Coalition The coalition ID, e.g. *coalition.side.RED*. -- @return #ZONE_GOAL_COALITION function ZONE_GOAL_COALITION:SetCoalition( Coalition ) + self.PreviousCoalition=self.Coalition or Coalition self.Coalition = Coalition return self end @@ -124,25 +127,19 @@ do -- ZoneGoal return self.Coalition end + --- Get the previous coaliton, i.e. the one owning the zone before the current one. + -- @param #ZONE_GOAL_COALITION self + -- @return DCSCoalition.DCSCoalition#coalition Coalition. + function ZONE_GOAL_COALITION:GetPreviousCoalition() + return self.PreviousCoalition + end + --- Get the owning coalition name of the zone. -- @param #ZONE_GOAL_COALITION self -- @return #string Coalition name. function ZONE_GOAL_COALITION:GetCoalitionName() - - if self.Coalition == coalition.side.BLUE then - return "Blue" - end - - if self.Coalition == coalition.side.RED then - return "Red" - end - - if self.Coalition == coalition.side.NEUTRAL then - return "Neutral" - end - - return "Unknown" + return UTILS.GetCoalitionName(self.Coalition) end @@ -151,13 +148,14 @@ do -- ZoneGoal -- @return #ZONE_GOAL_COALITION function ZONE_GOAL_COALITION:StatusZone() + -- Get current state. local State = self:GetState() - self:F( { State = self:GetState() } ) -- Debug text. local text=string.format("Zone state=%s, Owner=%s, Scanning...", State, self:GetCoalitionName()) - env.info(text) - + self:F(text) + + -- Scan zone. self:Scan( self.ObjectCategories, self.UnitCategories ) return self diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index dae5f2ab4..b779f8efc 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -62,6 +62,7 @@ -- @field #string modex Tail number of the tanker. -- @field #boolean eplrs If true, enable data link, e.g. if used as AWACS. -- @field #boolean recovery If true, tanker will recover using the AIRBOSS marshal pattern. +-- @field #number terminaltype Terminal type of used parking spots on airbases. -- @extends Core.Fsm#FSM --- Recovery Tanker. @@ -298,6 +299,7 @@ RECOVERYTANKER = { modex = nil, eplrs = nil, recovery = nil, + terminaltype = nil, } --- Unique ID (global). @@ -306,7 +308,7 @@ _RECOVERYTANKERID=0 --- Class version. -- @field #string version -RECOVERYTANKER.version="1.0.8" +RECOVERYTANKER.version="1.0.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -381,6 +383,7 @@ function RECOVERYTANKER:New(carrierunit, tankergroupname) self:SetPatternUpdateInterval() self:SetAWACS(false) self:SetRecoveryAirboss(false) + self.terminaltype=AIRBASE.TerminalType.OpenMedOrBig -- Debug trace. if false then @@ -615,8 +618,9 @@ end --- 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 The home airbase. Can be the airbase name or a Moose AIRBASE object. +-- @param #number terminaltype (Optional) The terminal type of parking spots used for spawning at airbases. Default AIRBASE.TerminalType.OpenMedOrBig. -- @return #RECOVERYTANKER self -function RECOVERYTANKER:SetHomeBase(airbase) +function RECOVERYTANKER:SetHomeBase(airbase, terminaltype) if type(airbase)=="string" then self.airbase=AIRBASE:FindByName(airbase) else @@ -625,6 +629,9 @@ function RECOVERYTANKER:SetHomeBase(airbase) if not self.airbase then self:E(self.lid.."ERROR: Airbase is nil!") end + if termialtype then + self.terminaltype=terminaltype + end return self end @@ -937,7 +944,7 @@ function RECOVERYTANKER:onafterStart(From, Event, To) else -- Spawn tanker at airbase. - self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, AIRBASE.TerminalType.OpenMedOrBig) + self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, self.terminaltype) end diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index b3b0fc69f..8e979ccf9 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -237,7 +237,7 @@ _RESCUEHELOID=0 --- Class version. -- @field #string version -RESCUEHELO.version="1.0.9" +RESCUEHELO.version="1.1.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -902,7 +902,7 @@ function RESCUEHELO:onafterStart(From, Event, To) else -- Check if an uncontrolled helo group was requested. - if self.useuncontrolled then + if self.uncontrolledac then -- Use an uncontrolled aircraft group. self.helo=GROUP:FindByName(self.helogroupname) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 683252718..acfc30323 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1037,3 +1037,26 @@ function UTILS.CheckMemory(output) end return mem end + + +--- Get the coalition name from its numerical ID, e.g. coaliton.side.RED. +-- @param #number Coalition The coalition ID. +-- @return #string The coalition name, i.e. "Neutral", "Red" or "Blue" (or "Unknown"). +function UTILS.GetCoalitionName(Coalition) + + if Coalition then + if Coalition==coalition.side.BLUE then + return "Blue" + elseif Coalition==coalition.side.RED then + return "Red" + elseif Coalition==coalition.side.NEUTRAL then + return "Neutral" + else + return "Unknown" + end + else + return "Unknown" + end + +end + diff --git a/Moose Development/Moose/Wrapper/Identifiable.lua b/Moose Development/Moose/Wrapper/Identifiable.lua index 13a5b3234..7943fe049 100644 --- a/Moose Development/Moose/Wrapper/Identifiable.lua +++ b/Moose Development/Moose/Wrapper/Identifiable.lua @@ -168,20 +168,13 @@ function IDENTIFIABLE:GetCoalitionName() local DCSIdentifiable = self:GetDCSObject() if DCSIdentifiable then + + -- Get coaliton ID. local IdentifiableCoalition = DCSIdentifiable:getCoalition() self:T3( IdentifiableCoalition ) - if IdentifiableCoalition == coalition.side.BLUE then - return "Blue" - end + return UTILS.GetCoalitionName(IdentifiableCoalition) - if IdentifiableCoalition == coalition.side.RED then - return "Red" - end - - if IdentifiableCoalition == coalition.side.NEUTRAL then - return "Neutral" - end end self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) From 9fb311bf7dc7c0787899f629bde8bcf781047317 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 29 Dec 2019 23:10:49 +0100 Subject: [PATCH 413/485] Update AI_A2A_Patrol.lua --- Moose Development/Moose/AI/AI_A2A_Patrol.lua | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index 164a5edeb..45fef37cf 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -296,11 +296,7 @@ function AI_A2A_PATROL.PatrolRoute( AIPatrol, Fsm ) AIPatrol:F( { "AI_A2A_PATROL.PatrolRoute:", AIPatrol:GetName() } ) if AIPatrol and AIPatrol:IsAlive() then - if Fsm then - Fsm:Route() - else - AIPatrol:E("WARNING: FSM in AI_A2A_PATROL.PatrolRoute is nil!") - end + Fsm:Route() end end @@ -313,7 +309,6 @@ end -- @param #string Event The Event string. -- @param #string To The To State string. function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) - self:F2() -- When RTB, don't allow anymore the routing. From 740ee74f61e948c005b052de0939427c5d3d1469 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 31 Dec 2019 00:00:45 +0100 Subject: [PATCH 414/485] SUPPRESSION --- .../Moose/Core/ScheduleDispatcher.lua | 2 +- .../Moose/Functional/Suppression.lua | 296 ++++++++++-------- 2 files changed, 167 insertions(+), 131 deletions(-) diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 01b8a7000..bfd1dabb4 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -336,7 +336,7 @@ function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) Schedule.ScheduleID = nil else - self:E(string.format("Error no ScheduleID for CallID=%s", tostring(CallID))) + self:T(string.format("Error no ScheduleID for CallID=%s", tostring(CallID))) end else diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua index 098efeb20..073c009bd 100644 --- a/Moose Development/Moose/Functional/Suppression.lua +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -287,7 +287,7 @@ SUPPRESSION.MenuF10=nil --- PSEUDOATC version. -- @field #number version -SUPPRESSION.version="0.9.1" +SUPPRESSION.version="0.9.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -363,6 +363,7 @@ function SUPPRESSION:New(group) self:AddTransition("FallingBack", "FightBack", "CombatReady") self:AddTransition("Retreating", "Retreated", "Retreated") self:AddTransition("*", "Dead", "*") + self:AddTransition("*", "Stop", "Stopped") self:AddTransition("TakingCover", "Hit", "TakingCover") self:AddTransition("FallingBack", "Hit", "FallingBack") @@ -874,28 +875,14 @@ function SUPPRESSION:StatusReport(message) local roe=self.CurrentROE local state=self.CurrentAlarmState local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife() - local ammotot=self.Controllable:GetAmmunition() + local ammotot=group:GetAmmunition() - --[[ - local text=string.format("Status of group %s\n", name) - text=text..string.format("Number of units: %d of %d\n", nunits, self.IniGroupStrength) - text=text..string.format("Current state: %s\n", self:GetState()) - text=text..string.format("ROE: %s\n", roe) - text=text..string.format("Alarm state: %s\n", state) - text=text..string.format("Hits taken: %d\n", self.Nhit) - text=text..string.format("Life min: %3.0f\n", life_min) - text=text..string.format("Life max: %3.0f\n", life_max) - text=text..string.format("Life ave: %3.0f\n", life_ave) - text=text..string.format("Life ave0: %3.0f\n", life_ave0) - text=text..string.format("Ammo tot: %d\n", at) - text=text..string.format("Group strength: %3.0f", groupstrength) - ]] - - local text=string.format("State %s, Units=%d/%d, ROE=%s Alarm State=%s, Hits=%d, Life=%d/%d/%d/%d, Ammo=%d", + + local text=string.format("State %s, Units=%d/%d, ROE=%s, AlarmState=%s, Hits=%d, Life(min/max/ave/ave0)=%d/%d/%d/%d, Total Ammo=%d", self:GetState(), nunits, self.IniGroupStrength, self.CurrentROE, self.CurrentAlarmState, self.Nhit, life_min, life_max, life_ave, life_ave0, ammotot) MESSAGE:New(text, 10):ToAllIf(message or self.Debug) - self:I(self.lid.."\n"..text) + self:I(self.lid..text) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -983,7 +970,6 @@ function SUPPRESSION:onafterStart(Controllable, From, Event, To) world.addEventHandler(self) end - self:__Status(-1) end @@ -995,23 +981,40 @@ end -- @param #string To To state. function SUPPRESSION:onafterStatus(Controllable, From, Event, To) - --local text=string.format("State=%s, ROE %d, Life=%.1f", Controllable:GetName()) - --MESSAGE:New(text, 10):ToAllIf(self.Debug) - + -- Suppressed group. local group=self.Controllable --Wrapper.Group#GROUP - local n=group:GetAmmunition() + -- Check if group object exists. + if group then - self:StatusReport(false) - - -- Retreat if completely out of ammo and retreat zone defined. - if n==0 and self.RetreatZone then - - self:Retreat() + -- Number of alive units. + local nunits=group:CountAliveUnits() - end + -- Check if there are units. + if nunits>0 then + + -- Retreat if completely out of ammo and retreat zone defined. + local nammo=group:GetAmmunition() + if nammo==0 and self.RetreatZone then + self:Retreat() + end - self:__Status(-30) + -- Status report. + self:StatusReport(false) + + -- Call status again if not "Stopped". + if self:GetState()~="Stopped" then + self:__Status(-30) + end + + else + self:Stop() + end + + else + self:Stop() + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1409,27 +1412,54 @@ end function SUPPRESSION:onafterDead(Controllable, From, Event, To) self:_EventFromTo("onafterDead", Event, From, To) - -- Number of units left in the group. - local nunits=#self.Controllable:GetUnits() - - local text=string.format("Group %s: One of our units just died! %d units left.", self.Controllable:GetName(), nunits) - MESSAGE:New(text, 10):ToAllIf(self.Debug) - self:T(self.lid..text) - - -- Go to stop state. - if nunits==0 then - self:T(self.lid..string.format("Stopping SUPPRESSION for group %s.", Controllable:GetName())) - self:Stop() - if self.mooseevents then - self:UnHandleEvent(EVENTS.Dead) - self:UnHandleEvent(EVENTS.Hit) - else - world.removeEventHandler(self) + local group=self.Controllable --Wrapper.Group#GROUP + + if group then + + -- Number of units left in the group. + local nunits=group:CountAliveUnits() + + local text=string.format("Group %s: One of our units just died! %d units left.", self.Controllable:GetName(), nunits) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Go to stop state. + if nunits==0 then + self:Stop() end + + else + self:Stop() end end +--- After "Stop" event. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterStop(Controllable, From, Event, To) + self:_EventFromTo("onafterStop", Event, From, To) + + local text=string.format("Stopping SUPPRESSION for group %s", self.Controllable:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Clear all pending schedules + self.CallScheduler:Clear() + + if self.mooseevents then + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.Hit) + else + world.removeEventHandler(self) + end + +end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Event Handler ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1623,95 +1653,101 @@ function SUPPRESSION:_Run(fin, speed, formation, wait) local group=self.Controllable -- Wrapper.Controllable#CONTROLLABLE - -- Clear all tasks. - group:ClearTasks() + if group and group:IsAlive() then - -- Current coordinates of group. - local ini=group:GetCoordinate() - - -- Distance between current and final point. - local dist=ini:Get2DDistance(fin) - - -- Heading from ini to fin. - local heading=self:_Heading(ini, fin) - - -- Number of waypoints. - local nx - if dist <= 50 then - nx=2 - elseif dist <= 100 then - nx=3 - elseif dist <= 500 then - nx=4 - else - nx=5 - end - - -- Number of intermediate waypoints. - local dx=dist/(nx-1) + -- Clear all tasks. + group:ClearTasks() - -- Waypoint and task arrays. - local wp={} - local tasks={} - - -- First waypoint is the current position of the group. - wp[1]=ini:WaypointGround(speed, formation) - tasks[1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, 1, false) - - if self.Debug then - local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName())) - end - - self:T2(self.lid..string.format("Number of waypoints %d", nx)) - for i=1,nx-2 do - - local x=dx*i - local coord=ini:Translate(x, heading) + -- Current coordinates of group. + local ini=group:GetCoordinate() - wp[#wp+1]=coord:WaypointGround(speed, formation) - tasks[#tasks+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, false) + -- Distance between current and final point. + local dist=ini:Get2DDistance(fin) - self:T2(self.lid..string.format("%d x = %4.1f", i, x)) - if self.Debug then - local MarkerID=coord:MarkToAll(string.format("Waypoing %d of group %s", #wp, self.Controllable:GetName())) + -- Heading from ini to fin. + local heading=self:_Heading(ini, fin) + + -- Number of waypoints. + local nx + if dist <= 50 then + nx=2 + elseif dist <= 100 then + nx=3 + elseif dist <= 500 then + nx=4 + else + nx=5 end + -- Number of intermediate waypoints. + local dx=dist/(nx-1) + + -- Waypoint and task arrays. + local wp={} + local tasks={} + + -- First waypoint is the current position of the group. + wp[1]=ini:WaypointGround(speed, formation) + tasks[1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, 1, false) + + if self.Debug then + local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName())) + end + + self:T2(self.lid..string.format("Number of waypoints %d", nx)) + for i=1,nx-2 do + + local x=dx*i + local coord=ini:Translate(x, heading) + + wp[#wp+1]=coord:WaypointGround(speed, formation) + tasks[#tasks+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, false) + + self:T2(self.lid..string.format("%d x = %4.1f", i, x)) + if self.Debug then + local MarkerID=coord:MarkToAll(string.format("Waypoing %d of group %s", #wp, self.Controllable:GetName())) + end + + end + self:T2(self.lid..string.format("Total distance: %4.1f", dist)) + + -- Final waypoint. + wp[#wp+1]=fin:WaypointGround(speed, formation) + if self.Debug then + local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)", #wp, self.Controllable:GetName())) + end + + -- Task to hold. + local ConditionWait=group:TaskCondition(nil, nil, nil, nil, wait, nil) + local TaskHold = group:TaskHold() + + -- Task combo to make group hold at final waypoint. + local TaskComboFin = {} + TaskComboFin[#TaskComboFin+1] = group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, true) + TaskComboFin[#TaskComboFin+1] = group:TaskControlled(TaskHold, ConditionWait) + + -- Add final task. + tasks[#tasks+1]=group:TaskCombo(TaskComboFin) + + -- Original waypoints of the group. + local Waypoints = group:GetTemplateRoutePoints() + + -- New points are added to the default route. + for i,p in ipairs(wp) do + table.insert(Waypoints, i, wp[i]) + end + + -- Set task for all waypoints. + for i,wp in ipairs(Waypoints) do + group:SetTaskWaypoint(Waypoints[i], tasks[i]) + end + + -- Submit task and route group along waypoints. + group:Route(Waypoints) + + else + self:E(self.lid..string.format("ERROR: Group is not alive!")) end - self:T2(self.lid..string.format("Total distance: %4.1f", dist)) - - -- Final waypoint. - wp[#wp+1]=fin:WaypointGround(speed, formation) - if self.Debug then - local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)", #wp, self.Controllable:GetName())) - end - - -- Task to hold. - local ConditionWait=group:TaskCondition(nil, nil, nil, nil, wait, nil) - local TaskHold = group:TaskHold() - - -- Task combo to make group hold at final waypoint. - local TaskComboFin = {} - TaskComboFin[#TaskComboFin+1] = group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, true) - TaskComboFin[#TaskComboFin+1] = group:TaskControlled(TaskHold, ConditionWait) - - -- Add final task. - tasks[#tasks+1]=group:TaskCombo(TaskComboFin) - - -- Original waypoints of the group. - local Waypoints = group:GetTemplateRoutePoints() - - -- New points are added to the default route. - for i,p in ipairs(wp) do - table.insert(Waypoints, i, wp[i]) - end - - -- Set task for all waypoints. - for i,wp in ipairs(Waypoints) do - group:SetTaskWaypoint(Waypoints[i], tasks[i]) - end - - -- Submit task and route group along waypoints. - group:Route(Waypoints) end From 6208c79d1f47ee6e3e9ca67e1a63f7a5c38e8ca7 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 2 Jan 2020 16:17:25 +0100 Subject: [PATCH 415/485] ARTY v1.1.7 & AIRBASE - Improved ARTY._PassingWaypoint function. - Corrected AIRBASE.Ras_Al_Khaimah --- .../Moose/Functional/Artillery.lua | 35 +++++++++---------- Moose Development/Moose/Wrapper/Airbase.lua | 2 +- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index d1742c271..f5233e3b0 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -693,7 +693,7 @@ ARTY.db={ --- Arty script version. -- @field #string version -ARTY.version="1.1.6" +ARTY.version="1.1.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3848,26 +3848,23 @@ end -- @param #boolean final True if it is the final waypoint. function ARTY._PassingWaypoint(group, arty, i, final) - -- Debug message. - local text=string.format("%s, passing waypoint %d.", group:GetName(), i) - if final then - text=string.format("%s, arrived at destination.", group:GetName()) - end - arty:T(self.lid..text) + if group and group:IsAlive() then + + local groupname=tostring(group:GetName()) - --[[ - if final then - MESSAGE:New(text, 10):ToCoalitionIf(group:GetCoalition(), arty.Debug or arty.report) - else - MESSAGE:New(text, 10):ToAllIf(arty.Debug) + -- Debug message. + local text=string.format("%s, passing waypoint %d.", groupname, i) + if final then + text=string.format("%s, arrived at destination.", groupname) + end + arty:T(arty.lid..text) + + -- Arrived event. + if final and arty.groupname==groupname then + arty:Arrived() + end + end - ]] - - -- Arrived event. - if final and arty.groupname==group:GetName() then - arty:Arrived() - end - end --- Relocate to another position, e.g. after an engagement to avoid couter strikes. diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index e3effd07c..37530551f 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -275,7 +275,7 @@ AIRBASE.PersianGulf = { ["Lavan_Island_Airport"] = "Lavan Island Airport", ["Liwa_Airbase"] = "Liwa Airbase", ["Qeshm_Island"] = "Qeshm Island", - ["Ras_Al_Khaimah_International_Airport"] = "Ras Al Khaimah International Airport", + ["Ras_Al_Khaimah"] = "Ras Al Khaimah", ["Sas_Al_Nakheel_Airport"] = "Sas Al Nakheel Airport", ["Sharjah_Intl"] = "Sharjah Intl", ["Shiraz_International_Airport"] = "Shiraz International Airport", From d5b8ed62ae49f202ae08f7a084bea83e11930836 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 2 Jan 2020 20:15:42 +0100 Subject: [PATCH 416/485] AI_AIR - Fixed call to .Resume function if AI_A2A_CAP is calling. --- Moose Development/Moose/AI/AI_Air.lua | 12 +++++++++++- Moose Development/Moose/AI/AI_Air_Patrol.lua | 1 - Moose Development/Moose/Core/Base.lua | 3 ++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index eeb9053b7..c829cc6e9 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -748,10 +748,20 @@ function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) AIGroup:OptionROEHoldFire() AIGroup:OptionROTEvadeFire() + + -- Get Class name for .Resume function + local classname=self:GetClassName() + + -- AI_A2A_CAP can call this function but does not have a .Resume function. Try to fix. + local fsm=self + if classname=="AI_A2A_CAP" then + fsm=self:GetParent(self, AI_A2A_CAP) + classname=fsm:GetClassName() + end local Tasks = {} Tasks[#Tasks+1] = AIGroup:TaskRefueling() - Tasks[#Tasks+1] = AIGroup:TaskFunction( self:GetClassName() .. ".Resume", self ) + Tasks[#Tasks+1] = AIGroup:TaskFunction( classname .. ".Resume", fsm ) RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks ) AIGroup:Route( RefuelRoute, self.TaskDelay ) diff --git a/Moose Development/Moose/AI/AI_Air_Patrol.lua b/Moose Development/Moose/AI/AI_Air_Patrol.lua index 6458c80d1..32bd99cea 100644 --- a/Moose Development/Moose/AI/AI_Air_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Air_Patrol.lua @@ -356,7 +356,6 @@ function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) local taskOrbit=AIPatrol:TaskOrbit(c1, altitude, UTILS.KmphToMps(speedkmh), c2) -- Task function to redo the patrol at other random position. - --local taskPatrol=AIPatrol:TaskFunction("AI_A2A_PATROL.PatrolRoute", self) local taskPatrol=AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute", self) -- Controlled task with task condition. diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index d20f068ee..e95eac8ce 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -298,7 +298,8 @@ end -- -- -- @param #BASE self --- @param #BASE Child is the Child class from which the Parent class needs to be retrieved. +-- @param #BASE Child This is the Child class from which the Parent class needs to be retrieved. +-- @param #BASE FromClass (Optional) The class from which to get the parent. -- @return #BASE function BASE:GetParent( Child, FromClass ) From aa6515e1cabae024e237b947095a9384ceb19e6f Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 2 Jan 2020 22:50:35 +0100 Subject: [PATCH 417/485] AI_AIR - Fixed onafterRefuel --- Moose Development/Moose/AI/AI_A2A_Cap.lua | 2 +- Moose Development/Moose/AI/AI_Air.lua | 40 ++++++++++------------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Cap.lua b/Moose Development/Moose/AI/AI_A2A_Cap.lua index ae4171ae5..a5bee30e4 100644 --- a/Moose Development/Moose/AI/AI_A2A_Cap.lua +++ b/Moose Development/Moose/AI/AI_A2A_Cap.lua @@ -119,7 +119,7 @@ function AI_A2A_CAP:New2( AICap, EngageMinSpeed, EngageMaxSpeed, EngageFloorAlti local AI_Air = AI_AIR:New( AICap ) local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AICap, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) - local self = BASE:Inherit( self, AI_Air_Engage ) + local self = BASE:Inherit( self, AI_Air_Engage ) --#AI_A2A_CAP self:SetFuelThreshold( .2, 60 ) self:SetDamageThreshold( 0.4 ) diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index c829cc6e9..5fbd84349 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -707,13 +707,14 @@ end function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) self:F( { AIGroup, From, Event, To } ) - if AIGroup and AIGroup:IsAlive() then + + -- Get tanker group. local Tanker = GROUP:FindByName( self.TankerName ) - if Tanker:IsAlive() and Tanker:IsAirPlane() then + if Tanker and Tanker:IsAlive() and Tanker:IsAirPlane() then - self:I( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. "), at tanker " .. self.TankerName ) + self:I( "Group " .. self.Controllable:GetName() .. " ... Refuelling! State=" .. self:GetState() .. ", Refuelling tanker " .. self.TankerName ) local RefuelRoute = {} @@ -724,27 +725,15 @@ function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) 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 - ) + 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 = FromRefuelCoord:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToRefuelSpeed, - true - ) + --- Create a route point of type air. NOT used! + local ToRefuelRoutePoint = Tanker:GetCoordinate():WaypointAir(self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToRefuelSpeed, true) self:F( { ToRefuelSpeed = ToRefuelSpeed } ) RefuelRoute[#RefuelRoute+1] = FromRefuelRoutePoint - --RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint + RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint AIGroup:OptionROEHoldFire() AIGroup:OptionROTEvadeFire() @@ -753,21 +742,26 @@ function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) local classname=self:GetClassName() -- AI_A2A_CAP can call this function but does not have a .Resume function. Try to fix. - local fsm=self if classname=="AI_A2A_CAP" then - fsm=self:GetParent(self, AI_A2A_CAP) - classname=fsm:GetClassName() + classname="AI_AIR_PATROL" end + + env.info("FF refueling classname="..classname) local Tasks = {} Tasks[#Tasks+1] = AIGroup:TaskRefueling() - Tasks[#Tasks+1] = AIGroup:TaskFunction( classname .. ".Resume", fsm ) + Tasks[#Tasks+1] = AIGroup:TaskFunction( classname .. ".Resume", self ) RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks ) AIGroup:Route( RefuelRoute, self.TaskDelay ) + else + + -- No tanker defined ==> RTB! self:RTB() + end + end end From 09ed562f617d49eb252e77c50c3b6656fd0ec7ae Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 4 Jan 2020 00:15:00 +0100 Subject: [PATCH 418/485] SUPPRESSION & AIRBOSS - Fixed bug/typo in SUPPRESSION:SetHomeBase() function. - Added AIRBOSS Marshal event. --- .../Moose/Functional/Suppression.lua | 12 +++-- Moose Development/Moose/Ops/Airboss.lua | 53 ++++++------------- .../Moose/Ops/RecoveryTanker.lua | 2 +- 3 files changed, 27 insertions(+), 40 deletions(-) diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua index 073c009bd..92e6c170b 100644 --- a/Moose Development/Moose/Functional/Suppression.lua +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -266,7 +266,10 @@ SUPPRESSION={ } --- Enumerator of possible rules of engagement. --- @field #list ROE +-- @type SUPPRESSION.ROE +-- @field #string Hold Hold fire. +-- @field #string Free Weapon fire. +-- @field #string Return Return fire. SUPPRESSION.ROE={ Hold="Weapon Hold", Free="Weapon Free", @@ -274,7 +277,10 @@ SUPPRESSION.ROE={ } --- Enumerator of possible alarm states. --- @field #list AlarmState +-- @type SUPPRESSION.AlarmState +-- @field #string Auto Automatic. +-- @field #string Green Green. +-- @field #string Red Red. SUPPRESSION.AlarmState={ Auto="Auto", Green="Green", @@ -287,7 +293,7 @@ SUPPRESSION.MenuF10=nil --- PSEUDOATC version. -- @field #number version -SUPPRESSION.version="0.9.2" +SUPPRESSION.version="0.9.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index da53af408..83502fa80 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1691,7 +1691,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.1.0" +AIRBOSS.version="1.1.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -2081,6 +2081,7 @@ function AIRBOSS:New(carriername, alias) self:AddTransition("*", "RecoveryCase", "*") -- Switch to another case recovery. self:AddTransition("*", "PassingWaypoint", "*") -- Carrier is passing a waypoint. self:AddTransition("*", "LSOGrade", "*") -- LSO grade. + self:AddTransition("*", "Marshal", "*") -- A flight was send into the marshal stack. self:AddTransition("*", "Save", "*") -- Save player scores to file. self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS FMS. @@ -2254,29 +2255,6 @@ function AIRBOSS:New(carriername, alias) -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. - --- Triggers the FSM event "LSOgrade". Called when the LSO grades a player - -- @function [parent=#AIRBOSS] LSOgrade - -- @param #AIRBOSS self - -- @param #AIRBOSS.PlayerData playerData Player Data. - -- @param #AIRBOSS.LSOgrade grade LSO grade. - - --- Triggers the FSM event "LSOgrade". Delayed called when the LSO grades a player. - -- @function [parent=#AIRBOSS] __LSOgrade - -- @param #AIRBOSS self - -- @param #number delay Delay in seconds. - -- @param #AIRBOSS.PlayerData playerData Player Data. - -- @param #AIRBOSS.LSOgrade grade LSO grade. - - --- On after "LSOgrade" user function. Called when the carrier passes a waypoint of its route. - -- @function [parent=#AIRBOSS] OnAfterLSOgrade - -- @param #AIRBOSS self - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param #AIRBOSS.PlayerData playerData Player Data. - -- @param #AIRBOSS.LSOgrade grade LSO grade. - - --- Triggers the FSM event "LSOGrade". Called when the LSO grades a player -- @function [parent=#AIRBOSS] LSOGrade -- @param #AIRBOSS self @@ -2300,27 +2278,24 @@ function AIRBOSS:New(carriername, alias) -- @param #AIRBOSS.LSOgrade grade LSO grade. - --- Triggers the FSM event "LSOGrade". Called when the LSO grades a player - -- @function [parent=#AIRBOSS] LSOGrade + --- Triggers the FSM event "Marshal". Called when a flight is send to the Marshal stack. + -- @function [parent=#AIRBOSS] Marshal -- @param #AIRBOSS self - -- @param #AIRBOSS.PlayerData playerData Player Data. - -- @param #AIRBOSS.LSOgrade grade LSO grade. + -- @param #AIRBOSS.FlightGroup flight The flight group data. - --- Triggers the FSM event "LSOGrade". Delayed called when the LSO grades a player. - -- @function [parent=#AIRBOSS] __LSOGrade + --- Triggers the FSM event "Marshal". Delayed call when a flight is send to the Marshal stack. + -- @function [parent=#AIRBOSS] __Marshal -- @param #AIRBOSS self -- @param #number delay Delay in seconds. - -- @param #AIRBOSS.PlayerData playerData Player Data. - -- @param #AIRBOSS.LSOgrade grade LSO grade. + -- @param #AIRBOSS.FlightGroup flight The flight group data. - --- On after "LSOGrade" user function. Called when the carrier passes a waypoint of its route. - -- @function [parent=#AIRBOSS] OnAfterLSOGrade + --- On after "Marshal" user function. Called when a flight is send to the Marshal stack. + -- @function [parent=#AIRBOSS] OnAfterMarshal -- @param #AIRBOSS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. - -- @param #AIRBOSS.PlayerData playerData Player Data. - -- @param #AIRBOSS.LSOgrade grade LSO grade. + -- @param #AIRBOSS.FlightGroup flight The flight group data. --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. @@ -6187,6 +6162,9 @@ function AIRBOSS:_MarshalPlayer(playerData, stack) -- Set stack flag. flight.flag=stack + + -- Trigger Marshal event. + self:Marshal(flight) end else @@ -6443,6 +6421,9 @@ function AIRBOSS:_MarshalAI(flight, nstack, respawn) -- Route group. flight.group:Route(wp, 1) + + -- Trigger Marshal event. + self:Marshal(flight) end diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua index b779f8efc..e43cd9cc7 100644 --- a/Moose Development/Moose/Ops/RecoveryTanker.lua +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -629,7 +629,7 @@ function RECOVERYTANKER:SetHomeBase(airbase, terminaltype) if not self.airbase then self:E(self.lid.."ERROR: Airbase is nil!") end - if termialtype then + if terminaltype then self.terminaltype=terminaltype end return self From fcfcfeae00cf234ba1bad12468a2b7862b8c9d5f Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 6 Jan 2020 00:15:44 +0100 Subject: [PATCH 419/485] Misc --- .../Moose/AI/AI_Cargo_Dispatcher.lua | 4 + Moose Development/Moose/Core/Set.lua | 4 +- Moose Development/Moose/Core/Spawn.lua | 7 +- .../Moose/Functional/Suppression.lua | 12 +- .../Moose/Functional/ZoneCaptureCoalition.lua | 6 + Moose Development/Moose/Utilities/Utils.lua | 2 +- .../Moose/Wrapper/Controllable.lua | 161 ++++++++++++++++++ Moose Development/Moose/Wrapper/Unit.lua | 39 +++++ 8 files changed, 223 insertions(+), 12 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua index 311a07b59..424a0f814 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua @@ -1151,6 +1151,10 @@ function AI_CARGO_DISPATCHER:onafterMonitor() self.PickupCargo[Carrier] = CargoCoordinate PickupCargo = Cargo break + else + local text=string.format("WARNING: Cargo %s is too heavy to be loaded into transport. Cargo weight %.1f > %.1f load capacity of carrier %s.", + tostring(Cargo:GetName()), Cargo:GetWeight(), LargestLoadCapacity, tostring(Carrier:GetName())) + self:I(text) end end end diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 149d22d23..dedec77c9 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -1576,8 +1576,8 @@ do -- SET_GROUP --- Iterate the SET_GROUP and count how many GROUPs and UNITs are alive. -- @param #SET_GROUP self - -- @return #number The number of GROUPs completely in the Zone - -- @return #number The number of UNITS alive. + -- @return #number The number of GROUPs alive. + -- @return #number The number of UNITs alive. function SET_GROUP:CountAlive() local CountG = 0 local CountU = 0 diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 4865754ca..200d8a7a0 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -214,9 +214,8 @@ -- ### **Scheduled** spawning methods -- -- * @{#SPAWN.SpawnScheduled}(): Spawn groups at scheduled but randomized intervals. --- * @{#SPAWN.SpawnScheduledStart}(): Start or continue to spawn groups at scheduled time intervals. --- * @{#SPAWN.SpawnScheduledStop}(): Stop the spawning of groups at scheduled time intervals. --- +--- * @{#SPAWN.SpawnScheduleStart}(): Start or continue to spawn groups at scheduled time intervals. +-- * @{#SPAWN.SpawnScheduleStop}(): Stop the spawning of groups at scheduled time intervals. -- -- -- ## Retrieve alive GROUPs spawned by the SPAWN object @@ -260,7 +259,7 @@ -- immediately when :SpawnScheduled() is initiated. The methods @{#SPAWN.InitDelayOnOff}() and @{#SPAWN.InitDelayOn}() can be used to -- activate a delay before the first @{Wrapper.Group} is spawned. For completeness, a method @{#SPAWN.InitDelayOff}() is also available, that -- can be used to switch off the initial delay. Because there is no delay by default, this method would only be used when a --- @{#SPAWN.SpawnScheduledStop}() ; @{#SPAWN.SpawnScheduledStart}() sequence would have been used. +-- @{#SPAWN.SpawnScheduleStop}() ; @{#SPAWN.SpawnScheduleStart}() sequence would have been used. -- -- -- @field #SPAWN SPAWN diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua index 92e6c170b..c66cc1906 100644 --- a/Moose Development/Moose/Functional/Suppression.lua +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -22,7 +22,7 @@ -- -- The implementation is based on an idea and script by MBot. See the [DCS forum threat](https://forums.eagle.ru/showthread.php?t=107635) for details. -- --- In addition to suppressing the fire, conditions can be specified which let the group retreat to a defined zone, move away from the attacker +-- In addition to suppressing the fire, conditions can be specified, which let the group retreat to a defined zone, move away from the attacker -- or hide at a nearby scenery object. -- -- ==== @@ -882,10 +882,11 @@ function SUPPRESSION:StatusReport(message) local state=self.CurrentAlarmState local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife() local ammotot=group:GetAmmunition() + local detectedG=group:GetDetectedGroupSet():CountAlive() + local detectedU=group:GetDetectedUnitSet():Count() - - local text=string.format("State %s, Units=%d/%d, ROE=%s, AlarmState=%s, Hits=%d, Life(min/max/ave/ave0)=%d/%d/%d/%d, Total Ammo=%d", - self:GetState(), nunits, self.IniGroupStrength, self.CurrentROE, self.CurrentAlarmState, self.Nhit, life_min, life_max, life_ave, life_ave0, ammotot) + local text=string.format("State %s, Units=%d/%d, ROE=%s, AlarmState=%s, Hits=%d, Life(min/max/ave/ave0)=%d/%d/%d/%d, Total Ammo=%d, Detected=%d/%d", + self:GetState(), nunits, self.IniGroupStrength, self.CurrentROE, self.CurrentAlarmState, self.Nhit, life_min, life_max, life_ave, life_ave0, ammotot, detectedG, detectedU) MESSAGE:New(text, 10):ToAllIf(message or self.Debug) self:I(self.lid..text) @@ -997,11 +998,12 @@ function SUPPRESSION:onafterStatus(Controllable, From, Event, To) local nunits=group:CountAliveUnits() -- Check if there are units. - if nunits>0 then + if nunits>0 then -- Retreat if completely out of ammo and retreat zone defined. local nammo=group:GetAmmunition() if nammo==0 and self.RetreatZone then + self:I(self.lid..string.format("Out of ammo!")) self:Retreat() end diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 81c2aefd1..1e9417e96 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -874,6 +874,12 @@ do -- ZONE_CAPTURE_COALITION self:Capture() end + if self:IsAttacked() then + local unitset=self:GetScannedSetUnit() --Core.Set#SET_UNIT + + unitset:ForEachUnitInZone(self) + end + -- Status text. local text=string.format("CAPTURE ZONE %s: Owner=%s (Previous=%s): Status %s", self:GetZoneName(), self:GetCoalitionName(), UTILS.GetCoalitionName(self:GetPreviousCoalition()), State) local NewState = self:GetState() diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index acfc30323..26ef8c76e 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -78,7 +78,7 @@ CALLSIGN={ }, -- AWACS AWACS={ - Overloard=1, + Overlord=1, Magic=2, Wizard=3, Focus=4, diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index b42085516..28eaff805 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -2802,7 +2802,9 @@ function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRad self:F2( self.ControllableName ) local DCSControllable = self:GetDCSObject() + if DCSControllable then + local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil @@ -2841,7 +2843,25 @@ function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRad end --- Check if a target is detected. +-- The optional parametes specify the detection methods that can be applied. +-- If **no** detection method is given, the detection will use **all** the available methods by default. +-- If **at least one** detection method is specified, only the methods set to *true* will be used. -- @param Wrapper.Controllable#CONTROLLABLE self +-- @param DCS#Object DCSObject The DCS object that is checked. +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return #boolean True if target is detected. +-- @return #boolean True if target is visible by line of sight. +-- @return #number Mission time when target was detected. +-- @return #boolean True if target type is known. +-- @return #boolean True if distance to target is known. +-- @return DCS#Vec3 Last known position vector of the target. +-- @return DCS#Vec3 Last known velocity vector of the target. function CONTROLLABLE:IsTargetDetected( DCSObject, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) self:F2( self.ControllableName ) @@ -2867,6 +2887,147 @@ function CONTROLLABLE:IsTargetDetected( DCSObject, DetectVisual, DetectOptical, return nil end +--- Check if a certain UNIT is detected by the controllable. +-- The optional parametes specify the detection methods that can be applied. +-- If **no** detection method is given, the detection will use **all** the available methods by default. +-- If **at least one** detection method is specified, only the methods set to *true* will be used. +-- @param #CONTROLLABLE self +-- @param Wrapper.Unit#UNIT Unit The unit that is supposed to be detected. +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return #boolean True if target is detected. +-- @return #boolean True if target is visible by line of sight. +-- @return #number Mission time when target was detected. +-- @return #boolean True if target type is known. +-- @return #boolean True if distance to target is known. +-- @return DCS#Vec3 Last known position vector of the target. +-- @return DCS#Vec3 Last known velocity vector of the target. +function CONTROLLABLE:IsUnitDetected( Unit, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) + self:F2( self.ControllableName ) + + if Unit and Unit:IsAlive() then + return self:IsTargetDetected(Unit:GetDCSObject(), DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + end + + return nil +end + +--- Check if a certain GROUP is detected by the controllable. +-- The optional parametes specify the detection methods that can be applied. +-- If **no** detection method is given, the detection will use **all** the available methods by default. +-- If **at least one** detection method is specified, only the methods set to *true* will be used. +-- @param #CONTROLLABLE self +-- @param Wrapper.Group#GROUP Group The group that is supposed to be detected. +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return #boolean True if any unit of the group is detected. +function CONTROLLABLE:IsGroupDetected( Group, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) + self:F2( self.ControllableName ) + + if Group and Group:IsAlive() then + for _,_unit in pairs(Group:GetUnits()) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + + local isdetected=self:IsUnitDetected(unit, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + if isdetected then + return true + end + end + end + return false + end + + return nil +end + + +--- Return the detected targets of the controllable. +-- The optional parametes specify the detection methods that can be applied. +-- If **no** detection method is given, the detection will use **all** the available methods by default. +-- If **at least one** detection method is specified, only the methods set to *true* will be used. +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return Core.Set#SET_UNIT Set of detected units. +function CONTROLLABLE:GetDetectedUnitSet(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + -- Get detected DCS units. + local detectedtargets=self:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + local unitset=SET_UNIT:New() + + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do + local DetectedObject=Detection.object -- DCS#Object + + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then + local unit=UNIT:Find(DetectedObject) + + if unit and unit:IsAlive() then + + if not unitset:FindUnit(unit:GetName()) then + unitset:AddUnit(unit) + end + + end + end + end + + return unitset +end + +--- Return the detected target groups of the controllable as a @{SET_GROUP}. +-- The optional parametes specify the detection methods that can be applied. +-- If no detection method is given, the detection will use all the available methods by default. +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return Core.Set#SET_GROUP Set of detected groups. +function CONTROLLABLE:GetDetectedGroupSet(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + -- Get detected DCS units. + local detectedtargets=self:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + local groupset=SET_GROUP:New() + + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do + local DetectedObject=Detection.object -- DCS#Object + + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then + local unit=UNIT:Find(DetectedObject) + + if unit and unit:IsAlive() then + local group=unit:GetGroup() + + if group and not groupset:FindGroup(group:GetName()) then + groupset:AddGroup(group) + end + + end + end + end + + return groupset +end + + -- Options --- Can the CONTROLLABLE hold their weapons? diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 7f974be19..c747bc92e 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -1165,6 +1165,45 @@ do -- Detection return IsLOS end + + --- Forces the unit to become aware of the specified target, without the unit manually detecting the other unit itself. + -- Applies only to a Unit Controller. Cannot be used at the group level. + -- @param #UNIT self + -- @param #UNIT TargetUnit The unit to be known. + -- @param #boolean TypeKnown The target type is known. If *false*, the type is not known. + -- @param #boolean DistanceKnown The distance to the target is known. If *false*, distance is unknown. + function UNIT:KnowUnit(TargetUnit, TypeKnown, DistanceKnown) + + -- Defaults. + if TypeKnown~=false then + TypeKnown=true + end + if DistanceKnown~=false then + DistanceKnown=true + end + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local Controller = DCSControllable:getController() --self:_GetController() + + if Controller then + + local object=TargetUnit:GetDCSObject() + + if object then + + self:I(string.format("Unit %s now knows target unit %s. Type known=%s, distance known=%s", self:GetName(), TargetUnit:GetName(), tostring(TypeKnown), tostring(DistanceKnown))) + + Controller:knowTarget(object, TypeKnown, DistanceKnown) + + end + + end + + end + + end end \ No newline at end of file From 4661f6981ef68d40b0be8951207b7fc884745bc4 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 6 Jan 2020 21:09:21 +0100 Subject: [PATCH 420/485] Misc --- Moose Development/Moose/Core/Set.lua | 14 ---- Moose Development/Moose/Core/Zone.lua | 2 +- .../Moose/Functional/Suppression.lua | 76 ++++++++++++------- .../Moose/Functional/ZoneCaptureCoalition.lua | 14 ++-- Moose Development/Moose/Ops/ATIS.lua | 10 ++- Moose Development/Moose/Utilities/Utils.lua | 20 +++++ 6 files changed, 85 insertions(+), 51 deletions(-) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index dedec77c9..58cb5c386 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -2186,20 +2186,6 @@ do -- SET_UNIT 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 diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index a1db93d70..802656890 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -949,7 +949,7 @@ function ZONE_RADIUS:SearchZone( EvaluateFunction, ObjectCategories ) local function EvaluateZone( ZoneDCSUnit ) - env.info( ZoneDCSUnit:getName() ) + --env.info( ZoneDCSUnit:getName() ) local ZoneUnit = UNIT:Find( ZoneDCSUnit ) diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua index c66cc1906..1698f0c0a 100644 --- a/Moose Development/Moose/Functional/Suppression.lua +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -83,6 +83,8 @@ -- @field #string DefaultAlarmState Alarm state the group will go to when it is changed back from another state. Default is "Auto". -- @field #string DefaultROE ROE the group will get once suppression is over. Default is "Free". -- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true. +-- @field Core.Zone#ZONE BattleZone +-- @field #boolean AutoEngage -- @extends Core.Fsm#FSM_CONTROLLABLE -- @@ -307,8 +309,7 @@ SUPPRESSION.version="0.9.3" --- Creates a new AI_suppression object. -- @param #SUPPRESSION self -- @param Wrapper.Group#GROUP group The GROUP object for which suppression should be applied. --- @return #SUPPRESSION SUPPRESSION object. --- @return nil If group does not exist or is not a ground group. +-- @return #SUPPRESSION SUPPRESSION object or *nil* if group does not exist or is not a ground group. function SUPPRESSION:New(group) -- Inherits from FSM_CONTROLLABLE @@ -333,18 +334,16 @@ function SUPPRESSION:New(group) self:SetControllable(group) -- Get DCS descriptors of group. - local DCSgroup=Group.getByName(group:GetName()) - local DCSunit=DCSgroup:getUnit(1) - self.DCSdesc=DCSunit:getDesc() + self.DCSdesc=group:GetDCSDesc(1) -- Get max speed the group can do and convert to km/h. - self.SpeedMax=self.DCSdesc.speedMaxOffRoad*3.6 + self.SpeedMax=group:GetSpeedMax() -- Set speed to maximum. self.Speed=self.SpeedMax -- Is this infantry or not. - self.IsInfantry=DCSunit:hasAttribute("Infantry") + self.IsInfantry=group:GetUnit(1):HasAttribute("Infantry") -- Type of group. self.Type=group:GetTypeName() @@ -368,6 +367,7 @@ function SUPPRESSION:New(group) self:AddTransition("TakingCover", "FightBack", "CombatReady") self:AddTransition("FallingBack", "FightBack", "CombatReady") self:AddTransition("Retreating", "Retreated", "Retreated") + self:AddTransition("*", "OutOfAmmo", "*") self:AddTransition("*", "Dead", "*") self:AddTransition("*", "Stop", "Stopped") @@ -598,6 +598,24 @@ function SUPPRESSION:New(group) -- @param #string To To state. + --- Trigger "OutOfAmmo" event. + -- @function [parent=#SUPPRESSION] OutOfAmmo + -- @param #SUPPRESSION self + + --- Trigger "OutOfAmmo" event after a delay. + -- @function [parent=#SUPPRESSION] __OutOfAmmo + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + + --- User function for OnAfter "OutOfAmmo" event. + -- @function [parent=#SUPPRESSION] OnAfterOutOfAmmo + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Trigger "Dead" event. -- @function [parent=#SUPPRESSION] Dead -- @param #SUPPRESSION self @@ -1002,9 +1020,8 @@ function SUPPRESSION:onafterStatus(Controllable, From, Event, To) -- Retreat if completely out of ammo and retreat zone defined. local nammo=group:GetAmmunition() - if nammo==0 and self.RetreatZone then - self:I(self.lid..string.format("Out of ammo!")) - self:Retreat() + if nammo==0 then + self:OutOfAmmo() end -- Status report. @@ -1016,34 +1033,18 @@ function SUPPRESSION:onafterStatus(Controllable, From, Event, To) end else + -- Stop FSM as there are no units left. self:Stop() end else + -- Stop FSM as there group object does not exist. self:Stop() end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Before "Hit" event. (Of course, this is not really before the group got hit.) --- @param #SUPPRESSION self --- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Wrapper.Unit#UNIT Unit Unit that was hit. --- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. --- @return boolean -function SUPPRESSION:onbeforeHit(Controllable, From, Event, To, Unit, AttackUnit) - self:_EventFromTo("onbeforeHit", Event, From, To) - - --local Tnow=timer.getTime() - --env.info(self.lid..string.format("Last hit = %s %s", tostring(self.LastHit), tostring(Tnow))) - - return true -end - --- After "Hit" event. -- @param #SUPPRESSION self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -1314,6 +1315,25 @@ function SUPPRESSION:onafterTakeCover(Controllable, From, Event, To, Hideout) end +--- After "OutOfAmmo" event. Triggered when group is completely out of ammo. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterOutOfAmmo(Controllable, From, Event, To) + self:_EventFromTo("onafterOutOfAmmo", Event, From, To) + + -- Info to log. + self:I(self.lid..string.format("Out of ammo!")) + + -- Order retreat if retreat zone was specified. + if self.RetreatZone then + self:Retreat() + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Before "Retreat" event. We check that the group is not already retreating. diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 1e9417e96..4b0801681 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -874,14 +874,16 @@ do -- ZONE_CAPTURE_COALITION self:Capture() end - if self:IsAttacked() then - local unitset=self:GetScannedSetUnit() --Core.Set#SET_UNIT - - unitset:ForEachUnitInZone(self) - end + -- Get red and blue unit sets. + local unitsetRed=self:GetScannedSetUnit():FilterCoalitions(coalition.side.RED):FilterActive(true):FilterOnce() + local unitsetBlu=self:GetScannedSetUnit():FilterCoalitions(coalition.side.BLUE):FilterActive(true):FilterOnce() + + -- Count number of units. + local nRed=unitsetRed:Count() + local nBlu=unitsetBlu:Count() -- Status text. - local text=string.format("CAPTURE ZONE %s: Owner=%s (Previous=%s): Status %s", self:GetZoneName(), self:GetCoalitionName(), UTILS.GetCoalitionName(self:GetPreviousCoalition()), State) + local text=string.format("CAPTURE ZONE %s: Owner=%s (Previous=%s): #blue=%d, #red=%d, Status %s", self:GetZoneName(), self:GetCoalitionName(), UTILS.GetCoalitionName(self:GetPreviousCoalition()), nBlu, nRed, State) local NewState = self:GetState() if NewState~=State then text=text..string.format(" --> %s", NewState) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 6d7a939bd..a1b5f4866 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -515,7 +515,7 @@ _ATIS={} --- ATIS class version. -- @field #string version -ATIS.version="0.6.0" +ATIS.version="0.6.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -996,9 +996,15 @@ function ATIS:onafterStatus(From, Event, To) -- Get FSM state. local fsmstate=self:GetState() + + local relayunitstatus="N/A" + if self.relayunitname then + local ru=UNIT:FindByName(self.relayunitname) + relayunitstatus=tostring(ru:IsAlive()) + end -- Info text. - local text=string.format("State %s", fsmstate) + local text=string.format("State %s: Freq=%.3f MHz %s, Relay unit=%s (alive=%s)", fsmstate, self.frequency, UTILS.GetModulationName(self.modulation), tostring(self.relayunitname), relayunitstatus) self:I(self.lid..text) self:__Status(-60) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 26ef8c76e..766b8de47 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1060,3 +1060,23 @@ function UTILS.GetCoalitionName(Coalition) end +--- Get the modulation name from its numerical value. +-- @param #number Modulation The modulation enumerator number. Can be either 0 or 1. +-- @return #string The modulation name, i.e. "AM"=0 or "FM"=1. Anything else will return "Unknown". +function UTILS.GetModulationName(Modulation) + + if Modulation then + if Modulation==0 then + return "AM" + elseif Modulation==1 then + return "FM" + else + return "Unknown" + end + else + return "Unknown" + end + +end + + From 6000421957d8262d0d6df19763ff08543c66dcd2 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 6 Jan 2020 21:54:59 +0100 Subject: [PATCH 421/485] Update Fox.lua --- Moose Development/Moose/Functional/Fox.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index 1963afd83..5c5ec9f20 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -961,7 +961,7 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) end if target then - self:I(self.lid..string.format("Missile %s with NO explicit target got closest unit to missile as target %s. Dist=%s m", missile.missileType, target:GetName(), tostring(mindist))) + self:T(self.lid..string.format("Missile %s with NO explicit target got closest unit to missile as target %s. Dist=%s m", missile.missileType, target:GetName(), tostring(mindist))) end end @@ -1066,7 +1066,7 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) else -- Destroy missile. - self:I(self.lid..string.format("Missile %s(%s) fired by %s has no current target. Checking back in 0.1 sec.", missile.missileType, missile.missileName, missile.shooterName)) + self:T(self.lid..string.format("Missile %s(%s) fired by %s has no current target. Checking back in 0.1 sec.", missile.missileType, missile.missileName, missile.shooterName)) return timer.getTime()+0.1 -- No target ==> terminate timer. From 3e8455410eba8ac3a18eb90804333aca436b4b9d Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 7 Jan 2020 23:34:36 +0100 Subject: [PATCH 422/485] ZCC, ATIS, CONTROLLABLE --- .../Moose/Functional/ZoneCaptureCoalition.lua | 4 ++-- Moose Development/Moose/Ops/ATIS.lua | 6 ++++-- .../Moose/Wrapper/Controllable.lua | 20 ++++++++++++------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 4b0801681..7371ef1bc 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -875,8 +875,8 @@ do -- ZONE_CAPTURE_COALITION end -- Get red and blue unit sets. - local unitsetRed=self:GetScannedSetUnit():FilterCoalitions(coalition.side.RED):FilterActive(true):FilterOnce() - local unitsetBlu=self:GetScannedSetUnit():FilterCoalitions(coalition.side.BLUE):FilterActive(true):FilterOnce() + local unitsetRed=self:GetScannedSetUnit():FilterCoalitions("red"):FilterActive(true):FilterOnce() + local unitsetBlu=self:GetScannedSetUnit():FilterCoalitions("blue"):FilterActive(true):FilterOnce() -- Count number of units. local nRed=unitsetRed:Count() diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index a1b5f4866..16329abd8 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -515,7 +515,7 @@ _ATIS={} --- ATIS class version. -- @field #string version -ATIS.version="0.6.1" +ATIS.version="0.6.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1000,7 +1000,9 @@ function ATIS:onafterStatus(From, Event, To) local relayunitstatus="N/A" if self.relayunitname then local ru=UNIT:FindByName(self.relayunitname) - relayunitstatus=tostring(ru:IsAlive()) + if ru then + relayunitstatus=tostring(ru:IsAlive()) + end end -- Info text. diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 28eaff805..86a62afb7 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -2136,8 +2136,10 @@ do -- Patrol methods -- @param #table ZoneList Table of zones. -- @param #number Speed Speed in km/h the group moves at. -- @param #string Formation (Optional) Formation the group should use. + -- @param #number DelayMin Delay in seconds before the group progresses to the next route point. Default 1 sec. + -- @param #number DelayMax Max. delay in seconds. Actual delay is randomly chosen between DelayMin and DelayMax. Default equal to DelayMin. -- @return #CONTROLLABLE - function CONTROLLABLE:PatrolZones( ZoneList, Speed, Formation ) + function CONTROLLABLE:PatrolZones( ZoneList, Speed, Formation, DelayMin, DelayMax ) if not type( ZoneList ) == "table" then ZoneList = { ZoneList } @@ -2148,14 +2150,18 @@ do -- Patrol methods if not self:IsInstanceOf( "GROUP" ) then PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP end + + DelayMin=DelayMin or 1 + if not DelayMax or DelayMax Date: Wed, 8 Jan 2020 12:04:45 +0100 Subject: [PATCH 423/485] ATIS v0.6.2 - Added F10 marker option. --- Moose Development/Moose/Ops/ATIS.lua | 63 +++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 16329abd8..2894f8949 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -82,6 +82,8 @@ -- @field #number runwaym2t Optional correction for magnetic to true runway heading conversion (and vice versa) in degrees. -- @field #boolean windtrue Report true (from) heading of wind. Default is magnetic. -- @field #boolean altimeterQNH Report altimeter QNH. +-- @field #boolean usemarker Use mark on the F10 map. +-- @field #number markerid Numerical ID of the F10 map mark point. -- @extends Core.Fsm#FSM --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde @@ -309,6 +311,8 @@ ATIS = { runwaym2t = nil, windtrue = nil, altimeterQNH = nil, + usemarker = nil, + markerid = nil, } --- NATO alphabet. @@ -570,6 +574,7 @@ function ATIS:New(airbasename, frequency, modulation) self:SetRunwayCorrectionMagnetic2True() self:SetRadioPower() self:SetAltimeterQNH(true) + self:SetMapMarks(false) -- Start State. self:SetStartState("Stopped") @@ -687,6 +692,19 @@ function ATIS:SetRadioPower(power) return self end +--- Use F10 map mark points. +-- @param #ATIS self +-- @param #boolean switch If *true* or *nil*, marks are placed on F10 map. If *false* this feature is set to off (default). +-- @return #ATIS self +function ATIS:SetMapMarks(switch) + if switch==nil or switch==true then + self.usemarker=true + else + self.usemarker=false + end + return self +end + --- Set magnetic runway headings as depicted on the runway, *e.g.* "13" for 130° or "25L" for the left runway with magnetic heading 250°. -- @param #ATIS self -- @param #table headings Magnetic headings. Inverse (-180°) headings are added automatically. You only need to specify one heading per runway direction. "L"eft and "R" right can also be appended. @@ -955,6 +973,12 @@ end -- @param #ATIS self function ATIS:onafterStart(From, Event, To) + -- Check that this is an airdrome. + if self.airbase:GetAirbaseCategory()~=Airbase.Category.AIRDROME then + self:E(self.lid..string.format("ERROR: Cannot start ATIS for airbase %s! Only AIRDROMES are supported but NOT FARPS or SHIPS.", self.airbasename)) + return + end + -- Info. self:I(self.lid..string.format("Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation)) @@ -1315,6 +1339,7 @@ function ATIS:onafterBroadcast(From, Event, To) -- Information tag subtitle=string.format("Information %s", NATO) + local _INFORMATION=subtitle self:Transmission(ATIS.Sound.Information, 0.5, subtitle) self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) @@ -1436,6 +1461,7 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2]) end end + local _ALTIMETER=subtitle self:Transmission(ATIS.Sound.Altimeter, 1.0, subtitle) self:Transmission(ATIS.Sound.QNH, 0.5) self.radioqueue:Number2Transmission(QNH[1]) @@ -1469,6 +1495,7 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=string.format("Temperature %s °C", TEMPERATURE) end end + local _TEMPERATURE=subtitle self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) if temperature<0 then self:Transmission(ATIS.Sound.Minus, 0.2) @@ -1489,6 +1516,7 @@ function ATIS:onafterBroadcast(From, Event, To) if turbulence>0 then subtitle=subtitle..", gusting" end + local _WIND=subtitle self:Transmission(ATIS.Sound.WindFrom, 1.0, subtitle) self.radioqueue:Number2Transmission(WINDFROM) self:Transmission(ATIS.Sound.At, 0.2) @@ -1509,6 +1537,7 @@ function ATIS:onafterBroadcast(From, Event, To) elseif rwyLeft==false then subtitle=subtitle.." Right" end + local _RUNACT=subtitle self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) self.radioqueue:Number2Transmission(runway) if rwyLeft==true then @@ -1536,7 +1565,7 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=subtitle.." feet" end - -- Transmitt. + -- Transmit. self:Transmission(ATIS.Sound.RunwayLength, 1.0, subtitle) if tonumber(L1000)>0 then self.radioqueue:Number2Transmission(L1000) @@ -1700,9 +1729,41 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=string.format("End of information %s", NATO) self:Transmission(ATIS.Sound.EndOfInformation, 0.5, subtitle) self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + + -- Update F10 marker. + if self.usemarker then + self:UpdateMarker(_INFORMATION, _RUNACT, _WIND, _ALTIMETER, _TEMPERATURE) + end end +--- Update F10 map marker. +-- @param #ATIS self +-- @param #string information Information tag text. +-- @param #string runact Active runway text. +-- @param #string wind Wind text. +-- @param #string altimeter Altimeter text. +-- @param #string temperature Temperature text. +-- @return #number Marker ID. +function ATIS:UpdateMarker(information, runact, wind, altimeter, temperature) + + if self.markerid then + self.airbase:GetCoordinate():RemoveMark(self.markerid) + end + + local text=string.format("ATIS on %.3f %s, %s:\n", self.frequency, UTILS.GetModulationName(self.modulation), tostring(information)) + text=text..string.format("%s\n", tostring(runact)) + text=text..string.format("%s\n", tostring(wind)) + text=text..string.format("%s\n", tostring(altimeter)) + text=text..string.format("%s", tostring(temperature)) + -- More info is not displayed on the marker! + + -- Place new mark + self.markerid=self.airbase:GetCoordinate():MarkToAll(text, true) + + return self.markerid +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- From 431e8a05f9313ffdbc78428ca323b9220e836380 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 8 Jan 2020 18:45:57 +0100 Subject: [PATCH 424/485] ZCC - Fixed unit counting in status report. --- Moose Development/Moose/Core/Zone.lua | 3 +-- .../Moose/Functional/ZoneCaptureCoalition.lua | 23 ++++++++++++------- Moose Development/Moose/Ops/ATIS.lua | 4 ++++ .../Moose/Wrapper/Controllable.lua | 1 + 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 802656890..1b55b5ba3 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -750,7 +750,7 @@ function ZONE_RADIUS:GetScannedUnits() end ---- Count the number of different coalitions inside the zone. +--- Get a set of scanned units. -- @param #ZONE_RADIUS self -- @return Core.Set#SET_UNIT Set of units and statics inside the zone. function ZONE_RADIUS:GetScannedSetUnit() @@ -949,7 +949,6 @@ function ZONE_RADIUS:SearchZone( EvaluateFunction, ObjectCategories ) local function EvaluateZone( ZoneDCSUnit ) - --env.info( ZoneDCSUnit:getName() ) local ZoneUnit = UNIT:Find( ZoneDCSUnit ) diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 7371ef1bc..e6a07e323 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -874,16 +874,23 @@ do -- ZONE_CAPTURE_COALITION self:Capture() end - -- Get red and blue unit sets. - local unitsetRed=self:GetScannedSetUnit():FilterCoalitions("red"):FilterActive(true):FilterOnce() - local unitsetBlu=self:GetScannedSetUnit():FilterCoalitions("blue"):FilterActive(true):FilterOnce() - - -- Count number of units. - local nRed=unitsetRed:Count() - local nBlu=unitsetBlu:Count() + -- Count stuff in zone. + local unitset=self:GetScannedSetUnit() + local nRed=0 + local nBlue=0 + for _,object in pairs(unitset:GetSet()) do + local coal=object:GetCoalition() + if object:IsAlive() then + if coal==coalition.side.RED then + nRed=nRed+1 + elseif coal==coalition.side.BLUE then + nBlue=nBlue+1 + end + end + end -- Status text. - local text=string.format("CAPTURE ZONE %s: Owner=%s (Previous=%s): #blue=%d, #red=%d, Status %s", self:GetZoneName(), self:GetCoalitionName(), UTILS.GetCoalitionName(self:GetPreviousCoalition()), nBlu, nRed, State) + local text=string.format("CAPTURE ZONE %s: Owner=%s (Previous=%s): #blue=%d, #red=%d, Status %s", self:GetZoneName(), self:GetCoalitionName(), UTILS.GetCoalitionName(self:GetPreviousCoalition()), nBlue, nRed, State) local NewState = self:GetState() if NewState~=State then text=text..string.format(" --> %s", NewState) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 2894f8949..8c51087d4 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -243,6 +243,10 @@ -- ![Banner Image](..\Presentations\ATIS\ATIS_SoundFolder.png) -- -- **Note** that the default folder name is *ATIS Soundfiles/*. If you want to change it, you can use the @{#ATIS.SetSoundfilesPath}(*path*), where *path* is the path of the directory. This must end with a slash "/"! +-- +-- # Marks on the F10 Map +-- +-- You can place marks on the F10 map via the @{#ATIS.SetMapMarks}() function. These will contain info about the ATIS frequency, the currently active runway and some basic info about the weather (wind, pressure and temperature). -- -- # Examples -- diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 86a62afb7..939345713 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -2380,6 +2380,7 @@ do -- Route methods local FromCoordinate = self:GetCoordinate() + --local FromWP = FromCoordinate:WaypointGround() local FromWP = FromCoordinate:WaypointGround(Speed, Formation) local ToWP = ToCoordinate:WaypointGround( Speed, Formation ) From e6e5787fc694fd196048fd92e42b649c951ddb2d Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 8 Jan 2020 19:26:40 +0100 Subject: [PATCH 425/485] Update Point.lua --- Moose Development/Moose/Core/Point.lua | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index fa8d58d65..57e2fc2cb 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1215,11 +1215,11 @@ do -- COORDINATE -- @param #COORDINATE self -- @param #number Speed (optional) Speed in km/h. The default speed is 20 km/h. -- @param #string Formation (optional) The route point Formation, which is a text string that specifies exactly the Text in the Type of the route point, like "Vee", "Echelon Right". + -- @param #table DCSTasks A table of DCS tasks that are executed at the waypoints. Mind the curly brackets {}! -- @return #table The route point. - function COORDINATE:WaypointGround( Speed, Formation ) + function COORDINATE:WaypointGround( Speed, Formation, DCSTasks ) self:F2( { Formation, Speed } ) - local RoutePoint = {} RoutePoint.x = self.x RoutePoint.y = self.z @@ -1242,12 +1242,10 @@ do -- COORDINATE -- }, -- end of ["params"] -- }, -- end of ["task"] - RoutePoint.task = {} RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} - RoutePoint.task.params.tasks = {} - + RoutePoint.task.params.tasks = DCSTasks or {} return RoutePoint end From 1e1154d190caf2149d3ed8f236466cbc5a344b5e Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 19 Jan 2020 22:14:45 +0100 Subject: [PATCH 426/485] controllable --- .../Moose/AI/AI_A2A_Dispatcher.lua | 2 +- Moose Development/Moose/Core/Zone.lua | 4 + .../Moose/Functional/Warehouse.lua | 42 +++++----- .../Moose/Wrapper/Controllable.lua | 79 ++++++++++++++++--- 4 files changed, 94 insertions(+), 33 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index b780591fe..0e13f7c2c 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -3656,7 +3656,7 @@ do -- AI_A2A_DISPATCHER for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do - self:I( { DefenderSquadron = DefenderSquadron.Name } ) + self:T( { DefenderSquadron = DefenderSquadron.Name } ) local Airbase = DefenderSquadron.Airbase local AirbaseCoordinate = Airbase:GetCoordinate() diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 1b55b5ba3..8ef630eee 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -689,6 +689,10 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) local ObjectCategory = ZoneObject:getCategory() + --local name=ZoneObject:getName() + --env.info(string.format("Zone object %s", tostring(name))) + --self:E(ZoneObject) + if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then local CoalitionDCSUnit = ZoneObject:getCoalition() diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index cc03d7b1b..bd98267cb 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -5957,27 +5957,29 @@ function WAREHOUSE:_OnEventArrived(EventData) if self.uid==wid then local request=self:_GetRequestOfGroup(group, self.pending) - local istransport=self:_GroupIsTransport(group,request) - - -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations. - local rightairbase=group:GetCoordinate():GetClosestAirbase():GetName()==request.warehouse:GetAirbase():GetName() - - -- Check that group is cargo and not transport. - if istransport==false and rightairbase then - - -- Debug info. - local text=string.format("Air asset group %s from warehouse %s arrived at its destination.", group:GetName(), self.alias) - self:_InfoMessage(text) - - -- Trigger arrived event for this group. Note that each unit of a group will trigger this event. So the onafterArrived function needs to take care of that. - -- Actually, we only take the first unit of the group that arrives. If it does, we assume the whole group arrived, which might not be the case, since - -- some units might still be taxiing or whatever. Therefore, we add 10 seconds for each additional unit of the group until the first arrived event is triggered. - local nunits=#group:GetUnits() - local dt=10*(nunits-1)+1 -- one unit = 1 sec, two units = 11 sec, three units = 21 sec before we call the group arrived. - self:__Arrived(dt, group) - + + if request then + local istransport=self:_GroupIsTransport(group,request) + + -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations. + local rightairbase=group:GetCoordinate():GetClosestAirbase():GetName()==request.warehouse:GetAirbase():GetName() + + -- Check that group is cargo and not transport. + if istransport==false and rightairbase then + + -- Debug info. + local text=string.format("Air asset group %s from warehouse %s arrived at its destination.", group:GetName(), self.alias) + self:_InfoMessage(text) + + -- Trigger arrived event for this group. Note that each unit of a group will trigger this event. So the onafterArrived function needs to take care of that. + -- Actually, we only take the first unit of the group that arrives. If it does, we assume the whole group arrived, which might not be the case, since + -- some units might still be taxiing or whatever. Therefore, we add 10 seconds for each additional unit of the group until the first arrived event is triggered. + local nunits=#group:GetUnits() + local dt=10*(nunits-1)+1 -- one unit = 1 sec, two units = 11 sec, three units = 21 sec before we call the group arrived. + self:__Arrived(dt, group) + + end end - end else diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 939345713..969cd2672 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1,4 +1,4 @@ ---- **Wrapper** -- CONTROLLABLE is an intermediate class wrapping Group and Unit classes "controllers". + --- **Wrapper** -- CONTROLLABLE is an intermediate class wrapping Group and Unit classes "controllers". -- -- === -- @@ -2375,16 +2375,30 @@ do -- Route methods -- @param #number Speed (optional) Speed in km/h. The default speed is 20 km/h. -- @param #string Formation (optional) The route point Formation, which is a text string that specifies exactly the Text in the Type of the route point, like "Vee", "Echelon Right". -- @param #number DelaySeconds Wait for the specified seconds before executing the Route. + -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{CONTROLLABLE} object, the number of the waypoint and the total number of waypoints. + -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*. -- @return #CONTROLLABLE The CONTROLLABLE. - function CONTROLLABLE:RouteGroundTo( ToCoordinate, Speed, Formation, DelaySeconds ) + function CONTROLLABLE:RouteGroundTo( ToCoordinate, Speed, Formation, DelaySeconds, WaypointFunction, WaypointFunctionArguments ) local FromCoordinate = self:GetCoordinate() - --local FromWP = FromCoordinate:WaypointGround() local FromWP = FromCoordinate:WaypointGround(Speed, Formation) local ToWP = ToCoordinate:WaypointGround( Speed, Formation ) - self:Route( { FromWP, ToWP }, DelaySeconds ) + local route={FromWP, ToWP} + + -- Add passing waypoint function. + if WaypointFunction then + local N=#route + for n,waypoint in pairs(route) do + waypoint.task = {} + waypoint.task.id = "ComboTask" + waypoint.task.params = {} + waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} + end + end + + self:Route( route, DelaySeconds ) return self end @@ -2395,8 +2409,10 @@ do -- Route methods -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. -- @param #number DelaySeconds (Optional) Wait for the specified seconds before executing the Route. Default is one second. -- @param #string OffRoadFormation (Optional) The formation at initial and final waypoint. Default is "Off Road". + -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{CONTROLLABLE} object, the number of the waypoint and the total number of waypoints. + -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*. -- @return #CONTROLLABLE The CONTROLLABLE. - function CONTROLLABLE:RouteGroundOnRoad( ToCoordinate, Speed, DelaySeconds, OffRoadFormation ) + function CONTROLLABLE:RouteGroundOnRoad( ToCoordinate, Speed, DelaySeconds, OffRoadFormation, WaypointFunction, WaypointFunctionArguments ) -- Defaults. Speed=Speed or 20 @@ -2404,7 +2420,7 @@ do -- Route methods OffRoadFormation=OffRoadFormation or "Off Road" -- Get the route task. - local route=self:TaskGroundOnRoad(ToCoordinate, Speed, OffRoadFormation) + local route=self:TaskGroundOnRoad(ToCoordinate, Speed, OffRoadFormation, nil, nil, WaypointFunction, WaypointFunctionArguments) -- Route controllable to destination. self:Route( route, DelaySeconds ) @@ -2417,15 +2433,17 @@ do -- Route methods -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. -- @param #number DelaySeconds (Optional) Wait for the specified seconds before executing the Route. Default is one second. + -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{CONTROLLABLE} object, the number of the waypoint and the total number of waypoints. + -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*. -- @return #CONTROLLABLE The CONTROLLABLE. - function CONTROLLABLE:RouteGroundOnRailRoads( ToCoordinate, Speed, DelaySeconds) + function CONTROLLABLE:RouteGroundOnRailRoads( ToCoordinate, Speed, DelaySeconds, WaypointFunction, WaypointFunctionArguments ) -- Defaults. Speed=Speed or 20 DelaySeconds=DelaySeconds or 1 -- Get the route task. - local route=self:TaskGroundOnRailRoads(ToCoordinate, Speed) + local route=self:TaskGroundOnRailRoads(ToCoordinate, Speed, WaypointFunction, WaypointFunctionArguments ) -- Route controllable to destination. self:Route( route, DelaySeconds ) @@ -2442,10 +2460,12 @@ do -- Route methods -- @param #string OffRoadFormation (Optional) The formation at initial and final waypoint. Default is "Off Road". -- @param #boolean Shortcut (Optional) If true, controllable will take the direct route if the path on road is 10x longer or path on road is less than 5% of total path. -- @param Core.Point#COORDINATE FromCoordinate (Optional) Explicit initial coordinate. Default is the position of the controllable. + -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{CONTROLLABLE} object, the number of the waypoint and the total number of waypoints. + -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*. -- @return DCS#Task Task. -- @return #boolean If true, path on road is possible. If false, task will route the group directly to its destination. - function CONTROLLABLE:TaskGroundOnRoad( ToCoordinate, Speed, OffRoadFormation, Shortcut, FromCoordinate ) - self:F2({ToCoordinate=ToCoordinate, Speed=Speed, OffRoadFormation=OffRoadFormation}) + function CONTROLLABLE:TaskGroundOnRoad( ToCoordinate, Speed, OffRoadFormation, Shortcut, FromCoordinate, WaypointFunction, WaypointFunctionArguments ) + self:I({ToCoordinate=ToCoordinate, Speed=Speed, OffRoadFormation=OffRoadFormation, WaypointFunction=WaypointFunction, Args=WaypointFunctionArguments}) -- Defaults. Speed=Speed or 20 @@ -2495,6 +2515,7 @@ do -- Route methods if LongRoad and Shortcut then -- Road is long ==> we take the short cut. + table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) @@ -2523,7 +2544,18 @@ do -- Route methods table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) end - + + -- Add passing waypoint function. + if WaypointFunction then + local N=#route + for n,waypoint in pairs(route) do + waypoint.task = {} + waypoint.task.id = "ComboTask" + waypoint.task.params = {} + waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} + end + end + return route, canroad end @@ -2531,8 +2563,10 @@ do -- Route methods -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. + -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{CONTROLLABLE} object, the number of the waypoint and the total number of waypoints. + -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*. -- @return Task - function CONTROLLABLE:TaskGroundOnRailRoads(ToCoordinate, Speed) + function CONTROLLABLE:TaskGroundOnRailRoads(ToCoordinate, Speed, WaypointFunction, WaypointFunctionArguments ) self:F2({ToCoordinate=ToCoordinate, Speed=Speed}) -- Defaults. @@ -2557,10 +2591,31 @@ do -- Route methods table.insert(route, PathOnRail[2]:WaypointGround(Speed, "On Railroad")) end + + -- Add passing waypoint function. + if WaypointFunction then + local N=#route + for n,waypoint in pairs(route) do + waypoint.task = {} + waypoint.task.id = "ComboTask" + waypoint.task.params = {} + waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} + end + end return route end + --- Task function when controllable passes a waypoint. + -- @param #CONTROLLABLE controllable The controllable object. + -- @param #number n Current waypoint number passed. + -- @param #number N Total number of waypoints. + -- @param #function waypointfunction Function called when a waypoint is passed. + function CONTROLLABLE.___PassingWaypoint(controllable, n, N, waypointfunction, ...) + waypointfunction(controllable, n, N, ...) + end + + --- Make the AIR Controllable fly towards a specific point. -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. From c72625c60f8cf578a7b2f9cc5bd7efcd9f6acb00 Mon Sep 17 00:00:00 2001 From: NachtRaveVL Date: Mon, 20 Jan 2020 15:28:03 -0800 Subject: [PATCH 427/485] Adding route name fetch to getGroupRoute() --- Moose Development/Moose/Utilities/Routines.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Moose Development/Moose/Utilities/Routines.lua b/Moose Development/Moose/Utilities/Routines.lua index a28cb2bed..15e4cc1c8 100644 --- a/Moose Development/Moose/Utilities/Routines.lua +++ b/Moose Development/Moose/Utilities/Routines.lua @@ -1631,6 +1631,11 @@ function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints bu for point_num, point in pairs(group_data.route.points) do local routeData = {} + if env.mission.version > 7 then + routeData.name = env.getValueDictByKey(point.name) + else + routeData.name = point.name + end if not point.point then routeData.x = point.x routeData.y = point.y From e4906eb083fc56da91d699f3a79ac19da48c84e5 Mon Sep 17 00:00:00 2001 From: Wingthor Date: Sat, 1 Feb 2020 19:30:32 +0100 Subject: [PATCH 428/485] Test Commits and pipe --- .gitignore | 1 + Moose Development/Moose/Utilities/Utils.lua | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 135102f04..f908db72e 100644 --- a/.gitignore +++ b/.gitignore @@ -221,3 +221,4 @@ _gsdata_/ .gitattributes .gitignore Moose Test Missions/MOOSE_Test_Template.miz +Moose Development/Moose/.vscode/launch.json diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 766b8de47..7346dcc38 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -436,7 +436,7 @@ UTILS.tostringLL = function( lat, lon, acc, DMS) secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' end - -- 024° 23' 12"N or 024° 23' 12.03"N + -- 024� 23' 12"N or 024� 23' 12.03"N return string.format('%03d°', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' .. string.format('%03d°', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi @@ -1080,3 +1080,4 @@ function UTILS.GetModulationName(Modulation) end +-- Just a test to see commits in new environment for Wingthor \ No newline at end of file From 5e1fb36d0123b41e205e973c65051ffe691c1f32 Mon Sep 17 00:00:00 2001 From: Wingthor Date: Sat, 1 Feb 2020 20:27:00 +0100 Subject: [PATCH 429/485] Fixed issue 1254 --- Moose Development/Moose/Functional/Range.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index d181ebbec..ba217f14b 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -1088,7 +1088,7 @@ end -- @return #RANGE self function RANGE:SetSoundfilesPath(path) self.soundpath=tostring(path or "Range Soundfiles/") - self:I(self.lid..string.format("Setting sound files path to %s", self.soundpath)) + self:I(self.id..string.format("Setting sound files path to %s", self.soundpath)) return self end From b68e254bc04f3a24009c7c377d7d4f50d87d0ae3 Mon Sep 17 00:00:00 2001 From: Wingthor Date: Thu, 6 Feb 2020 15:36:54 +0100 Subject: [PATCH 430/485] Fixes issue #1258 WAREHOUSE:onafterChangeCountry() --- .gitignore | 4 ++++ Moose Development/Moose/Functional/Warehouse.lua | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f908db72e..5e42c0c93 100644 --- a/.gitignore +++ b/.gitignore @@ -222,3 +222,7 @@ _gsdata_/ .gitignore Moose Test Missions/MOOSE_Test_Template.miz Moose Development/Moose/.vscode/launch.json +MooseCodeWS.code-workspace +.gitignore +.gitignore +/.gitignore diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index bd98267cb..9c25b50f5 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -4894,8 +4894,14 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) self.queue={} -- Airbase could have been captured before and already belongs to the new coalition. - local airbase=AIRBASE:FindByName(self.airbasename) - local airbasecoaltion=airbase:GetCoalition() + -- Check if Warehouse has a arbiase atthached + local airbasecoaltion + if self.airbasse ~= nil then + local airbase=AIRBASE:FindByName(self.airbasename) + airbasecoaltion=airbase:GetCoalition() + else -- Warehouse has no airbase attached so just keep whatever, self.airbase will still be nil since CoalitionNew will not be nil if Warehouse have a airbse attacjed. + airbasecoaltion = nil + end if CoalitionNew==airbasecoaltion then -- Airbase already owned by the coalition that captured the warehouse. Airbase can be used by this warehouse. From f685064c0c6efee4ffe2970f7fd6a9f6569a5f41 Mon Sep 17 00:00:00 2001 From: Wingthor Date: Sat, 8 Feb 2020 12:44:50 +0100 Subject: [PATCH 431/485] fixed the typo on self.airbasse to self.airbase. Tested with and without this line `SetAirbase(AIRBASE:FindByName(AIRBASE.Caucasus.Kutaisi))` in the test script. Resolves issue #1258 --- Moose Development/Moose/Functional/Warehouse.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 9c25b50f5..999eb97c9 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -4896,7 +4896,7 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) -- Airbase could have been captured before and already belongs to the new coalition. -- Check if Warehouse has a arbiase atthached local airbasecoaltion - if self.airbasse ~= nil then + if self.airbase ~= nil then local airbase=AIRBASE:FindByName(self.airbasename) airbasecoaltion=airbase:GetCoalition() else -- Warehouse has no airbase attached so just keep whatever, self.airbase will still be nil since CoalitionNew will not be nil if Warehouse have a airbse attacjed. From a2759b1cc01f1de8581b6ef372c0c5dd213e123a Mon Sep 17 00:00:00 2001 From: Wingthor Date: Tue, 11 Feb 2020 15:55:10 +0100 Subject: [PATCH 432/485] Issue #1264 Abu_Musa_Island_Airport --- .scannerwork/.sonar_lock | 0 .scannerwork/report-task.txt | 6 ++++++ Moose Development/Moose/Functional/ATC_Ground.lua | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .scannerwork/.sonar_lock create mode 100644 .scannerwork/report-task.txt diff --git a/.scannerwork/.sonar_lock b/.scannerwork/.sonar_lock new file mode 100644 index 000000000..e69de29bb diff --git a/.scannerwork/report-task.txt b/.scannerwork/report-task.txt new file mode 100644 index 000000000..cc70ad160 --- /dev/null +++ b/.scannerwork/report-task.txt @@ -0,0 +1,6 @@ +projectKey=Test +serverUrl=http://localhost:9000 +serverVersion=8.1.0.31237 +dashboardUrl=http://localhost:9000/dashboard?id=Test +ceTaskId=AXAlUJO97YLjwz1VUDXR +ceTaskUrl=http://localhost:9000/api/ce/task?id=AXAlUJO97YLjwz1VUDXR diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index ba407eb37..ded08800f 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -2458,7 +2458,7 @@ end ATC_GROUND_PERSIANGULF = { ClassName = "ATC_GROUND_PERSIANGULF", Airbases = { - [AIRBASE.PersianGulf.Abu_Musa_Island_Airport] = { + [AIRBASE.PersianGulf.Al_Dhafra_AB] = { ---FIX by Wingthor Changed from Abu_Musa_Island_Airport PointsRunways = { [1] = { [1]={["y"]=-122813.71002344,["x"]=-31689.936027827,}, From ce60e7ce741ffcdc42db91dfe86d2a7d5cfb5b28 Mon Sep 17 00:00:00 2001 From: 132nd-Entropy Date: Wed, 12 Feb 2020 16:25:45 +0100 Subject: [PATCH 433/485] FIX: removed Mellan_Airstrip from list of Nevada airfields (not a valid airport and has no taxiway designations, resulting in a scripting error if included) --- Moose Development/Moose/Functional/ATC_Ground.lua | 4 +--- Moose Development/Moose/Wrapper/Airbase.lua | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index ded08800f..9ded69874 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -1052,7 +1052,6 @@ end -- * `AIRBASE.Nevada.Laughlin_Airport` -- * `AIRBASE.Nevada.Lincoln_County` -- * `AIRBASE.Nevada.McCarran_International_Airport` --- * `AIRBASE.Nevada.Mellan_Airstrip` -- * `AIRBASE.Nevada.Mesquite` -- * `AIRBASE.Nevada.Mina_Airport_3Q0` -- * `AIRBASE.Nevada.Nellis_AFB` @@ -1096,8 +1095,7 @@ end -- -- -- Monitor specific airbases. -- ATC_Ground = ATC_GROUND_NEVADA:New( --- { AIRBASE.Nevada.Laughlin_Airport, --- AIRBASE.Nevada.Mellan_Airstrip, +-- { AIRBASE.Nevada.Laughlin_Airport, -- AIRBASE.Nevada.Lincoln_County, -- AIRBASE.Nevada.North_Las_Vegas, -- AIRBASE.Nevada.McCarran_International_Airport diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 37530551f..cf349c3b3 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -124,7 +124,6 @@ AIRBASE.Caucasus = { -- * AIRBASE.Nevada.Jean_Airport -- * AIRBASE.Nevada.Laughlin_Airport -- * AIRBASE.Nevada.Lincoln_County --- * AIRBASE.Nevada.Mellan_Airstrip -- * AIRBASE.Nevada.Mesquite -- * AIRBASE.Nevada.Mina_Airport_3Q0 -- * AIRBASE.Nevada.North_Las_Vegas @@ -144,7 +143,6 @@ AIRBASE.Nevada = { ["Jean_Airport"] = "Jean Airport", ["Laughlin_Airport"] = "Laughlin Airport", ["Lincoln_County"] = "Lincoln County", - ["Mellan_Airstrip"] = "Mellan Airstrip", ["Mesquite"] = "Mesquite", ["Mina_Airport_3Q0"] = "Mina Airport 3Q0", ["North_Las_Vegas"] = "North Las Vegas", From 1f9ffcd92db83d6cca7f8e140f8ea7216b39fb99 Mon Sep 17 00:00:00 2001 From: Wingthor Date: Thu, 13 Feb 2020 09:58:59 +0100 Subject: [PATCH 434/485] The error is to deep to solve on the fly. In database new groups appear, but not in map, when regular spawning (:Spawn()) is done. Spawning scheduled causes groups to appear on map. OnSpawnGroup is not triggered in neither of methods. #Issue 1255 is for now solved by adding a caution to documentation for function/directive: "CAUTION: this directive will NOT work with OnSpawnGroup function." Requires work from author or someone else with deep knowledge of the class. --- Moose Development/Moose/Core/Spawn.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 200d8a7a0..1b0244185 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -998,6 +998,7 @@ end --- Makes the groups visible before start (like a batallion). -- The method will take the position of the group as the first position in the array. +-- CAUTION: this directive will NOT work with OnSpawnGroup function. -- @param #SPAWN self -- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. -- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. From d6c919a097fd3febe1b7acd52596ab47840ec097 Mon Sep 17 00:00:00 2001 From: Wingthor Date: Thu, 13 Feb 2020 12:16:56 +0100 Subject: [PATCH 435/485] Added some line to docs to tell Spawn state might not be as expected. Added: Requested assets spawn in various "Engagement Rules" (ROE) and Alerts modes. If your assets will cross into dangerous areas, be sure to change these states. You can do this in @{#WAREHOUSE:OnAfterAssetSpawned}(*From, *Event, *To, *group, *asset, *request)) function. Initial Spawn states is as follows: GROUND: ROE, "Return Fire" Alarm, "Green" AIR: ROE, "Return Fire" Reaction to Threat, "Passive Defense" NAVAL ROE, "Return Fire" Alarm,"N/A" Solves issue #1260 --- Moose Development/Moose/Functional/Warehouse.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 999eb97c9..284a95cea 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -282,6 +282,13 @@ -- -- Assets of the warehouse can be requested by other MOOSE warehouses. A request will first be scrutinized to check if can be fulfilled at all. If the request is valid, it is -- put into the warehouse queue and processed as soon as possible. + +-- Requested assets spawn in various "Rule of Engagement Rules" (ROE) and Alerts modes. If your assets will cross into dangerous areas, be sure to change these states. You can do this in @{#WAREHOUSE:OnAfterAssetSpawned}(*From, *Event, *To, *group, *asset, *request)) function. +-- +-- Initial Spawn states is as follows: +-- GROUND: ROE, "Return Fire" Alarm, "Green" +-- AIR: ROE, "Return Fire" Reaction to Threat, "Passive Defense" +-- NAVAL ROE, "Return Fire" Alarm,"N/A" -- -- A request can be added by the @{#WAREHOUSE.AddRequest}(*warehouse*, *AssetDescriptor*, *AssetDescriptorValue*, *nAsset*, *TransportType*, *nTransport*, *Prio*, *Assignment*) function. -- The parameters are From 04da941c363ccb635e7fd3011f8929bea2f7ce32 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 15 Feb 2020 23:51:28 +0100 Subject: [PATCH 436/485] Updates --- Moose Development/Moose/Core/Fsm.lua | 5 +- Moose Development/Moose/Core/Point.lua | 6 +- Moose Development/Moose/Core/Spawn.lua | 7 +- Moose Development/Moose/Core/UserFlag.lua | 11 +- Moose Development/Moose/DCS.lua | 53 +- Moose Development/Moose/Functional/Range.lua | 14 +- Moose Development/Moose/Ops/ATIS.lua | 62 ++- Moose Development/Moose/Utilities/Enums.lua | 34 ++ .../Moose/Wrapper/Controllable.lua | 500 ++++++++++-------- Moose Development/Moose/Wrapper/Group.lua | 198 ++++++- Moose Development/Moose/Wrapper/Scenery.lua | 13 +- Moose Development/Moose/Wrapper/Unit.lua | 85 ++- 12 files changed, 708 insertions(+), 280 deletions(-) diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index 5149b4aa6..09d447f65 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -727,14 +727,17 @@ do -- FSM if DelaySeconds ~= nil then if DelaySeconds < 0 then -- Only call the event ONCE! DelaySeconds = math.abs( DelaySeconds ) - if not self._EventSchedules[EventName] then + if not self._EventSchedules[EventName] then CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true ) self._EventSchedules[EventName] = CallID + self:T2(string.format("NEGATIVE Event %s delayed by %.1f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) else + self:T2(string.format("NEGATIVE Event %s delayed by %.1f sec CANCELLED as we already have such an event in the queue.", EventName, DelaySeconds)) -- reschedule end else CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true ) + self:T2(string.format("Event %s delayed by %.1f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) end else error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 57e2fc2cb..919f8ec36 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -357,7 +357,7 @@ do -- COORDINATE -- @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)) + self:F(string.format("Scanning in radius %.1f m.", radius or 100)) local SphereSearch = { id = world.VolumeType.SPHERE, @@ -437,9 +437,11 @@ do -- COORDINATE end for _,static in pairs(Statics) do self:T(string.format("Scan found static %s", static:getName())) + _DATABASE:AddStatic(static:getName()) end for _,scenery in pairs(Scenery) do - self:T(string.format("Scan found scenery %s", scenery:getTypeName())) + self:T(string.format("Scan found scenery %s typename=%s", scenery:getName(), scenery:getTypeName())) + SCENERY:Register(scenery:getName(), scenery) end return gotunits, gotstatics, gotscenery, Units, Statics, Scenery diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 200d8a7a0..eecaca873 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -509,14 +509,17 @@ end -- @param #SPAWN self -- @param #string AirbaseName Name of the airbase. -- @param #number Takeoff (Optional) Takeoff type. Can be SPAWN.Takeoff.Hot (default), SPAWN.Takeoff.Cold or SPAWN.Takeoff.Runway. +-- @param #number TerminalTyple (Optional) The terminal type. -- @return #SPAWN self -function SPAWN:InitAirbase( AirbaseName, Takeoff ) +function SPAWN:InitAirbase( AirbaseName, Takeoff, TerminalType ) self:F( ) self.SpawnInitAirbase=AIRBASE:FindByName(AirbaseName) self.SpawnInitTakeoff=Takeoff or SPAWN.Takeoff.Hot + self.SpawnInitTerminalType=TerminalType + return self end @@ -1133,7 +1136,7 @@ function SPAWN:Spawn() self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, self.AliveUnits } ) if self.SpawnInitAirbase then - return self:SpawnAtAirbase(self.SpawnInitAirbase, self.SpawnInitTakeoff) + return self:SpawnAtAirbase(self.SpawnInitAirbase, self.SpawnInitTakeoff, nil, self.SpawnInitTerminalType) else return self:SpawnWithIndex( self.SpawnIndex + 1 ) end diff --git a/Moose Development/Moose/Core/UserFlag.lua b/Moose Development/Moose/Core/UserFlag.lua index bef5cefff..30e0d99d9 100644 --- a/Moose Development/Moose/Core/UserFlag.lua +++ b/Moose Development/Moose/Core/UserFlag.lua @@ -19,6 +19,8 @@ do -- UserFlag --- @type USERFLAG + -- @field #string ClassName Name of the class + -- @field #string UserFlagName Name of the flag. -- @extends Core.Base#BASE @@ -30,7 +32,8 @@ do -- UserFlag -- -- @field #USERFLAG USERFLAG = { - ClassName = "USERFLAG", + ClassName = "USERFLAG", + UserFlagName = nil, } --- USERFLAG Constructor. @@ -46,6 +49,12 @@ do -- UserFlag return self end + --- Get the userflag name. + -- @param #USERFLAG self + -- @return #string Name of the user flag. + function USERFLAG:GetName() + return self.UserFlagName + end --- Set the userflag to a given Number. -- @param #USERFLAG self diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index b330bf43c..df37c1c50 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -428,6 +428,10 @@ do -- Types -- @type TaskArray -- @list <#Task> + --- + --@type WaypointAir + --@field #boolean lateActivated + --@field #boolean uncontrolled end -- @@ -1205,6 +1209,7 @@ do -- AI -- @field TAKEOFF -- @field TAKEOFF_PARKING -- @field TURNING_POINT + -- @field TAKEOFF_PARKING_HOT -- @field LAND --- @type AI.Task.TurnMethod @@ -1241,8 +1246,8 @@ do -- AI --- @type AI.Option.Naval -- @field #AI.Option.Naval.id id -- @field #AI.Option.Naval.val val - - --TODO: work on formation + + --- @type AI.Option.Air.id -- @field NO_OPTION -- @field ROE @@ -1251,7 +1256,34 @@ do -- AI -- @field FLARE_USING -- @field FORMATION -- @field RTB_ON_BINGO - -- @field SILENCE + -- @field SILENCE + -- @field RTB_ON_OUT_OF_AMMO + -- @field ECM_USING + -- @field PROHIBIT_AA + -- @field PROHIBIT_JETT + -- @field PROHIBIT_AB + -- @field PROHIBIT_AG + -- @field MISSILE_ATTACK + -- @field PROHIBIT_WP_PASS_REPORT + + --- @type AI.Option.Air.id.FORMATION + -- @field LINE_ABREAST + -- @field TRAIL + -- @field WEDGE + -- @field ECHELON_RIGHT + -- @field ECHELON_LEFT + -- @field FINGER_FOUR + -- @field SPREAD_FOUR + -- @field WW2_BOMBER_ELEMENT + -- @field WW2_BOMBER_ELEMENT_HEIGHT + -- @field WW2_FIGHTER_VIC + -- @field HEL_WEDGE + -- @field HEL_ECHELON + -- @field HEL_FRONT + -- @field HEL_COLUMN + -- @field COMBAT_BOX + -- @field JAVELIN_DOWN + --- @type AI.Option.Air.val -- @field #AI.Option.Air.val.ROE ROE @@ -1284,12 +1316,27 @@ do -- AI -- @field AGAINST_FIRED_MISSILE -- @field WHEN_FLYING_IN_SAM_WEZ -- @field WHEN_FLYING_NEAR_ENEMIES + + --- @type AI.Option.Air.val.ECM_USING + -- @field NEVER_USE + -- @field USE_IF_ONLY_LOCK_BY_RADAR + -- @field USE_IF_DETECTED_LOCK_BY_RADAR + -- @field ALWAYS_USE + + --- @type AI.Option.Air.val.MISSILE_ATTACK + -- @field MAX_RANGE + -- @field NEZ_RANGE + -- @field HALF_WAY_RMAX_NEZ + -- @field TARGET_THREAT_EST + -- @field RANDOM_RANGE + --- @type AI.Option.Ground.id -- @field NO_OPTION -- @field ROE @{#AI.Option.Ground.val.ROE} -- @field DISPERSE_ON_ATTACK true or false -- @field ALARM_STATE @{#AI.Option.Ground.val.ALARM_STATE} + -- @field ENGAGE_AIR_WEAPONS --- @type AI.Option.Ground.val -- @field #AI.Option.Ground.val.ROE ROE diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index ba217f14b..6b292f2fd 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -516,7 +516,7 @@ RANGE.MenuF10Root=nil --- Range script version. -- @field #string version -RANGE.version="2.2.1" +RANGE.version="2.2.2" --TODO list: --TODO: Verbosity level for messages. @@ -754,7 +754,7 @@ function RANGE:onafterStart() if self.rangecontrolfreq then -- Radio queue. - self.rangecontrol=RADIOQUEUE:New(self.rangecontrolfreq) + self.rangecontrol=RADIOQUEUE:New(self.rangecontrolfreq, nil, self.rangename) -- Init numbers. self.rangecontrol:SetDigit(0, RANGE.Sound.RC0.filename, RANGE.Sound.RC0.duration, self.soundpath) @@ -778,7 +778,7 @@ function RANGE:onafterStart() if self.instructorfreq then -- Radio queue. - self.instructor=RADIOQUEUE:New(self.instructorfreq) + self.instructor=RADIOQUEUE:New(self.instructorfreq, nil, self.rangename) -- Init numbers. self.instructor:SetDigit(0, RANGE.Sound.IR0.filename, RANGE.Sound.IR0.duration, self.soundpath) @@ -1341,9 +1341,9 @@ function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) -- Debug or error output. if _isstatic==true then - self:T(self.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + self:I(self.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) elseif _isstatic==false then - self:T(self.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + self:I(self.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) else self:E(self.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 @@ -2967,10 +2967,10 @@ function RANGE:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName) end else - self:T(self.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) + self:E(self.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) end else - self:T(self.id.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) + self:E(self.id.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) end end diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 8c51087d4..db3be94ab 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -523,7 +523,7 @@ _ATIS={} --- ATIS class version. -- @field #string version -ATIS.version="0.6.2" +ATIS.version="0.6.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1142,6 +1142,10 @@ function ATIS:onafterBroadcast(From, Event, To) local WINDFROM=string.format("%03d", windFrom-magvar) local WINDSPEED=string.format("%d", UTILS.MpsToKnots(windSpeed)) + + if WINDFROM=="000" then + WINDFROM="360" + end if self.metric then WINDSPEED=string.format("%d", windSpeed) @@ -1151,27 +1155,7 @@ function ATIS:onafterBroadcast(From, Event, To) --- Runway --- -------------- - -- Get active runway data based on wind direction. - local runact=self.airbase:GetActiveRunway(self.runwaym2t) - - -- Active runway "31". - local runway=self:GetMagneticRunway(windFrom) or runact.idx - - -- Left or right in case there are two runways with the same heading. - local rwyLeft=nil - - -- Check if user explicitly specified a runway. - if self.activerunway then - - -- Get explicit runway heading if specified. - local runwayno=self:GetRunwayWithoutLR(self.activerunway) - if runwayno~="" then - runway=runwayno - end - - -- Was "L"eft or "R"ight given? - rwyLeft=self:GetRunwayLR(self.activerunway) - end + local runway, rwyLeft=self:GetActiveRunway() ------------ --- Time --- @@ -1772,11 +1756,41 @@ end -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Get runway from user supplied magnetic heading. +--- Get active runway runway. -- @param #ATIS self --- @return #string Runway magnetic heading divided by ten (and rounded). Eg, "13" for 130°. +-- @return #string Active runway, e.g. "31" for 310 deg. +-- @return #boolean Use Left=true, Right=false, or nil. function ATIS:GetActiveRunway() + + local coord=self.airbase:GetCoordinate() + local height=coord:GetLandHeight() + + -- Get wind direction and speed in m/s. + local windFrom, windSpeed=coord:GetWind(height+10) + + -- Get active runway data based on wind direction. + local runact=self.airbase:GetActiveRunway(self.runwaym2t) + + -- Active runway "31". + local runway=self:GetMagneticRunway(windFrom) or runact.idx + + -- Left or right in case there are two runways with the same heading. + local rwyLeft=nil + + -- Check if user explicitly specified a runway. + if self.activerunway then + -- Get explicit runway heading if specified. + local runwayno=self:GetRunwayWithoutLR(self.activerunway) + if runwayno~="" then + runway=runwayno + end + + -- Was "L"eft or "R"ight given? + rwyLeft=self:GetRunwayLR(self.activerunway) + end + + return runway, rwyLeft end --- Get runway from user supplied magnetic heading. diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index a5bd06237..91c814937 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -1,3 +1,11 @@ +--- **Utilities** Enumerators. +-- +-- See the [Simulator Scripting Engine Documentation](https://wiki.hoggitworld.com/view/Simulator_Scripting_Engine_Documentation) on Hoggit for further explanation and examples. +-- +-- @module DCS +-- @image MOOSE.JPG + + ENUMS = {} ENUMS.ROE = { @@ -12,4 +20,30 @@ ENUMS.ROT = { PassiveDefense = 2, EvadeFire = 3, Vertical = 4 +} + +ENUMS.WeaponFlag={ + -- Auto + Auto=1073741822, + -- Bombs + LGB=2, + TvGB=4, + SNSGB=8, + HEBomb=16, + Penetrator=32, + NapalmBomb=64, + FAEBomb=128, + ClusterBomb=256, + Dispencer=512, + CandleBomb=1024, + ParachuteBomb=2147483648, + GuidedBomb=14, -- (LGB + TvGB + SNSGB) + AnyUnguidedBomb=2147485680, -- (HeBomb + Penetrator + NapalmBomb + FAEBomb + ClusterBomb + Dispencer + CandleBomb + ParachuteBomb) + AnyBomb=2147485694, -- (GuidedBomb + AnyUnguidedBomb) + -- Rockets + LightRocket=2048, + MarkerRocket=4096, + CandleRocket=8192, + HeavyRocket=16384, + AnyRocket=30720 -- (LightRocket + MarkerRocket + CandleRocket + HeavyRocket) } \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 969cd2672..45e1ec861 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -312,6 +312,7 @@ function CONTROLLABLE:ClearTasks() if DCSControllable then local Controller = self:_GetController() + env.info("FF clearing tasks!") Controller:resetTask() return self end @@ -879,15 +880,15 @@ function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, At local DCSTask DCSTask = { id = 'AttackGroup', params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, + groupId = AttackGroup:GetID(), + weaponType = WeaponType, + expend = WeaponExpend, + attackQty = AttackQty, directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, - altitude = Altitude, - attackQtyLimit = AttackQtyLimit, + direction = Direction, + altitudeEnabled = AltitudeEnabled, + altitude = Altitude, + attackQtyLimit = AttackQtyLimit, }, }, @@ -912,16 +913,16 @@ function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, Atta DCSTask = { id = 'AttackUnit', params = { - unitId = AttackUnit:GetID(), - groupAttack = GroupAttack or false, - expend = WeaponExpend or "Auto", + unitId = AttackUnit:GetID(), + groupAttack = GroupAttack or false, + expend = WeaponExpend or "Auto", directionEnabled = Direction and true or false, - direction = math.rad(Direction or 0), - altitudeEnabled = Altitude and true or false, - altitude = Altitude or math.max(1000, AttackUnit:GetAltitude()), - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - weaponType = WeaponType + direction = math.rad(Direction or 0), + altitudeEnabled = Altitude and true or false, + altitude = Altitude or math.max(1000, AttackUnit:GetAltitude()), + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + weaponType = WeaponType } } @@ -974,18 +975,18 @@ function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, D DCSTask = { id = 'Bombing', params = { - x = Vec2.x, - y = Vec2.y, - groupAttack = _groupattack, - expend = WeaponExpend or "Auto", - attackQtyLimit = false, --AttackQty and true or false, - attackQty = AttackQty or 1, + x = Vec2.x, + y = Vec2.y, + groupAttack = _groupattack, + expend = WeaponExpend or "Auto", + attackQtyLimit = false, + attackQty = AttackQty or 1, directionEnabled = _directionenabled, - direction = _direction, - altitudeEnabled = _altitudeenabled, - altitude = _altitude, - weaponType = WeaponType, - --attackType=_attacktype, + direction = _direction, + altitudeEnabled = _altitudeenabled, + altitude = _altitude, + weaponType = WeaponType, + attackType = _attacktype, }, } @@ -1010,18 +1011,18 @@ function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, Atta DCSTask = { id = 'AttackMapObject', params = { - point = Vec2, - x = Vec2.x, - y = Vec2.y, - groupAttack = GroupAttack or false, - expend = WeaponExpend or "Auto", - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, + point = Vec2, + x = Vec2.x, + y = Vec2.y, + groupAttack = GroupAttack or false, + expend = WeaponExpend or "Auto", + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude or 30, - weaponType = WeaponType or 1073741822, + direction = Direction, + altitudeEnabled = Altitude and true or false, + altitude = Altitude or 30, + weaponType = WeaponType or 1073741822, }, }, @@ -1030,6 +1031,186 @@ function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, Atta end +--- (AIR) Delivering weapon via CarpetBombing (all bombers in formation release at same time) at the point on the ground. +-- @param #CONTROLLABLE self +-- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. +-- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. +-- @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 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 #number Altitude (optional) The altitude from where to attack. +-- @param #number WeaponType (optional) The WeaponType. +-- @param #number CarpetLength (optional) default to 500 m. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskCarpetBombing(Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, CarpetLength) + self:F2( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, CarpetLength } ) + + local _groupattack=false + if GroupAttack then + _groupattack=GroupAttack + end + + local _direction=0 + local _directionenabled=false + if Direction then + _direction=math.rad(Direction) + _directionenabled=true + end + + local _altitude=0 + local _altitudeenabled=false + if Altitude then + _altitude=Altitude + _altitudeenabled=true + end + + -- default to 500m + local _carpetLength = 500 + if CarpetLength then + _carpetLength = CarpetLength + end + + local _weaponexpend = "Auto" + if WeaponExpend then + _weaponexpend = WeaponExpend + end + + -- Build Task Structure + local DCSTask + DCSTask = { + id = 'CarpetBombing', + params = { + attackType = "Carpet", + point = Vec2, + x = Vec2.x, + y = Vec2.y, + groupAttack = _groupattack, + carpetLength = _carpetLength, + weaponType = WeaponType, + expend = "All", + attackQtyLimit = false, --AttackQty and true or false, + attackQty = AttackQty or 1, + directionEnabled = _directionenabled, + direction = _direction, + altitudeEnabled = _altitudeenabled, + altitude = _altitude + } + } + + return DCSTask +end + + + +--- (AIR) Following another airborne controllable. +-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. +-- Used to support CarpetBombing Task +-- @param #CONTROLLABLE self +-- @param #CONTROLLABLE FollowControllable The controllable to be followed. +-- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. +-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskFollowBigFormation(FollowControllable, Vec3, LastWaypointIndex ) + + local DCSTask = { + id = 'FollowBigFormation', + params = { + groupId = FollowControllable:GetID(), + pos = Vec3, + lastWptIndexFlag = LastWaypointIndex and true or false, + lastWptIndex = LastWaypointIndex + } + } + + return DCSTask +end + + +--- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. +-- @param #CONTROLLABLE self +-- @param DCS#Vec2 Vec2 The point where to wait. Needs to have x and y components. +-- @param Core.Set#SET_GROUP GroupSetForEmparking Set of groups to embark. +-- @param #number Duration (Optional) The maximum duration in seconds to wait until all groups have embarked. +-- @param Core.Set#SET_GROUP (Optional) DistributionGroupSet Set of groups identifying the groups needing to board specific helicopters. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, DistributionGroupSet) + + -- Table of group IDs for embarking. + local g4e={} + + if GroupSetForEmbarking then + for _,_group in pairs(GroupSetForEmbarking:GetSet()) do + local group=_group --Wrapper.Group#GROUP + table.insert(g4e, group:GetID()) + end + else + self:E("ERROR: No groups for embarking specified!") + return nil + end + + + + local DCSTask = { + id = 'Embarking', + params = { + Vec2 = Vec2, + x = Vec2.x, + y = Vec2.y, + groupsForEmbarking = g4e, + durationFlag = Duration and true or false, + duration = Duration, + distributionFlag = DistributionGroupSet and true or false, + distribution = Distribution, + } + } + + self:T3( { DCSTask } ) + return DCSTask +end + + + +--- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. +-- @param #CONTROLLABLE self +-- @param DCS#Vec2 Vec2 The point where to wait. +-- @param #number Duration The duration in seconds to wait. +-- @param #CONTROLLABLE EmbarkingControllable The controllable to be embarked. +-- @return DCS#Task The DCS task structure +function CONTROLLABLE:TaskDisembarking(Vec2, Duration, EmbarkingControllable) + + -- Table of group IDs for embarking. + local g4e={} + + if GroupSetForEmbarking then + for _,_group in pairs(GroupSetForEmbarking:GetSet()) do + local group=_group --Wrapper.Group#GROUP + table.insert(g4e, group:GetID()) + end + else + self:E("ERROR: No groups for embarking specified!") + return nil + end + + + local DCSTask = { + id = 'Disembarking', + params = { + point = Vec2, + x = Vec2.x, + y = Vec2.y, + duration = Duration, + groupsForEmbarking = { EmbarkingControllable:GetID() }, + durationFlag = durationflag, + distributionFlag = false, + distribution = {}, + } + } + + return DCSTask +end + +---- + --- (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Point The point to hold the position. @@ -1039,39 +1220,20 @@ end function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) self:F2( { self.ControllableName, Point, Altitude, Speed } ) - -- pattern = enum AI.Task.OribtPattern, - -- point = Vec2, - -- point2 = Vec2, - -- speed = Distance, - -- altitude = Distance - local LandHeight = land.getHeight( Point ) self:T3( { LandHeight } ) - local DCSTask = { id = 'Orbit', - params = { pattern = AI.Task.OrbitPattern.CIRCLE, - point = Point, - speed = Speed, + local DCSTask = { + id = 'Orbit', + params = { + pattern = AI.Task.OrbitPattern.CIRCLE, + point = Point, + speed = Speed, altitude = Altitude + LandHeight } } - - -- local AITask = { id = 'ControlledTask', - -- params = { task = { id = 'Orbit', - -- params = { pattern = AI.Task.OrbitPattern.CIRCLE, - -- point = Point, - -- speed = Speed, - -- altitude = Altitude + LandHeight - -- } - -- }, - -- stopCondition = { duration = Duration - -- } - -- } - -- } - -- ) - return DCSTask end @@ -1111,7 +1273,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 (optional) The coordinate where to orbit. If the coordinate is not given, then the current position of the controllable is used. +-- @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 } ) @@ -1139,10 +1301,6 @@ function CONTROLLABLE:TaskHoldPosition() end - - - - --- (AIR) Delivering weapon on the runway. See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_bombingRunway) -- -- Make sure the aircraft has the following role: @@ -1165,36 +1323,18 @@ end function CONTROLLABLE:TaskBombingRunway(Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack) self:F2( { self.ControllableName, Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack } ) --- BombingRunway = { --- id = 'BombingRunway', --- params = { --- runwayId = AirdromeId, --- weaponType = number, --- expend = enum AI.Task.WeaponExpend, --- attackQty = number, --- direction = Azimuth, --- groupAttack = boolean, --- } --- } - - -- Defaults. - WeaponType=WeaponType or 2147485694 - WeaponExpend=WeaponExpend or AI.Task.WeaponExpend.ALL - AttackQty=AttackQty or 1 - - local DCSTask - DCSTask = { id = 'BombingRunway', + local DCSTask = { + id = 'BombingRunway', params = { - runwayId = Airbase:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, + runwayId = Airbase:GetID(), + weaponType = WeaponType or ENUMS.WeaponFlag.AnyBomb, + expend = WeaponExpend or AI.Task.WeaponExpend.ALL, + attackQty = AttackQty or 1, + direction = Direction and math.rad(Direction) or nil, groupAttack = GroupAttack, }, - }, + } - self:T3( { DCSTask } ) return DCSTask end @@ -1203,16 +1343,9 @@ end -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskRefueling() - self:F2( { self.ControllableName } ) - --- Refueling = { --- id = 'Refueling', --- params = {} --- } local DCSTask={id='Refueling', params={}} - - self:T3( { DCSTask } ) + return DCSTask end @@ -1222,37 +1355,16 @@ end -- @param DCS#Vec2 Point The point where to land. -- @param #number Duration The duration in seconds to stay on the ground. -- @return #CONTROLLABLE self -function CONTROLLABLE:TaskLandAtVec2( Point, Duration ) - self:F2( { self.ControllableName, Point, Duration } ) +function CONTROLLABLE:TaskLandAtVec2(Vec2, Duration) --- Land = { --- id= 'Land', --- params = { --- point = Vec2, --- durationFlag = boolean, --- duration = Time --- } --- } - - local DCSTask - if Duration and Duration > 0 then - DCSTask = { id = 'Land', - params = { - point = Point, - durationFlag = true, - duration = Duration, - }, - } - else - DCSTask = { id = 'Land', - params = { - point = Point, - durationFlag = false, - }, - } - end - - self:T3( DCSTask ) + local DCSTask = { + id = 'Land', + params = { + point = Vec2, + durationFlag = Duration and true or false, + duration = Duration, + }, + } return DCSTask end @@ -1423,21 +1535,7 @@ end -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskHold() - self:F2( { self.ControllableName } ) - --- Hold = { --- id = 'Hold', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'Hold', - params = { - } - } - - self:T3( { DCSTask } ) + local DCSTask = {id = 'Hold', params = {}} return DCSTask end @@ -1456,23 +1554,13 @@ end function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink ) self:F2( { self.ControllableName, AttackGroup, WeaponType, Designation, Datalink } ) --- FAC_AttackGroup = { --- id = 'FAC_AttackGroup', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_AttackGroup', + local DCSTask = { + id = 'FAC_AttackGroup', params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, + groupId = AttackGroup:GetID(), + weaponType = WeaponType, designation = Designation, - datalink = Datalink, + datalink = Datalink, } } @@ -1486,26 +1574,18 @@ end -- @param #CONTROLLABLE self -- @param DCS#Distance Distance Maximal distance from the target to a route leg. If the target is on a greater distance it will be ignored. -- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. --- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority ) self:F2( { self.ControllableName, Distance, TargetTypes, Priority } ) --- EngageTargets ={ --- id = 'EngageTargets', --- params = { --- maxDist = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'EngageTargets', + local DCSTask = { + id = 'EngageTargets', params = { - maxDist = Distance, - targetTypes = TargetTypes, - priority = Priority + maxDistEnabled = Distance and true or false, + maxDist = Distance, + targetTypes = TargetTypes or {"Air"}, + priority = Priority or 0, } } @@ -1519,29 +1599,19 @@ end -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 2D-coordinates of the zone. -- @param DCS#Distance Radius Radius of the zone. --- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param DCS#AttributeNameArray (Optional) TargetTypes Array of target categories allowed to engage. Default {"Air"}. +-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, Priority ) self:F2( { self.ControllableName, Vec2, Radius, TargetTypes, Priority } ) --- EngageTargetsInZone = { --- id = 'EngageTargetsInZone', --- params = { --- point = Vec2, --- zoneRadius = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'EngageTargetsInZone', + local DCSTask = { + id = 'EngageTargetsInZone', params = { - point = Vec2, - zoneRadius = Radius, - targetTypes = TargetTypes, - priority = Priority + point = Vec2, + zoneRadius = Radius, + targetTypes = TargetTypes or {"Air"}, + priority = Priority or 0 } } @@ -1669,18 +1739,8 @@ end -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskAWACS( ) self:F2( { self.ControllableName } ) - --- AWACS = { --- id = 'AWACS', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'AWACS', - params = { - } - } + + local DCSTask = {id = 'AWACS', params = {}} self:T3( { DCSTask } ) return DCSTask @@ -1693,17 +1753,7 @@ end function CONTROLLABLE:EnRouteTaskTanker( ) self:F2( { self.ControllableName } ) --- Tanker = { --- id = 'Tanker', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'Tanker', - params = { - } - } + local DCSTask = {id = 'Tanker', params = {}} self:T3( { DCSTask } ) return DCSTask @@ -1718,17 +1768,7 @@ end function CONTROLLABLE:EnRouteTaskEWR( ) self:F2( { self.ControllableName } ) --- EWR = { --- id = 'EWR', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'EWR', - params = { - } - } + local DCSTask = {id = 'EWR', params = {}} self:T3( { DCSTask } ) return DCSTask diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index b85d519b2..82616e283 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -173,6 +173,62 @@ GROUPTEMPLATE.Takeoff = { [GROUP.Takeoff.Cold] = { "TakeOffParking", "From Parking Area" } } +--- Generalized group attributes. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit. +-- @type GROUP.Attribute +-- @field #string AIR_TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets. +-- @field #string AIR_AWACS Airborne Early Warning and Control System. +-- @field #string AIR_FIGHTER Fighter, interceptor, ... airplane. +-- @field #string AIR_BOMBER Aircraft which can be used for strategic bombing. +-- @field #string AIR_TANKER Airplane which can refuel other aircraft. +-- @field #string AIR_TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets. +-- @field #string AIR_ATTACKHELO Attack helicopter. +-- @field #string AIR_UAV Unpiloted Aerial Vehicle, e.g. drones. +-- @field #string AIR_OTHER Any airborne unit that does not fall into any other airborne category. +-- @field #string GROUND_APC Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. +-- @field #string GROUND_TRUCK Unarmed ground vehicles, which has the DCS "Truck" attribute. +-- @field #string GROUND_INFANTRY Ground infantry assets. +-- @field #string GROUND_ARTILLERY Artillery assets. +-- @field #string GROUND_TANK Tanks (modern or old). +-- @field #string GROUND_TRAIN Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. +-- @field #string GROUND_EWR Early Warning Radar. +-- @field #string GROUND_AAA Anti-Aircraft Artillery. +-- @field #string GROUND_SAM Surface-to-Air Missile system or components. +-- @field #string GROUND_OTHER Any ground unit that does not fall into any other ground category. +-- @field #string NAVAL_AIRCRAFTCARRIER Aircraft carrier. +-- @field #string NAVAL_WARSHIP War ship, i.e. cruisers, destroyers, firgates and corvettes. +-- @field #string NAVAL_ARMEDSHIP Any armed ship that is not an aircraft carrier, a cruiser, destroyer, firgatte or corvette. +-- @field #string NAVAL_UNARMEDSHIP Any unarmed naval vessel. +-- @field #string NAVAL_OTHER Any naval unit that does not fall into any other naval category. +-- @field #string OTHER_UNKNOWN Anything that does not fall into any other category. +GROUP.Attribute = { + AIR_TRANSPORTPLANE="Air_TransportPlane", + AIR_AWACS="Air_AWACS", + AIR_FIGHTER="Air_Fighter", + AIR_BOMBER="Air_Bomber", + AIR_TANKER="Air_Tanker", + AIR_TRANSPORTHELO="Air_TransportHelo", + AIR_ATTACKHELO="Air_AttackHelo", + AIR_UAV="Air_UAV", + AIR_OTHER="Air_OtherAir", + GROUND_APC="Ground_APC", + GROUND_TRUCK="Ground_Truck", + GROUND_INFANTRY="Ground_Infantry", + GROUND_ARTILLERY="Ground_Artillery", + GROUND_TANK="Ground_Tank", + GROUND_TRAIN="Ground_Train", + GROUND_EWR="Ground_EWR", + GROUND_AAA="Ground_AAA", + GROUND_SAM="Ground_SAM", + GROUND_OTHER="Ground_OtherGround", + NAVAL_AIRCRAFTCARRIER="Naval_AircraftCarrier", + NAVAL_WARSHIP="Naval_WarShip", + NAVAL_ARMEDSHIP="Naval_ArmedShip", + NAVAL_UNARMEDSHIP="Naval_UnarmedShip", + NAVAL_OTHER="Naval_OtherNaval", + OTHER_UNKNOWN="Other_Unknown", +} + + --- Create a new GROUP from a given GroupTemplate as a parameter. -- Note that the GroupTemplate is NOT spawned into the mission. -- It is merely added to the @{Core.Database}. @@ -810,7 +866,7 @@ end function GROUP:Activate() self:F2( { self.GroupName } ) trigger.action.activateGroup( self:GetDCSObject() ) - return self:GetDCSObject() + return self end @@ -1678,7 +1734,7 @@ function GROUP:Respawn( Template, Reset ) self:F(GroupUnit:GetName()) if GroupUnit:IsAlive() then - self:F("Alive") + self:I("FF Alive") -- Get unit position vector. local GroupUnitVec3 = GroupUnit:GetVec3() @@ -1722,7 +1778,7 @@ function GROUP:Respawn( Template, Reset ) end end - else -- Reset=false or nil + elseif Reset==false then -- Reset=false or nil -- Loop over template units. for UnitID, TemplateUnitData in pairs( Template.units ) do @@ -1764,7 +1820,30 @@ function GROUP:Respawn( Template, Reset ) self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) end - end + else + + local units=self:GetUnits() + + -- Loop over template units. + for UnitID, Unit in pairs(Template.units) do + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit:GetName()==Unit.name then + local coord=unit:GetCoordinate() + local heading=unit:GetHeading() + Unit.x=coord.x + Unit.y=coord.z + Unit.alt=coord.y + Unit.heading=math.rad(heading) + Unit.psi=-Unit.heading + end + end + + end + + end end @@ -2091,6 +2170,117 @@ function GROUP:GetDCSDesc(n) return nil end + +--- Get the generalized attribute of a self. +-- Note that for a heterogenious self, the attribute is determined from the attribute of the first unit! +-- @param #GROUP self +-- @return #string Generalized attribute of the self. +function GROUP:GetAttribute() + + -- Default + local attribute=GROUP.Attribute.OTHER_UNKNOWN --#GROUP.Attribute + + if self then + + ----------- + --- Air --- + ----------- + -- Planes + local transportplane=self:HasAttribute("Transports") and self:HasAttribute("Planes") + local awacs=self:HasAttribute("AWACS") + local fighter=self:HasAttribute("Fighters") or self:HasAttribute("Interceptors") or self:HasAttribute("Multirole fighters") or (self:HasAttribute("Bombers") and not self:HasAttribute("Strategic bombers")) + local bomber=self:HasAttribute("Strategic bombers") + local tanker=self:HasAttribute("Tankers") + local uav=self:HasAttribute("UAVs") + -- Helicopters + local transporthelo=self:HasAttribute("Transport helicopters") + local attackhelicopter=self:HasAttribute("Attack helicopters") + + -------------- + --- Ground --- + -------------- + -- Ground + local apc=self:HasAttribute("Infantry carriers") + local truck=self:HasAttribute("Trucks") and self:GetCategory()==Group.Category.GROUND + local infantry=self:HasAttribute("Infantry") + local artillery=self:HasAttribute("Artillery") + local tank=self:HasAttribute("Old Tanks") or self:HasAttribute("Modern Tanks") + local aaa=self:HasAttribute("AAA") + local ewr=self:HasAttribute("EWR") + local sam=self:HasAttribute("SAM elements") and (not self:HasAttribute("AAA")) + -- Train + local train=self:GetCategory()==Group.Category.TRAIN + + ------------- + --- Naval --- + ------------- + -- Ships + local aircraftcarrier=self:HasAttribute("Aircraft Carriers") + local warship=self:HasAttribute("Heavy armed ships") + local armedship=self:HasAttribute("Armed ships") + local unarmedship=self:HasAttribute("Unarmed ships") + + + -- Define attribute. Order is important. + if transportplane then + attribute=GROUP.Attribute.AIR_TRANSPORTPLANE + elseif awacs then + attribute=GROUP.Attribute.AIR_AWACS + elseif fighter then + attribute=GROUP.Attribute.AIR_FIGHTER + elseif bomber then + attribute=GROUP.Attribute.AIR_BOMBER + elseif tanker then + attribute=GROUP.Attribute.AIR_TANKER + elseif transporthelo then + attribute=GROUP.Attribute.AIR_TRANSPORTHELO + elseif attackhelicopter then + attribute=GROUP.Attribute.AIR_ATTACKHELO + elseif uav then + attribute=GROUP.Attribute.AIR_UAV + elseif apc then + attribute=GROUP.Attribute.GROUND_APC + elseif infantry then + attribute=GROUP.Attribute.GROUND_INFANTRY + elseif artillery then + attribute=GROUP.Attribute.GROUND_ARTILLERY + elseif tank then + attribute=GROUP.Attribute.GROUND_TANK + elseif aaa then + attribute=GROUP.Attribute.GROUND_AAA + elseif ewr then + attribute=GROUP.Attribute.GROUND_EWR + elseif sam then + attribute=GROUP.Attribute.GROUND_SAM + elseif truck then + attribute=GROUP.Attribute.GROUND_TRUCK + elseif train then + attribute=GROUP.Attribute.GROUND_TRAIN + elseif aircraftcarrier then + attribute=GROUP.Attribute.NAVAL_AIRCRAFTCARRIER + elseif warship then + attribute=GROUP.Attribute.NAVAL_WARSHIP + elseif armedship then + attribute=GROUP.Attribute.NAVAL_ARMEDSHIP + elseif unarmedship then + attribute=GROUP.Attribute.NAVAL_UNARMEDSHIP + else + if self:IsGround() then + attribute=GROUP.Attribute.GROUND_OTHER + elseif self:IsShip() then + attribute=GROUP.Attribute.NAVAL_OTHER + elseif self:IsAir() then + attribute=GROUP.Attribute.AIR_OTHER + else + attribute=GROUP.Attribute.OTHER_UNKNOWN + end + end + end + + return attribute +end + + do -- Route methods --- (AIR) Return the Group to an @{Wrapper.Airbase#AIRBASE}. diff --git a/Moose Development/Moose/Wrapper/Scenery.lua b/Moose Development/Moose/Wrapper/Scenery.lua index 01d9af957..9eccde422 100644 --- a/Moose Development/Moose/Wrapper/Scenery.lua +++ b/Moose Development/Moose/Wrapper/Scenery.lua @@ -31,6 +31,11 @@ SCENERY = { } +--- Register scenery object as POSITIONABLE. +--@param #SCENERY self +--@param #string SceneryName Scenery name. +--@param #DCS.Object SceneryObject DCS scenery object. +--@return #SCENERY Scenery object. function SCENERY:Register( SceneryName, SceneryObject ) local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) ) self.SceneryName = SceneryName @@ -38,11 +43,17 @@ function SCENERY:Register( SceneryName, SceneryObject ) return self end +--- Register scenery object as POSITIONABLE. +--@param #SCENERY self +--@return #DCS.Object DCS scenery object. function SCENERY:GetDCSObject() return self.SceneryObject end +--- Register scenery object as POSITIONABLE. +--@param #SCENERY self +--@return #number Threat level 0. +--@return #string "Scenery". function SCENERY:GetThreatLevel() - return 0, "Scenery" end diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index c747bc92e..9be25e3f8 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -478,8 +478,7 @@ end --- Returns the Unit's ammunition. -- @param #UNIT self --- @return DCS#Unit.Ammo --- @return #nil The DCS Unit is not existing or alive. +-- @return DCS#Unit.Ammo Table with ammuntion of the unit (or nil). This can be a complex table! function UNIT:GetAmmo() self:F2( self.UnitName ) @@ -664,8 +663,7 @@ end --- Returns relative amount of fuel (from 0.0 to 1.0) the UNIT has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. -- @param #UNIT self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The DCS Unit is not existing or alive. +-- @return #number The relative amount of fuel (from 0.0 to 1.0) or *nil* if the DCS Unit is not existing or alive. function UNIT:GetFuel() self:F3( self.UnitName ) @@ -1206,4 +1204,81 @@ do -- Detection end -end \ No newline at end of file +end + +--- Get the unit table from a unit's template. +-- @param #UNIT self +-- @return #table Table of the unit template (deep copy) or #nil. +function UNIT:GetTemplate() + + local group=self:GetGroup() + + local name=self:GetName() + + if group then + local template=group:GetTemplate() + + if template then + + for _,unit in pairs(template.units) do + + if unit.name==name then + return UTILS.DeepCopy(unit) + end + end + + end + end + + return nil +end + + +--- Get the payload table from a unit's template. +-- The payload table has elements: +-- +-- * pylons +-- * fuel +-- * chaff +-- * gun +-- +-- @param #UNIT self +-- @return #table Payload table (deep copy) or #nil. +function UNIT:GetTemplatePayload() + + local unit=self:GetTemplate() + + if unit then + return unit.payload + end + + return nil +end + +--- Get the pylons table from a unit's template. This can be a complex table depending on the weapons the unit is carrying. +-- @param #UNIT self +-- @return #table Table of pylons (deepcopy) or #nil. +function UNIT:GetTemplatePylons() + + local payload=self:GetTemplatePayload() + + if payload then + return payload.pylons + end + + return nil +end + +--- Get the fuel of the unit from its template. +-- @param #UNIT self +-- @return #number Fuel of unit in kg. +function UNIT:GetTemplateFuel() + + local payload=self:GetTemplatePayload() + + if payload then + return payload.fuel + end + + return nil +end From 6ab85072d227b4f0f3b54d0db7dbfc0e6a01d3cf Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 17 Feb 2020 23:19:28 +0100 Subject: [PATCH 437/485] DCS 2.5.6 fixes --- Moose Development/Moose/Core/Base.lua | 49 ++ Moose Development/Moose/Core/Database.lua | 18 +- Moose Development/Moose/Core/Event.lua | 622 ++++++++++-------- Moose Development/Moose/Core/Point.lua | 70 +- Moose Development/Moose/Wrapper/Airbase.lua | 53 +- .../Moose/Wrapper/Controllable.lua | 457 +++++-------- 6 files changed, 660 insertions(+), 609 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index e95eac8ce..381c6859f 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -626,6 +626,55 @@ do -- Event Handling -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Unknown precisely what creates this event, likely tied into newer damage model. Will update this page when new information become available. + -- + -- * initiator: The unit that had the failure. + -- + -- @function [parent=#BASE] OnEventDetailedFailure + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any modification to the "Score" as seen on the debrief menu would occur. + -- There is no information on what values the score was changed to. Event is likely similar to player_comment in this regard. + -- @function [parent=#BASE] OnEventScore + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs on the death of a unit. Contains more and different information. Similar to unit_lost it will occur for aircraft before the aircraft crash event occurs. + -- + -- * initiator: The unit that killed the target + -- * target: Target Object + -- * weapon: Weapon Object + -- + -- @function [parent=#BASE] OnEventKill + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any modification to the "Score" as seen on the debrief menu would occur. + -- There is no information on what values the score was changed to. Event is likely similar to player_comment in this regard. + -- @function [parent=#BASE] OnEventScore + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when the game thinks an object is destroyed. + -- + -- * initiator: The unit that is was destroyed. + -- + -- @function [parent=#BASE] OnEventUnitLost + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs shortly after the landing animation of an ejected pilot touching the ground and standing up. Event does not occur if the pilot lands in the water and sub combs to Davey Jones Locker. + -- + -- * initiator: Static object representing the ejected pilot. Place : Aircraft that the pilot ejected from. + -- * place: may not return as a valid object if the aircraft has crashed into the ground and no longer exists. + -- * subplace: is always 0 for unknown reasons. + -- + -- @function [parent=#BASE] OnEventLandingAfterEjection + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + end diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index fa12803fc..f691fb143 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -247,12 +247,15 @@ end --- Adds a Airbase based on the Airbase Name in the DATABASE. -- @param #DATABASE self --- @param #string AirbaseName The name of the airbase +-- @param #string AirbaseName The name of the airbase. +-- @return Wrapper.Airbase#AIRBASE Airbase object. function DATABASE:AddAirbase( AirbaseName ) if not self.AIRBASES[AirbaseName] then self.AIRBASES[AirbaseName] = AIRBASE:Register( AirbaseName ) end + + return self.AIRBASES[AirbaseName] end @@ -893,6 +896,7 @@ end --- @param #DATABASE self function DATABASE:_RegisterAirbases() + --[[ local CoalitionsData = { AirbasesRed = coalition.getAirbases( coalition.side.RED ), AirbasesBlue = coalition.getAirbases( coalition.side.BLUE ), AirbasesNeutral = coalition.getAirbases( coalition.side.NEUTRAL ) } for CoalitionId, CoalitionData in pairs( CoalitionsData ) do for DCSAirbaseId, DCSAirbase in pairs( CoalitionData ) do @@ -903,6 +907,18 @@ function DATABASE:_RegisterAirbases() self:AddAirbase( DCSAirbaseName ) end end + ]] + + for DCSAirbaseId, DCSAirbase in pairs(world.getAirbases()) do + local DCSAirbaseName = DCSAirbase:getName() + + -- This gives the incorrect value to be inserted into the airdromeID for DCS 2.5.6! + local airbaseID=DCSAirbase:getID() + + local airbase=self:AddAirbase( DCSAirbaseName ) + + self:I(string.format("Register Airbase: %s, getID=%d, GetID=%d (unique=%d)", DCSAirbaseName, DCSAirbase:getID(), airbase:GetID(), airbase:GetID(true))) + end return self end diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 4ef56c8ea..eec351941 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -221,9 +221,11 @@ EVENTS = { PlayerComment = world.event.S_EVENT_PLAYER_COMMENT, ShootingStart = world.event.S_EVENT_SHOOTING_START, ShootingEnd = world.event.S_EVENT_SHOOTING_END, + -- Added with DCS 2.5.1 MarkAdded = world.event.S_EVENT_MARK_ADDED, MarkChange = world.event.S_EVENT_MARK_CHANGE, MarkRemoved = world.event.S_EVENT_MARK_REMOVED, + -- Moose Events NewCargo = world.event.S_EVENT_NEW_CARGO, DeleteCargo = world.event.S_EVENT_DELETE_CARGO, NewZone = world.event.S_EVENT_NEW_ZONE, @@ -231,6 +233,12 @@ EVENTS = { NewZoneGoal = world.event.S_EVENT_NEW_ZONE_GOAL, DeleteZoneGoal = world.event.S_EVENT_DELETE_ZONE_GOAL, RemoveUnit = world.event.S_EVENT_REMOVE_UNIT, + -- Added with DCS 2.5.6 + DetailedFailure = world.event.S_EVENT_DETAILED_FAILURE or -1, --We set this to -1 for backward compatibility to DCS 2.5.5 and earlier + Kill = world.event.S_EVENT_KILL or -1, + Score = world.event.S_EVENT_SCORE or -1, + UnitLost = world.event.S_EVENT_UNIT_LOST or -1, + LandingAfterEjection = world.event.S_EVENT_LANDING_AFTER_EJECTION or -1, } --- The Event structure @@ -481,6 +489,32 @@ local _EVENTMETA = { Event = "OnEventRemoveUnit", Text = "S_EVENT_REMOVE_UNIT" }, + -- Added with DCS 2.5.6 + [EVENTS.DetailedFailure] = { + Order = 1, + Event = "OnEventDetailedFailure", + Text = "S_EVENT_DETAILED_FAILURE" + }, + [EVENTS.Kill] = { + Order = 1, + Event = "OnEventKill", + Text = "S_EVENT_KILL" + }, + [EVENTS.Score] = { + Order = 1, + Event = "OnEventScore", + Text = "S_EVENT_SCORE" + }, + [EVENTS.UnitLost] = { + Order = 1, + Event = "OnEventUnitLost", + Text = "S_EVENT_UNIT_LOST" + }, + [EVENTS.LandingAfterEjection] = { + Order = 1, + Event = "OnEventLandingAfterEjection", + Text = "S_EVENT_LANDING_AFTER_EJECTION" + }, } @@ -881,258 +915,213 @@ function EVENT:onEvent( Event ) end + -- Get event meta data. local EventMeta = _EVENTMETA[Event.id] - - --self:E( { EventMeta.Text, Event } ) -- Activate the see all incoming events ... - - if self and - self.Events and - self.Events[Event.id] and - self.MissionEnd == false and - ( Event.initiator ~= nil or ( Event.initiator == nil and Event.id ~= EVENTS.PlayerLeaveUnit ) ) then - - if Event.id and Event.id == EVENTS.MissionEnd then - self.MissionEnd = true - end - - if Event.initiator then - - Event.IniObjectCategory = Event.initiator:getCategory() - - if Event.IniObjectCategory == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) - if not Event.IniUnit then - -- Unit can be a CLIENT. Most likely this will be the case ... - Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) - end - Event.IniDCSGroupName = "" - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - Event.IniDCSGroupName = Event.IniDCSGroup:getName() - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - --if Event.IniGroup then - Event.IniGroupName = Event.IniDCSGroupName - --end - end - Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - end - - if Event.IniObjectCategory == Object.Category.STATIC then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - - if Event.IniObjectCategory == Object.Category.CARGO then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = CARGO:FindByName( Event.IniDCSUnitName ) - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - - if Event.IniObjectCategory == Object.Category.SCENERY then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.initiator:isExist() and Event.IniDCSUnit:getTypeName() or "SCENERY" -- TODO: Bug fix for 2.1! - end - end - - if Event.target then - - Event.TgtObjectCategory = Event.target:getCategory() - - if Event.TgtObjectCategory == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - --if Event.TgtGroup then - Event.TgtGroupName = Event.TgtDCSGroupName - --end - end - Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.STATIC then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.SCENERY then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target ) - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - end - - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit! - Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() - Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon:getCoalition() - Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon:getDesc().category - Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - - -- Place should be given for takeoff and landing events as well as base captured. It should be a DCS airbase. - if Event.place then - Event.Place=AIRBASE:Find(Event.place) - Event.PlaceName=Event.Place:GetName() - end - - -- Mark points. - if Event.idx then - Event.MarkID=Event.idx - Event.MarkVec3=Event.pos - Event.MarkCoordinate=COORDINATE:NewFromVec3(Event.pos) - Event.MarkText=Event.text - Event.MarkCoalition=Event.coalition - Event.MarkGroupID = Event.groupID - end - - if Event.cargo then - Event.Cargo = Event.cargo - Event.CargoName = Event.cargo.Name - end - - if Event.zone then - Event.Zone = Event.zone - Event.ZoneName = Event.zone.ZoneName - end - - local PriorityOrder = EventMeta.Order - local PriorityBegin = PriorityOrder == -1 and 5 or 1 - local PriorityEnd = PriorityOrder == -1 and 1 or 5 - - if Event.IniObjectCategory ~= Object.Category.STATIC then - self:F( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) - end - - for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do - - if self.Events[Event.id][EventPriority] then - - -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. - for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do - - --if Event.IniObjectCategory ~= Object.Category.STATIC then - -- self:E( { "Evaluating: ", EventClass:GetClassNameAndID() } ) - --end - - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - - -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. - if EventData.EventUnit then - - -- So now the EventClass must be a UNIT class!!! We check if it is still "Alive". - if EventClass:IsAlive() or - Event.id == EVENTS.PlayerEnterUnit or - Event.id == EVENTS.Crash or - Event.id == EVENTS.Dead or - Event.id == EVENTS.RemoveUnit then - - local UnitName = EventClass:GetName() - - if ( EventMeta.Side == "I" and UnitName == Event.IniDCSUnitName ) or - ( EventMeta.Side == "T" and UnitName == Event.TgtDCSUnitName ) then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:F( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ EventMeta.Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:F( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - else - -- The EventClass is not alive anymore, we remove it from the EventHandlers... - self:RemoveEvent( EventClass, Event.id ) - end - else - - -- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. - if EventData.EventGroup then - - -- So now the EventClass must be a GROUP class!!! We check if it is still "Alive". - if EventClass:IsAlive() or - Event.id == EVENTS.PlayerEnterUnit or - Event.id == EVENTS.Crash or - Event.id == EVENTS.Dead or - Event.id == EVENTS.RemoveUnit then - - -- We can get the name of the EventClass, which is now always a GROUP object. - local GroupName = EventClass:GetName() - if ( EventMeta.Side == "I" and GroupName == Event.IniDCSGroupName ) or - ( EventMeta.Side == "T" and GroupName == Event.TgtDCSGroupName ) then - + -- Check if this is a known event? + if EventMeta then + + if self and + self.Events and + self.Events[Event.id] and + self.MissionEnd == false and + ( Event.initiator ~= nil or ( Event.initiator == nil and Event.id ~= EVENTS.PlayerLeaveUnit ) ) then + + if Event.id and Event.id == EVENTS.MissionEnd then + self.MissionEnd = true + end + + if Event.initiator then + + Event.IniObjectCategory = Event.initiator:getCategory() + + if Event.IniObjectCategory == Object.Category.UNIT then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniDCSGroup = Event.IniDCSUnit:getGroup() + Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) + if not Event.IniUnit then + -- Unit can be a CLIENT. Most likely this will be the case ... + Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) + end + Event.IniDCSGroupName = "" + if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then + Event.IniDCSGroupName = Event.IniDCSGroup:getName() + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + --if Event.IniGroup then + Event.IniGroupName = Event.IniDCSGroupName + --end + end + Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + end + + if Event.IniObjectCategory == Object.Category.STATIC then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + end + + if Event.IniObjectCategory == Object.Category.CARGO then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = CARGO:FindByName( Event.IniDCSUnitName ) + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + end + + if Event.IniObjectCategory == Object.Category.SCENERY then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.initiator:isExist() and Event.IniDCSUnit:getTypeName() or "SCENERY" -- TODO: Bug fix for 2.1! + end + end + + if Event.target then + + Event.TgtObjectCategory = Event.target:getCategory() + + if Event.TgtObjectCategory == Object.Category.UNIT then + Event.TgtDCSUnit = Event.target + Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) + Event.TgtDCSGroupName = "" + if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then + Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() + Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) + --if Event.TgtGroup then + Event.TgtGroupName = Event.TgtDCSGroupName + --end + end + Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + + if Event.TgtObjectCategory == Object.Category.STATIC then + Event.TgtDCSUnit = Event.target + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + + if Event.TgtObjectCategory == Object.Category.SCENERY then + Event.TgtDCSUnit = Event.target + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target ) + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + end + + if Event.weapon then + Event.Weapon = Event.weapon + Event.WeaponName = Event.Weapon:getTypeName() + Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit! + Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() + Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon:getCoalition() + Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon:getDesc().category + Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() + --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() + end + + -- Place should be given for takeoff and landing events as well as base captured. It should be a DCS airbase. + if Event.place then + if Event.id==EVENTS.LandingAfterEjection then + -- Place is here the UNIT of which the pilot ejected. + Event.Place=UNIT:Find(Event.place) + else + Event.Place=AIRBASE:Find(Event.place) + Event.PlaceName=Event.Place:GetName() + end + end + + -- Mark points. + if Event.idx then + Event.MarkID=Event.idx + Event.MarkVec3=Event.pos + Event.MarkCoordinate=COORDINATE:NewFromVec3(Event.pos) + Event.MarkText=Event.text + Event.MarkCoalition=Event.coalition + Event.MarkGroupID = Event.groupID + end + + if Event.cargo then + Event.Cargo = Event.cargo + Event.CargoName = Event.cargo.Name + end + + if Event.zone then + Event.Zone = Event.zone + Event.ZoneName = Event.zone.ZoneName + end + + local PriorityOrder = EventMeta.Order + local PriorityBegin = PriorityOrder == -1 and 5 or 1 + local PriorityEnd = PriorityOrder == -1 and 1 or 5 + + if Event.IniObjectCategory ~= Object.Category.STATIC then + self:F( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) + end + + for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do + + if self.Events[Event.id][EventPriority] then + + -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. + for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do + + --if Event.IniObjectCategory ~= Object.Category.STATIC then + -- self:E( { "Evaluating: ", EventClass:GetClassNameAndID() } ) + --end + + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) + + -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. + if EventData.EventUnit then + + -- So now the EventClass must be a UNIT class!!! We check if it is still "Alive". + if EventClass:IsAlive() or + Event.id == EVENTS.PlayerEnterUnit or + Event.id == EVENTS.Crash or + Event.id == EVENTS.Dead or + Event.id == EVENTS.RemoveUnit then + + local UnitName = EventClass:GetName() + + if ( EventMeta.Side == "I" and UnitName == Event.IniDCSUnitName ) or + ( EventMeta.Side == "T" and UnitName == Event.TgtDCSUnitName ) then + -- First test if a EventFunction is Set, otherwise search for the default function if EventData.EventFunction then - + if Event.IniObjectCategory ~= 3 then - self:F( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) + self:F( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) end - + local Result, Value = xpcall( function() - return EventData.EventFunction( EventClass, Event, unpack( EventData.Params ) ) + return EventData.EventFunction( EventClass, Event ) end, ErrorHandler ) else @@ -1143,74 +1132,129 @@ function EVENT:onEvent( Event ) -- Now call the default event function. if Event.IniObjectCategory ~= 3 then - self:F( { "Calling " .. EventMeta.Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) + self:F( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) end - + local Result, Value = xpcall( function() - return EventFunction( EventClass, Event, unpack( EventData.Params ) ) + return EventFunction( EventClass, Event ) end, ErrorHandler ) end end end else -- The EventClass is not alive anymore, we remove it from the EventHandlers... - --self:RemoveEvent( EventClass, Event.id ) - end + self:RemoveEvent( EventClass, Event.id ) + end else - - -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. - -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. - if not EventData.EventUnit then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventFunction then - - -- There is an EventFunction defined, so call the EventFunction. - if Event.IniObjectCategory ~= 3 then - self:F2( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) - end, ErrorHandler ) - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ EventMeta.Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:F2( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + + -- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. + if EventData.EventGroup then + + -- So now the EventClass must be a GROUP class!!! We check if it is still "Alive". + if EventClass:IsAlive() or + Event.id == EVENTS.PlayerEnterUnit or + Event.id == EVENTS.Crash or + Event.id == EVENTS.Dead or + Event.id == EVENTS.RemoveUnit then + + -- We can get the name of the EventClass, which is now always a GROUP object. + local GroupName = EventClass:GetName() + + if ( EventMeta.Side == "I" and GroupName == Event.IniDCSGroupName ) or + ( EventMeta.Side == "T" and GroupName == Event.TgtDCSGroupName ) then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.EventFunction then + + if Event.IniObjectCategory ~= 3 then + self:F( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) + end + + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event, unpack( EventData.Params ) ) + end, ErrorHandler ) + + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ EventMeta.Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + if Event.IniObjectCategory ~= 3 then + self:F( { "Calling " .. EventMeta.Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) + end + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event, unpack( EventData.Params ) ) + end, ErrorHandler ) + end end - + end + else + -- The EventClass is not alive anymore, we remove it from the EventHandlers... + --self:RemoveEvent( EventClass, Event.id ) + end + else + + -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. + -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. + if not EventData.EventUnit then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.EventFunction then + + -- There is an EventFunction defined, so call the EventFunction. + if Event.IniObjectCategory ~= 3 then + self:F2( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + end local Result, Value = xpcall( function() - local Result, Value = EventFunction( EventClass, Event ) - return Result, Value + return EventData.EventFunction( EventClass, Event ) end, ErrorHandler ) + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ EventMeta.Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + if Event.IniObjectCategory ~= 3 then + self:F2( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + end + + local Result, Value = xpcall( + function() + local Result, Value = EventFunction( EventClass, Event ) + return Result, Value + end, ErrorHandler ) + end end + end - end end end end end - end - - -- 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. - -- To prevent this from happening, the Cargo object has a flag NoDestroy. - -- When true, the SET_CARGO won't Remove the Cargo object from the set. - -- But we need to switch that flag off after the event handlers have been called. - if Event.id == EVENTS.DeleteCargo then - Event.Cargo.NoDestroy = nil + + -- 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. + -- To prevent this from happening, the Cargo object has a flag NoDestroy. + -- When true, the SET_CARGO won't Remove the Cargo object from the set. + -- But we need to switch that flag off after the event handlers have been called. + if Event.id == EVENTS.DeleteCargo then + Event.Cargo.NoDestroy = nil + end + else + self:T( { EventMeta.Text, Event } ) end else - self:T( { EventMeta.Text, Event } ) + self:E(string.format("WARNING: Could not get EVENTMETA data for event ID=%d! Is this an unknown/new DCS event?", tostring(Event.id))) end Event = nil diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 919f8ec36..8ab8e2ae5 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1215,34 +1215,64 @@ do -- COORDINATE --- Build an ground type route point. -- @param #COORDINATE self - -- @param #number Speed (optional) Speed in km/h. The default speed is 20 km/h. - -- @param #string Formation (optional) The route point Formation, which is a text string that specifies exactly the Text in the Type of the route point, like "Vee", "Echelon Right". - -- @param #table DCSTasks A table of DCS tasks that are executed at the waypoints. Mind the curly brackets {}! + -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. + -- @param #string Formation (Optional) The route point Formation, which is a text string that specifies exactly the Text in the Type of the route point, like "Vee", "Echelon Right". + -- @param #table DCSTasks (Optional) A table of DCS tasks that are executed at the waypoints. Mind the curly brackets {}! -- @return #table The route point. function COORDINATE:WaypointGround( Speed, Formation, DCSTasks ) - self:F2( { Formation, Speed } ) + self:F2( { Speed, Formation, DCSTasks } ) local RoutePoint = {} - RoutePoint.x = self.x - RoutePoint.y = self.z - - RoutePoint.action = Formation or "" - --RoutePoint.formation_template = Formation and "" or nil - + + RoutePoint.x = self.x + RoutePoint.y = self.z + + RoutePoint.alt = self:GetLandHeight()+1 -- self.y + RoutePoint.alt_type = COORDINATE.WaypointAltType.BARO + + RoutePoint.action = Formation or "Off Road" + RoutePoint.formation_template="" + + RoutePoint.ETA=0 + RoutePoint.ETA_locked=true RoutePoint.speed = ( Speed or 20 ) / 3.6 RoutePoint.speed_locked = true - -- ["task"] = - -- { - -- ["id"] = "ComboTask", - -- ["params"] = - -- { - -- ["tasks"] = - -- { - -- }, -- end of ["tasks"] - -- }, -- end of ["params"] - -- }, -- end of ["task"] + RoutePoint.task = {} + RoutePoint.task.id = "ComboTask" + RoutePoint.task.params = {} + RoutePoint.task.params.tasks = DCSTasks or {} + + return RoutePoint + end + + --- Build route waypoint point for Naval units. + -- @param #COORDINATE self + -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. + -- @param #string Depth (Optional) Dive depth in meters. Only for submarines. Default is COORDINATE.y component. + -- @param #table DCSTasks (Optional) A table of DCS tasks that are executed at the waypoints. Mind the curly brackets {}! + -- @return #table The route point. + function COORDINATE:WaypointNaval( Speed, Depth, DCSTasks ) + self:F2( { Speed, Depth, DCSTasks } ) + + local RoutePoint = {} + + RoutePoint.x = self.x + RoutePoint.y = self.z + + RoutePoint.alt = Depth or self.y -- Depth is for submarines only. Ships should have alt=0. + RoutePoint.alt_type = "BARO" + + RoutePoint.type = "Turning Point" + RoutePoint.action = "Turning Point" + RoutePoint.formation_template = "" + + RoutePoint.ETA=0 + RoutePoint.ETA_locked=true + + RoutePoint.speed = ( Speed or 20 ) / 3.6 + RoutePoint.speed_locked = true RoutePoint.task = {} RoutePoint.task.id = "ComboTask" diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index cf349c3b3..14c93a7a9 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -342,8 +342,9 @@ AIRBASE.TerminalType = { -- @return Wrapper.Airbase#AIRBASE function AIRBASE:Register( AirbaseName ) - local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) + local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) --#AIRBASE self.AirbaseName = AirbaseName + self.AirbaseID = self:GetID(true) self.AirbaseZone = ZONE_RADIUS:New( AirbaseName, self:GetVec2(), 2500 ) return self end @@ -380,7 +381,7 @@ function AIRBASE:FindByID(id) for name,_airbase in pairs(_DATABASE.AIRBASES) do local airbase=_airbase --#AIRBASE - local aid=tonumber(airbase:GetID()) + local aid=tonumber(airbase:GetID(true)) if aid==id then return airbase @@ -430,6 +431,54 @@ function AIRBASE.GetAllAirbases(coalition, category) return airbases end +--- Get ID of the airbase. +-- @param #AIRBASE self +-- @param #boolean unique (Optional) If true, ships will get a negative sign as the unit ID might be the same as an airbase ID. Default off! +-- @return #number The airbase ID. +function AIRBASE:GetID(unique) + + if self.AirbaseID then + + return unique and self.AirbaseID or math.abs(self.AirbaseID) + + else + + for DCSAirbaseId, DCSAirbase in pairs(world.getAirbases()) do + + -- Get the airbase name. + local AirbaseName = DCSAirbase:getName() + + -- This gives the incorrect value to be inserted into the airdromeID for DCS 2.5.6! + local airbaseID=tonumber(DCSAirbase:getID()) + + -- No way AFIK to get the DCS version. So we check if the event exists. That should tell us if we are on DCS 2.5.6 or prior to that. + if world.event.S_EVENT_KILL and world.event.S_EVENT_KILL>0 and self:GetAirbaseCategory()==Airbase.Category.AIRDROME then + + -- We have to take the key value of this loop! + airbaseID=DCSAirbaseId + + -- Now another quirk: for Caucasus, we need to add 11 to the key value to get the correct ID. See https://forums.eagle.ru/showpost.php?p=4210774&postcount=11 + if UTILS.GetDCSMap()==DCSMAP.Caucasus then + airbaseID=airbaseID+11 + end + end + + if AirbaseName==self.AirbaseName then + if self:GetAirbaseCategory()==Airbase.Category.SHIP then + -- Ships get a negative sign as their unit number might be the same as the ID of another airbase. + return unique and -airbaseID or airbaseID + else + return airbaseID + end + end + + end + + end + + return nil +end + --- Returns a table of parking data for a given airbase. If the optional parameter *available* is true only available parking will be returned, otherwise all parking at the base is returned. Term types have the following enumerated values: -- diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 45e1ec861..97f17d59a 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -861,34 +861,18 @@ function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, At -- } -- } - local DirectionEnabled = nil - if Direction then - DirectionEnabled = true - else - DirectionEnabled = false - Direction=0 - end - local AltitudeEnabled = nil - if Altitude then - AltitudeEnabled = true - else - AltitudeEnabled = false - Altitude=0 - end - - local DCSTask - DCSTask = { id = 'AttackGroup', + local DCSTask = { id = 'AttackGroup', params = { groupId = AttackGroup:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, + weaponType = WeaponType or 1073741822, + expend = WeaponExpend or "Auto", + attackQtyLimit = AttackQty and true or false, attackQty = AttackQty, - directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or nil, + altitudeEnabled = Altitude and true or false, altitude = Altitude, - attackQtyLimit = AttackQtyLimit, }, }, @@ -914,15 +898,15 @@ function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, Atta id = 'AttackUnit', params = { unitId = AttackUnit:GetID(), - groupAttack = GroupAttack or false, + groupAttack = GroupAttack and GroupAttack or false, expend = WeaponExpend or "Auto", directionEnabled = Direction and true or false, - direction = math.rad(Direction or 0), + direction = Direction and math.rad(Direction) or nil, altitudeEnabled = Altitude and true or false, - altitude = Altitude or math.max(1000, AttackUnit:GetAltitude()), + altitude = Altitude, attackQtyLimit = AttackQty and true or false, attackQty = AttackQty, - weaponType = WeaponType + weaponType = WeaponType or 1073741822, } } @@ -946,25 +930,6 @@ end function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb ) self:F( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb } ) - local _groupattack=false - if GroupAttack then - _groupattack=GroupAttack - end - - local _direction=0 - local _directionenabled=false - if Direction then - _direction=math.rad(Direction) - _directionenabled=true - end - - local _altitude=5000 - local _altitudeenabled=false - if Altitude then - _altitude=Altitude - _altitudeenabled=true - end - local _attacktype=nil if Divebomb then _attacktype="Dive" @@ -975,17 +940,18 @@ function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, D DCSTask = { id = 'Bombing', params = { + point = Vec2, x = Vec2.x, y = Vec2.y, - groupAttack = _groupattack, + groupAttack = GroupAttack and GroupAttack or false, expend = WeaponExpend or "Auto", - attackQtyLimit = false, - attackQty = AttackQty or 1, - directionEnabled = _directionenabled, - direction = _direction, - altitudeEnabled = _altitudeenabled, - altitude = _altitude, - weaponType = WeaponType, + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or nil, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + weaponType = WeaponType or 1073741822, attackType = _attacktype, }, } @@ -998,7 +964,7 @@ end -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. -- @param #boolean GroupAttack (Optional) If true, all units in the group will attack the Unit when found. --- @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 DCS#AI.Task.WeaponExpend WeaponExpend (Optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit 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 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 #number Altitude (Optional) The altitude [meters] from where to attack. Default 30 m. @@ -1021,7 +987,7 @@ function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, Atta directionEnabled = Direction and true or false, direction = Direction, altitudeEnabled = Altitude and true or false, - altitude = Altitude or 30, + altitude = Altitude, weaponType = WeaponType or 1073741822, }, }, @@ -1035,93 +1001,62 @@ end -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. -- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @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 DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit 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 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 #number Altitude (optional) The altitude from where to attack. -- @param #number WeaponType (optional) The WeaponType. --- @param #number CarpetLength (optional) default to 500 m. --- @return DCS#Task The DCS task structure. +-- @param #number CarpetLength (optional) default to 500 m. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskCarpetBombing(Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, CarpetLength) self:F2( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, CarpetLength } ) - - local _groupattack=false - if GroupAttack then - _groupattack=GroupAttack - end - - local _direction=0 - local _directionenabled=false - if Direction then - _direction=math.rad(Direction) - _directionenabled=true - end - - local _altitude=0 - local _altitudeenabled=false - if Altitude then - _altitude=Altitude - _altitudeenabled=true - end - - -- default to 500m - local _carpetLength = 500 - if CarpetLength then - _carpetLength = CarpetLength - end - - local _weaponexpend = "Auto" - if WeaponExpend then - _weaponexpend = WeaponExpend - end - + -- Build Task Structure - local DCSTask - DCSTask = { + local DCSTask = { id = 'CarpetBombing', params = { attackType = "Carpet", point = Vec2, x = Vec2.x, - y = Vec2.y, - groupAttack = _groupattack, - carpetLength = _carpetLength, - weaponType = WeaponType, - expend = "All", - attackQtyLimit = false, --AttackQty and true or false, - attackQty = AttackQty or 1, - directionEnabled = _directionenabled, - direction = _direction, - altitudeEnabled = _altitudeenabled, - altitude = _altitude + y = Vec2.y, + groupAttack = GroupAttack and GroupAttack or false, + carpetLength = CarpetLength or 500, + weaponType = WeaponType or ENUMS.WeaponFlag.AnyBomb, + expend = WeaponExpend or "All", + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or nil, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, } } - + return DCSTask end ---- (AIR) Following another airborne controllable. --- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. --- Used to support CarpetBombing Task +--- (AIR) Following another airborne controllable. +-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. +-- Used to support CarpetBombing Task -- @param #CONTROLLABLE self -- @param #CONTROLLABLE FollowControllable The controllable to be followed. -- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. -- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskFollowBigFormation(FollowControllable, Vec3, LastWaypointIndex ) - - local DCSTask = { + + local DCSTask = { id = 'FollowBigFormation', params = { groupId = FollowControllable:GetID(), - pos = Vec3, + pos = Vec3, lastWptIndexFlag = LastWaypointIndex and true or false, lastWptIndex = LastWaypointIndex } } - + return DCSTask end @@ -1137,7 +1072,7 @@ function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, Distri -- Table of group IDs for embarking. local g4e={} - + if GroupSetForEmbarking then for _,_group in pairs(GroupSetForEmbarking:GetSet()) do local group=_group --Wrapper.Group#GROUP @@ -1147,9 +1082,17 @@ function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, Distri self:E("ERROR: No groups for embarking specified!") return nil end - - - + + -- Table of group IDs for embarking. + local Distribution={} + + if DistributionGroupSet then + for _,_group in pairs(DistributionGroupSet:GetSet()) do + local group=_group --Wrapper.Group#GROUP + table.insert(Distribution, group:GetID()) + end + end + local DCSTask = { id = 'Embarking', params = { @@ -1157,7 +1100,7 @@ function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, Distri x = Vec2.x, y = Vec2.y, groupsForEmbarking = g4e, - durationFlag = Duration and true or false, + durationFlag = Duration and true or false, duration = Duration, distributionFlag = DistributionGroupSet and true or false, distribution = Distribution, @@ -1169,68 +1112,22 @@ function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, Distri end - ---- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. --- @param #CONTROLLABLE self --- @param DCS#Vec2 Vec2 The point where to wait. --- @param #number Duration The duration in seconds to wait. --- @param #CONTROLLABLE EmbarkingControllable The controllable to be embarked. --- @return DCS#Task The DCS task structure -function CONTROLLABLE:TaskDisembarking(Vec2, Duration, EmbarkingControllable) - - -- Table of group IDs for embarking. - local g4e={} - - if GroupSetForEmbarking then - for _,_group in pairs(GroupSetForEmbarking:GetSet()) do - local group=_group --Wrapper.Group#GROUP - table.insert(g4e, group:GetID()) - end - else - self:E("ERROR: No groups for embarking specified!") - return nil - end - - - local DCSTask = { - id = 'Disembarking', - params = { - point = Vec2, - x = Vec2.x, - y = Vec2.y, - duration = Duration, - groupsForEmbarking = { EmbarkingControllable:GetID() }, - durationFlag = durationflag, - distributionFlag = false, - distribution = {}, - } - } - - return DCSTask -end - ----- - --- (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Point The point to hold the position. --- @param #number Altitude The altitude [m] to hold the position. +-- @param #number Altitude The altitude AGL in meters to hold the position. -- @param #number Speed The speed [m/s] flying when holding the position. -- @return #CONTROLLABLE self function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) self:F2( { self.ControllableName, Point, Altitude, Speed } ) - local LandHeight = land.getHeight( Point ) - - self:T3( { LandHeight } ) - local DCSTask = { id = 'Orbit', params = { pattern = AI.Task.OrbitPattern.CIRCLE, point = Point, speed = Speed, - altitude = Altitude + LandHeight + altitude = Altitude + land.getHeight( Point ) } } @@ -1331,7 +1228,7 @@ function CONTROLLABLE:TaskBombingRunway(Airbase, WeaponType, WeaponExpend, Attac expend = WeaponExpend or AI.Task.WeaponExpend.ALL, attackQty = AttackQty or 1, direction = Direction and math.rad(Direction) or nil, - groupAttack = GroupAttack, + groupAttack = GroupAttack and GroupAttack or false, }, } @@ -1345,7 +1242,7 @@ end function CONTROLLABLE:TaskRefueling() local DCSTask={id='Refueling', params={}} - + return DCSTask end @@ -1419,14 +1316,13 @@ function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) lastWptIndexFlagChangedManually = true end - local DCSTask - DCSTask = { + local DCSTask = { id = 'Follow', params = { - groupId = FollowControllable:GetID(), - pos = Vec3, - lastWptIndexFlag = LastWaypointIndexFlag, - lastWptIndex = LastWaypointIndex, + groupId = FollowControllable:GetID(), + pos = Vec3, + lastWptIndexFlag = LastWaypointIndexFlag, + lastWptIndex = LastWaypointIndex, lastWptIndexFlagChangedManually = lastWptIndexFlagChangedManually, } } @@ -1444,7 +1340,7 @@ end -- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. -- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. -- @param #number EngagementDistance Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. --- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. +-- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. Default {"Air"}. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes } ) @@ -1461,22 +1357,16 @@ function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, E -- } -- } - local LastWaypointIndexFlag = false - if LastWaypointIndex then - LastWaypointIndexFlag = true - end - - TargetTypes=TargetTypes or {} - local DCSTask - DCSTask = { id = 'Escort', + DCSTask = { + id = 'Escort', params = { - groupId = FollowControllable:GetID(), - pos = Vec3, - lastWptIndexFlag = LastWaypointIndexFlag, - lastWptIndex = LastWaypointIndex, + groupId = FollowControllable:GetID(), + pos = Vec3, + lastWptIndexFlag = LastWaypointIndex and true or false, + lastWptIndex = LastWaypointIndex, engagementDistMax = EngagementDistance, - targetTypes = TargetTypes, + targetTypes = TargetTypes or {"Air"}, }, }, @@ -1582,7 +1472,7 @@ function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority local DCSTask = { id = 'EngageTargets', params = { - maxDistEnabled = Distance and true or false, + maxDistEnabled = Distance and true or false, maxDist = Distance, targetTypes = TargetTypes or {"Air"}, priority = Priority or 0, @@ -1650,29 +1540,19 @@ function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, -- } -- } - local DirectionEnabled = nil - if Direction then - DirectionEnabled = true - end - - local AltitudeEnabled = nil - if Altitude then - AltitudeEnabled = true - end - - local DCSTask - DCSTask = { id = 'EngageControllable', + local DCSTask = { + id = 'EngageControllable', params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, - altitude = Altitude, - attackQtyLimit = AttackQtyLimit, - priority = Priority, + groupId = AttackGroup:GetID(), + weaponType = WeaponType, + expend = WeaponExpend or "Auto", + directionEnabled = Direction and true or false, + direction = Direction, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + priority = Priority or 1, }, }, @@ -1694,36 +1574,22 @@ end -- @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. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack ) - self:F2( { self.ControllableName, EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack } ) + self:F2( { self.ControllableName, EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack } ) - -- EngageUnit = { - -- id = 'EngageUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- priority = number, - -- } - -- } - - local DCSTask - DCSTask = { id = 'EngageUnit', + local DCSTask = { + id = 'EngageUnit', params = { - unitId = EngageUnit:GetID(), - priority = Priority or 1, - groupAttack = GroupAttack or false, - visible = Visible or false, - expend = WeaponExpend or "Auto", - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude, - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, + unitId = EngageUnit:GetID(), + priority = Priority or 1, + groupAttack = GroupAttack and GroupAttack or false, + visible = Visible and Visible or false, + expend = WeaponExpend or "Auto", + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or nil, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, controllableAttack = ControllableAttack, }, }, @@ -1739,7 +1605,7 @@ end -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskAWACS( ) self:F2( { self.ControllableName } ) - + local DCSTask = {id = 'AWACS', params = {}} self:T3( { DCSTask } ) @@ -1782,33 +1648,22 @@ end -- If the task is assigned to the controllable lead unit will be a FAC. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param DCS#AI.Task.Designation Designation (optional) Designation type. +-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default is 0. +-- @param #number WeaponType (Optional) Bitmask of weapon types those allowed to use. Default is "Auto". +-- @param DCS#AI.Task.Designation Designation (Optional) Designation type. -- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink ) self:F2( { self.ControllableName, AttackGroup, WeaponType, Priority, Designation, Datalink } ) --- FAC_EngageControllable = { --- id = 'FAC_EngageControllable', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean, --- priority = number, --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_EngageControllable', + local DCSTask = { + id = 'FAC_EngageControllable', params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, + groupId = AttackGroup:GetID(), + weaponType = WeaponType or "Auto", designation = Designation, - datalink = Datalink, - priority = Priority, + datalink = Datalink and Datalink or false, + priority = Priority or 0, } } @@ -1979,9 +1834,12 @@ function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) self:F2( { self.ControllableName, Point, Radius } ) local DCSTask --DCS#Task - DCSTask = { id = 'EmbarkToTransport', - params = { x = Point.x, - y = Point.y, + DCSTask = { + id = 'EmbarkToTransport', + params = { + point = Point, + x = Point.x, + y = Point.y, zoneRadius = Radius, } } @@ -2190,12 +2048,12 @@ do -- Patrol methods if not self:IsInstanceOf( "GROUP" ) then PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP end - + DelayMin=DelayMin or 1 if not DelayMax or DelayMax we take the short cut. - + table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) @@ -2584,7 +2447,7 @@ do -- Route methods table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) end - + -- Add passing waypoint function. if WaypointFunction then local N=#route @@ -2592,10 +2455,10 @@ do -- Route methods waypoint.task = {} waypoint.task.id = "ComboTask" waypoint.task.params = {} - waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} + waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} end end - + return route, canroad end @@ -2631,7 +2494,7 @@ do -- Route methods table.insert(route, PathOnRail[2]:WaypointGround(Speed, "On Railroad")) end - + -- Add passing waypoint function. if WaypointFunction then local N=#route @@ -2639,7 +2502,7 @@ do -- Route methods waypoint.task = {} waypoint.task.id = "ComboTask" waypoint.task.params = {} - waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} + waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} end end @@ -2904,9 +2767,9 @@ function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRad self:F2( self.ControllableName ) local DCSControllable = self:GetDCSObject() - + if DCSControllable then - + local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil @@ -3038,9 +2901,9 @@ function CONTROLLABLE:IsGroupDetected( Group, DetectVisual, DetectOptical, Detec for _,_unit in pairs(Group:GetUnits()) do local unit=_unit --Wrapper.Unit#UNIT if unit and unit:IsAlive() then - + local isdetected=self:IsUnitDetected(unit, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) - + if isdetected then return true end @@ -3071,23 +2934,23 @@ function CONTROLLABLE:GetDetectedUnitSet(DetectVisual, DetectOptical, DetectRada local detectedtargets=self:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) local unitset=SET_UNIT:New() - + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do local DetectedObject=Detection.object -- DCS#Object if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then local unit=UNIT:Find(DetectedObject) - + if unit and unit:IsAlive() then - + if not unitset:FindUnit(unit:GetName()) then - unitset:AddUnit(unit) + unitset:AddUnit(unit) end - + end end end - + return unitset end @@ -3108,24 +2971,24 @@ function CONTROLLABLE:GetDetectedGroupSet(DetectVisual, DetectOptical, DetectRad local detectedtargets=self:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) local groupset=SET_GROUP:New() - + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do local DetectedObject=Detection.object -- DCS#Object if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then local unit=UNIT:Find(DetectedObject) - + if unit and unit:IsAlive() then local group=unit:GetGroup() - + if group and not groupset:FindGroup(group:GetName()) then groupset:AddGroup(group) end - + end end end - + return groupset end @@ -3749,30 +3612,30 @@ end -- @return #boolean true if Controllable contains Helicopters. function CONTROLLABLE:IsHelicopter() self:F2() - + local DCSObject = self:GetDCSObject() - + if DCSObject then local Category = DCSObject:getDesc().category return Category == Unit.Category.HELICOPTER end - + return nil end --- Sets Controllable Option for Restriction of Afterburner. -- @param #CONTROLLABLE self --- @param #boolean RestrictBurner If true, restrict burner. If false or nil, allow (unrestrict) burner. +-- @param #boolean RestrictBurner If true, restrict burner. If false or nil, allow (unrestrict) burner. function CONTROLLABLE:OptionRestrictBurner(RestrictBurner) self:F2({self.ControllableName}) - + local DCSControllable = self:GetDCSObject() - + if DCSControllable then local Controller = self:_GetController() - + if Controller then - + -- Issue https://github.com/FlightControl-Master/MOOSE/issues/1216 if RestrictBurner == true then if self:IsAir() then @@ -3783,8 +3646,8 @@ function CONTROLLABLE:OptionRestrictBurner(RestrictBurner) Controller:setOption(16, false) end end - + end end - + end From 9583168c4d0250a421b29acfb585384523922fd7 Mon Sep 17 00:00:00 2001 From: 132nd-Entropy Date: Thu, 20 Feb 2020 14:38:11 +0100 Subject: [PATCH 438/485] Update ATIS.lua --- Moose Development/Moose/Ops/ATIS.lua | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index db3be94ab..f5c1199a6 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -354,10 +354,10 @@ ATIS.Alphabet = { --- Runway correction for converting true to magnetic heading. -- @type ATIS.RunwayM2T --- @field #number Caucasus 0° (East). --- @field #number Nevada +12° (East). --- @field #number Normandy -10° (West). --- @field #number PersianGulf +2° (East). +-- @field #number Caucasus 0° (East). +-- @field #number Nevada +12° (East). +-- @field #number Normandy -10° (West). +-- @field #number PersianGulf +2° (East). ATIS.RunwayM2T={ Caucasus=0, Nevada=12, @@ -709,9 +709,9 @@ function ATIS:SetMapMarks(switch) return self end ---- Set magnetic runway headings as depicted on the runway, *e.g.* "13" for 130° or "25L" for the left runway with magnetic heading 250°. +--- Set magnetic runway headings as depicted on the runway, *e.g.* "13" for 130° or "25L" for the left runway with magnetic heading 250�. -- @param #ATIS self --- @param #table headings Magnetic headings. Inverse (-180°) headings are added automatically. You only need to specify one heading per runway direction. "L"eft and "R" right can also be appended. +-- @param #table headings Magnetic headings. Inverse (-180�) headings are added automatically. You only need to specify one heading per runway direction. "L"eft and "R" right can also be appended. -- @return #ATIS self function ATIS:SetRunwayHeadingsMagnetic(headings) @@ -826,12 +826,12 @@ end -- -- To get *true* from *magnetic* heading one has to add easterly or substract westerly variation, e.g -- --- A magnetic heading of 180° corresponds to a true heading of +-- A magnetic heading of 180° corresponds to a true heading of -- --- * 186° on the Caucaus map --- * 192° on the Nevada map --- * 170° on the Normany map --- * 182° on the Persian Gulf map +-- * 186° on the Caucaus map +-- * 192° on the Nevada map +-- * 170° on the Normany map +-- * 182° on the Persian Gulf map -- -- Likewise, to convert *magnetic* into *true* heading, one has to substract easterly and add westerly variation. -- @@ -1085,7 +1085,7 @@ function ATIS:onafterBroadcast(From, Event, To) local g= 9.80665 --[m/s^2] local M= 0.0289644 --[kg/mol] local T0=coord:GetTemperature(0)+273.15 --[K] Temp at sea level. - local TS=288.15 -- Standard Temperature assumed by Altimeter is 15°C + local TS=288.15 -- Standard Temperature assumed by Altimeter is 15°C local q=qnh*100 -- Calculate Pressure. @@ -1197,10 +1197,10 @@ function ATIS:onafterBroadcast(From, Event, To) --- Temperature --- ------------------- - -- Temperature in °C (or °F). + -- Temperature in °C (or °F). local temperature=coord:GetTemperature(height+5) - -- Convert to °F. + -- Convert to °F. if self.TDegF then temperature=UTILS.CelciusToFarenheit(temperature) end @@ -1796,7 +1796,7 @@ end --- Get runway from user supplied magnetic heading. -- @param #ATIS self -- @param #number windfrom Wind direction (from) in degrees. --- @return #string Runway magnetic heading divided by ten (and rounded). Eg, "13" for 130°. +-- @return #string Runway magnetic heading divided by ten (and rounded). Eg, "13" for 130�. function ATIS:GetMagneticRunway(windfrom) local diffmin=nil @@ -1839,7 +1839,7 @@ function ATIS:GetNavPoint(navpoints, runway, left) local navL=self:GetRunwayLR(nav.runway) local hdgD=UTILS.HdgDiff(navy,rwyy) - if hdgD<=15 then --We allow an error of +-15° here. + if hdgD<=15 then --We allow an error of +-15° here. if navL==nil or (navL==true and left==true) or (navL==false and left==false) then return nav end From 042cc7a85f18e8b96e65dc884165bcab7758f5ac Mon Sep 17 00:00:00 2001 From: 132nd-Entropy Date: Thu, 20 Feb 2020 15:04:00 +0100 Subject: [PATCH 439/485] Update ATIS.lua --- Moose Development/Moose/Ops/ATIS.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index f5c1199a6..dfd9fbebe 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -709,9 +709,9 @@ function ATIS:SetMapMarks(switch) return self end ---- Set magnetic runway headings as depicted on the runway, *e.g.* "13" for 130° or "25L" for the left runway with magnetic heading 250�. +--- Set magnetic runway headings as depicted on the runway, *e.g.* "13" for 130° or "25L" for the left runway with magnetic heading 250°. -- @param #ATIS self --- @param #table headings Magnetic headings. Inverse (-180�) headings are added automatically. You only need to specify one heading per runway direction. "L"eft and "R" right can also be appended. +-- @param #table headings Magnetic headings. Inverse (-180°) headings are added automatically. You only need to specify one heading per runway direction. "L"eft and "R" right can also be appended. -- @return #ATIS self function ATIS:SetRunwayHeadingsMagnetic(headings) @@ -1796,7 +1796,7 @@ end --- Get runway from user supplied magnetic heading. -- @param #ATIS self -- @param #number windfrom Wind direction (from) in degrees. --- @return #string Runway magnetic heading divided by ten (and rounded). Eg, "13" for 130�. +-- @return #string Runway magnetic heading divided by ten (and rounded). Eg, "13" for 130°. function ATIS:GetMagneticRunway(windfrom) local diffmin=nil From fedee49eb112869b17805752ecd065cedc606b79 Mon Sep 17 00:00:00 2001 From: Pikes Date: Sat, 22 Feb 2020 16:35:18 +0000 Subject: [PATCH 440/485] Update Airbase.lua https://github.com/FlightControl-Master/MOOSE/issues/1279 I dont know if anything else needs to be done apart from this. --- Moose Development/Moose/Wrapper/Airbase.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 14c93a7a9..541412c3b 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -217,7 +217,14 @@ AIRBASE.Normandy = { ["Funtington"] = "Funtington", ["Tangmere"] = "Tangmere", ["Ford"] = "Ford", - } + ["Goulet"] = "Goulet", + ["Argentan"] = "Argentan", + ["Vrigny"] = "Vrigny", + ["Essay"] = "Essay", + ["Hauterive"] = "Hauterive", + ["Barville"] = "Barville", + ["Conches"] = "Conches", +} --- These are all airbases of the Persion Gulf Map: -- @@ -329,7 +336,7 @@ AIRBASE.TerminalType = { --- Runway data. -- @type AIRBASE.Runway -- @field #number heading Heading of the runway in degrees. --- @field #string idx Runway ID: heading 070° ==> idx="07". +-- @field #string idx Runway ID: heading 070° ==> idx="07". -- @field #number length Length of runway in meters. -- @field Core.Point#COORDINATE position Position of runway start. -- @field Core.Point#COORDINATE endpoint End point of runway. @@ -1107,7 +1114,7 @@ function AIRBASE:GetRunwayData(magvar, mark) -- Heading of runway. local hdg=c1:HeadingTo(c2) - -- Runway ID: heading=070° ==> idx="07" + -- Runway ID: heading=070° ==> idx="07" local idx=string.format("%02d", UTILS.Round((hdg-magvar)/10, 0)) -- Runway table. @@ -1217,7 +1224,7 @@ function AIRBASE:GetActiveRunway(magvar) local dot=UTILS.VecDot(Vwind, Vrunway) -- Debug. - --env.info(string.format("runway=%03d° dot=%.3f", runway.heading, dot)) + --env.info(string.format("runway=%03d° dot=%.3f", runway.heading, dot)) -- New min? if dotmin==nil or dot Date: Sat, 22 Feb 2020 22:17:34 +0000 Subject: [PATCH 441/485] Normandy added 6 airbases with coords added the coords added the list of airbases at the top --- .../Moose/Functional/ATC_Ground.lua | 89 ++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index 9ded69874..1c7cd5e65 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -1619,6 +1619,13 @@ end -- * `AIRBASE.Normandy.Sainte_Laurent_sur_Mer` -- * `AIRBASE.Normandy.Sommervieu` -- * `AIRBASE.Normandy.Tangmere` +-- * `AIRBASE.Normandy.Argentan` +-- * `AIRBASE.Normandy.Goulet` +-- * `AIRBASE.Normandy.Essay` +-- * `AIRBASE.Normandy.Hauterive` +-- * `AIRBASE.Normandy.Barville` +-- * `AIRBASE.Normandy.Conches` +-- * `AIRBASE.Normandy.Vrigny` -- -- # Installation -- @@ -1832,7 +1839,7 @@ ATC_GROUND_NORMANDY = { }, }, }, - [AIRBASE.Normandy.Ford] = { + [AIRBASE.Normandy.Ford_AF] = { PointsRunways = { [1] = { [1]={["y"]=-26506.13971428,["x"]=147514.39971429,}, @@ -2034,7 +2041,83 @@ ATC_GROUND_NORMANDY = { }, }, }, - }, + [AIRBASE.Normandy.Argentan] = { + PointsRunways = { + [1] = { + [1]={["y"]=22322.280338032,["x"]=-78607.309765269,}, + [2]={["y"]=23032.778713963,["x"]=-78967.17709893,}, + [3]={["y"]=23015.27074041,["x"]=-79008.02903722,}, + [4]={["y"]=22299.944963827,["x"]=-78650.366148928,}, + }, + }, + }, + [AIRBASE.Normandy.Goulet] = { + PointsRunways = { + [1] = { + [1]={["y"]=24901.788373185,["x"]=-89139.367511763,}, + [2]={["y"]=25459.965967043,["x"]=-89709.67940114,}, + [3]={["y"]=25422.459962713,["x"]=-89741.669816598,}, + [4]={["y"]=24857.663662208,["x"]=-89173.56416277,}, + }, + }, + }, + [AIRBASE.Normandy.Essay] = { + PointsRunways = { + [1] = { + [1]={["y"]=44610.072022849,["x"]=-105469.21149064,}, + [2]={["y"]=45417.939023956,["x"]=-105536.08535277,}, + [3]={["y"]=45412.558368383,["x"]=-105585.27991801,}, + [4]={["y"]=44602.38537203,["x"]=-105516.10006064,}, + }, + }, + }, + [AIRBASE.Normandy.Hauterive] = { + PointsRunways = { + [1] = { + [1]={["y"]=40617.185360953,["x"]=-107657.10147517,}, + [2]={["y"]=41114.628372034,["x"]=-108298.77015609,}, + [3]={["y"]=41080.006684855,["x"]=-108319.06562788,}, + [4]={["y"]=40584.558402807,["x"]=-107692.29370481,}, + }, + }, + }, + [AIRBASE.Normandy.Vrigny] = { + PointsRunways = { + [1] = { + [1]={["y"]=24892.131051827,["x"]=-89131.628297486,}, + [2]={["y"]=25469.738000575,["x"]=-89709.235246234,}, + [3]={["y"]=25418.869206793,["x"]=-89738.771965204,}, + [4]={["y"]=24859.312475193,["x"]=-89171.010589446,}, + }, + }, + }, + [AIRBASE.Normandy.Barville] = { + PointsRunways = { + [1] = { + [1]={["y"]=49027.850333166,["x"]=-109217.05049066,}, + [2]={["y"]=49755.022185805,["x"]=-110346.63783457,}, + [3]={["y"]=49682.657996586,["x"]=-110401.35222154,}, + [4]={["y"]=48921.951519675,["x"]=-109285.88471943,}, + }, + [2] = { + [1]={["y"]=48429.522036941,["x"]=-109818.90874734,}, + [2]={["y"]=49746.197284681,["x"]=-109954.81222465,}, + [3]={["y"]=49735.607403332,["x"]=-110032.47135455,}, + [4]={["y"]=48420.697135816,["x"]=-109900.09783768,}, + }, + }, + }, + [AIRBASE.Normandy.Conches] = { + PointsRunways = { + [1] = { + [1]={["y"]=95099.187473266,["x"]=-56389.619005858,}, + [2]={["y"]=95181.545025963,["x"]=-56465.440244849,}, + [3]={["y"]=94071.678958666,["x"]=-57627.596821795,}, + [4]={["y"]=94005.008558864,["x"]=-57558.31189651,}, + }, + }, + }, + }, } @@ -3181,4 +3264,4 @@ end - \ No newline at end of file + From 63a5d2e3ac0fa54bfedc8c0d67a9c4f4f36e83c7 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 25 Feb 2020 21:35:00 +0100 Subject: [PATCH 442/485] Fixes & Improvements --- Moose Development/Moose/Core/RadioQueue.lua | 12 +++++++-- Moose Development/Moose/Ops/ATIS.lua | 4 ++- Moose Development/Moose/Utilities/Utils.lua | 7 ++++-- Moose Development/Moose/Wrapper/Airbase.lua | 28 +++++++++++++++++---- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Core/RadioQueue.lua b/Moose Development/Moose/Core/RadioQueue.lua index 7ee3c02ae..756b5a967 100644 --- a/Moose Development/Moose/Core/RadioQueue.lua +++ b/Moose Development/Moose/Core/RadioQueue.lua @@ -351,13 +351,21 @@ function RADIOQUEUE:Broadcast(transmission) self.senderinit=true end + -- Set subtitle only if duration>0 sec. + local subtitle=nil + local duration=nil + if transmission.subtitle and transmission.subduration and transmission.subduration>0 then + subtitle=transmission.subtitle + duration=transmission.subduration + end + -- Command to tranmit the call. local commandTransmit={ id = "TransmitMessage", params = { file=filename, - duration=transmission.subduration, - subtitle=transmission.subtitle or "", + duration=duration, + subtitle=subtitle, loop=false, }} diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index dfd9fbebe..7dbb616dd 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -130,6 +130,7 @@ -- **Note** that you should use a different relay unit for each ATIS! -- -- By default, subtitles are displayed for 10 seconds. This can be changed using @{#ATIS.SetSubtitleDuration}(*duration*) with *duration* being the duration in seconds. +-- Setting a *duration* of 0 will completely disable all subtitles. -- -- ## Active Runway -- @@ -523,7 +524,7 @@ _ATIS={} --- ATIS class version. -- @field #string version -ATIS.version="0.6.3" +ATIS.version="0.6.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1537,6 +1538,7 @@ function ATIS:onafterBroadcast(From, Event, To) -- Runway length. if self.rwylength then + local runact=self.airbase:GetActiveRunway(self.runwaym2t) local length=runact.length if not self.metric then length=UTILS.MetersToFeet(length) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 7346dcc38..d31ae647a 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -474,8 +474,11 @@ UTILS.tostringMGRS = function(MGRS, acc) --R2.1 if acc == 0 then return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph else - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', UTILS.Round(MGRS.Easting/(10^(5-acc)), 0)) - .. ' ' .. string.format('%0' .. acc .. 'd', UTILS.Round(MGRS.Northing/(10^(5-acc)), 0)) + --return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', UTILS.Round(MGRS.Easting/(10^(5-acc)), 0)) + -- .. ' ' .. string.format('%0' .. acc .. 'd', UTILS.Round(MGRS.Northing/(10^(5-acc)), 0)) + + -- Truncate rather than round MGRS grid! + return string.format("%s %s %s %s", MGRS.UTMZone, MGRS.MGRSDigraph, string.sub(tostring(MGRS.Easting), 1, acc), string.sub(tostring(MGRS.Northing), 1, acc)) end end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 541412c3b..ccfbc5a59 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -352,7 +352,13 @@ function AIRBASE:Register( AirbaseName ) local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) --#AIRBASE self.AirbaseName = AirbaseName self.AirbaseID = self:GetID(true) - self.AirbaseZone = ZONE_RADIUS:New( AirbaseName, self:GetVec2(), 2500 ) + local vec2=self:GetVec2() + if vec2 then + self.AirbaseZone = ZONE_RADIUS:New( AirbaseName, vec2, 2500 ) + else + self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName)) + end + return self end @@ -450,7 +456,7 @@ function AIRBASE:GetID(unique) else - for DCSAirbaseId, DCSAirbase in pairs(world.getAirbases()) do + for DCSAirbaseId, DCSAirbase in ipairs(world.getAirbases()) do -- Get the airbase name. local AirbaseName = DCSAirbase:getName() @@ -458,8 +464,12 @@ function AIRBASE:GetID(unique) -- This gives the incorrect value to be inserted into the airdromeID for DCS 2.5.6! local airbaseID=tonumber(DCSAirbase:getID()) + local airbaseCategory=self:GetAirbaseCategory() + + --env.info(string.format("FF airbase=%s id=%s category=%s", tostring(AirbaseName), tostring(airbaseID), tostring(airbaseCategory))) + -- No way AFIK to get the DCS version. So we check if the event exists. That should tell us if we are on DCS 2.5.6 or prior to that. - if world.event.S_EVENT_KILL and world.event.S_EVENT_KILL>0 and self:GetAirbaseCategory()==Airbase.Category.AIRDROME then + if world.event.S_EVENT_KILL and world.event.S_EVENT_KILL>0 and airbaseCategory==Airbase.Category.AIRDROME then -- We have to take the key value of this loop! airbaseID=DCSAirbaseId @@ -471,7 +481,7 @@ function AIRBASE:GetID(unique) end if AirbaseName==self.AirbaseName then - if self:GetAirbaseCategory()==Airbase.Category.SHIP then + if airbaseCategory==Airbase.Category.SHIP then -- Ships get a negative sign as their unit number might be the same as the ID of another airbase. return unique and -airbaseID or airbaseID else @@ -1011,7 +1021,15 @@ end -- @param #AIRBASE self -- @return #number Category of airbase from GetDesc().category. function AIRBASE:GetAirbaseCategory() - return self:GetDesc().category + local desc=self:GetDesc() + local category=Airbase.Category.AIRDROME + + if desc and desc.category then + category=desc.category + else + self:E(string.format("ERROR: Cannot get category of airbase %s due to DCS 2.5.6 bug! Assuming it is an AIRDROME for now...", tostring(self.AirbaseName))) + end + return category end From f258e9263ef2209ff7c1221592a0cc99881961a1 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 26 Feb 2020 20:44:35 +0100 Subject: [PATCH 443/485] MGRS --- Moose Development/Moose/Core/Point.lua | 2 +- Moose Development/Moose/Core/Spawn.lua | 4 ++-- Moose Development/Moose/Functional/RAT.lua | 8 +++---- .../Moose/Functional/Warehouse.lua | 4 ++-- Moose Development/Moose/Utilities/Utils.lua | 24 +++++++++++++++---- Moose Development/Moose/Wrapper/Airbase.lua | 2 +- Moose Development/Moose/Wrapper/Group.lua | 2 +- 7 files changed, 31 insertions(+), 15 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 8ab8e2ae5..6358341ef 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1095,7 +1095,7 @@ do -- COORDINATE -- Airbase parameters for takeoff and landing points. if airbase then local AirbaseID = airbase:GetID() - local AirbaseCategory = airbase:GetDesc().category + local AirbaseCategory = airbase:GetAirbaseCategory() if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then RoutePoint.linkUnit = AirbaseID RoutePoint.helipadId = AirbaseID diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index eecaca873..3634000fb 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1545,7 +1545,7 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT -- Get airbase ID and category. local AirbaseID = SpawnAirbase:GetID() - local AirbaseCategory = SpawnAirbase:GetDesc().category + local AirbaseCategory = SpawnAirbase:GetAirbaseCategory() self:F( { AirbaseCategory = AirbaseCategory } ) -- Set airdromeId. @@ -1983,7 +1983,7 @@ function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex -- Get airbase ID and category. local AirbaseID = SpawnAirbase:GetID() - local AirbaseCategory = SpawnAirbase:GetDesc().category + local AirbaseCategory = SpawnAirbase:GetAirbaseCategory() self:F( { AirbaseCategory = AirbaseCategory } ) -- Set airdromeId. diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 7492a5d4d..327e2e518 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -3416,7 +3416,7 @@ function RAT:_GetAirportsOfCoalition() for _,coalition in pairs(self.ctable) do for _,_airport in pairs(self.airports_map) do local airport=_airport --Wrapper.Airbase#AIRBASE - local category=airport:GetDesc().category + local category=airport:GetAirbaseCategory() if airport:GetCoalition()==coalition then -- Planes cannot land on FARPs. --local condition1=self.category==RAT.cat.plane and airport:GetTypeName()=="FARP" @@ -3847,7 +3847,7 @@ function RAT:_OnBirth(EventData) -- Check if any unit of the group was spawned on top of another unit in the MOOSE data base. local ontop=false - if self.checkontop and (_airbase and _airbase:GetDesc().category==Airbase.Category.AIRDROME) then + if self.checkontop and (_airbase and _airbase:GetAirbaseCategory()==Airbase.Category.AIRDROME) then ontop=self:_CheckOnTop(SpawnGroup, self.ontopradius) end @@ -4457,7 +4457,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport if (Airport~=nil) and (Type~=RAT.wp.air) then local AirbaseID = Airport:GetID() - local AirbaseCategory = Airport:GetDesc().category + local AirbaseCategory = Airport:GetAirbaseCategory() if AirbaseCategory == Airbase.Category.SHIP then RoutePoint.linkUnit = AirbaseID RoutePoint.helipadId = AirbaseID @@ -5141,7 +5141,7 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take local spawnonrunway=false local spawnonairport=false if spawnonground then - local AirbaseCategory = departure:GetDesc().category + local AirbaseCategory = departure:GetAirbaseCategory() if AirbaseCategory == Airbase.Category.SHIP then spawnonship=true elseif AirbaseCategory == Airbase.Category.HELIPAD then diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 284a95cea..6c359763c 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -282,7 +282,7 @@ -- -- Assets of the warehouse can be requested by other MOOSE warehouses. A request will first be scrutinized to check if can be fulfilled at all. If the request is valid, it is -- put into the warehouse queue and processed as soon as possible. - +-- -- Requested assets spawn in various "Rule of Engagement Rules" (ROE) and Alerts modes. If your assets will cross into dangerous areas, be sure to change these states. You can do this in @{#WAREHOUSE:OnAfterAssetSpawned}(*From, *Event, *To, *group, *asset, *request)) function. -- -- Initial Spawn states is as follows: @@ -3011,7 +3011,7 @@ end function WAREHOUSE:GetAirbaseCategory() local category=-1 if self.airbase then - category=self.airbase:GetDesc().category + category=self.airbase:GetAirbaseCategory() end return category end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index d31ae647a..15ae1ac1b 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -471,15 +471,31 @@ end -- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. UTILS.tostringMGRS = function(MGRS, acc) --R2.1 + if acc == 0 then return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph else - --return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', UTILS.Round(MGRS.Easting/(10^(5-acc)), 0)) - -- .. ' ' .. string.format('%0' .. acc .. 'd', UTILS.Round(MGRS.Northing/(10^(5-acc)), 0)) + + -- Test if Easting/Northing have less than 4 digits. + --MGRS.Easting=123 -- should be 00123 + --MGRS.Northing=5432 -- should be 05432 - -- Truncate rather than round MGRS grid! - return string.format("%s %s %s %s", MGRS.UTMZone, MGRS.MGRSDigraph, string.sub(tostring(MGRS.Easting), 1, acc), string.sub(tostring(MGRS.Northing), 1, acc)) + -- Truncate rather than round MGRS grid! + local Easting=tostring(MGRS.Easting) + local Northing=tostring(MGRS.Northing) + + -- Count number of missing digits. Easting/Northing should have 5 digits. However, it is passed as a number. Therefore, any leading zeros would not be displayed by lua. + local nE=5-string.len(Easting) + local nN=5-string.len(Northing) + + -- Get leading zeros (if any). + for i=1,nE do Easting="0"..Easting end + for i=1,nN do Northing="0"..Northing end + + -- Return MGRS string. + return string.format("%s %s %s %s", MGRS.UTMZone, MGRS.MGRSDigraph, string.sub(Easting, 1, acc), string.sub(Northing, 1, acc)) end + end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index ccfbc5a59..0c538cd4f 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -952,7 +952,7 @@ function AIRBASE:CheckOnRunWay(group, radius, despawn) radius=radius or 50 -- We only check at real airbases (not FARPS or ships). - if self:GetDesc().category~=Airbase.Category.AIRDROME then + if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME then return false end diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 82616e283..3fc5e672a 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1922,7 +1922,7 @@ function GROUP:RespawnAtCurrentAirbase(SpawnTemplate, Takeoff, Uncontrolled) -- -- Aibase id and category. local AirbaseID = airbase:GetID() - local AirbaseCategory = airbase:GetDesc().category + local AirbaseCategory = airbase:GetAirbaseCategory() if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then SpawnPoint.linkUnit = AirbaseID From 4ef827836c70ce1f56762f1f0159774dbd1fc657 Mon Sep 17 00:00:00 2001 From: Michael Barnes Date: Thu, 5 Mar 2020 13:05:11 +1000 Subject: [PATCH 444/485] Added SPAWN:InitGroupHeading function, to allow orientation of the overall group formation when spawned (handy for static formations like SAM batteries, etc.) --- Moose Development/Moose/Core/Spawn.lua | 102 +++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 3634000fb..a2f03ca50 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -16,7 +16,7 @@ -- * Spawn late activated. -- * Spawn with or without an initial delay. -- * Respawn after landing, on the runway or at the ramp after engine shutdown. --- * Spawn with custom heading. +-- * Spawn with custom heading, both for a group formation and for the units in the group. -- * Spawn with different skills. -- * Spawn with different liveries. -- * Spawn with an inner and outer radius to set the initial position. @@ -550,6 +550,41 @@ function SPAWN:InitHeading( HeadingMin, HeadingMax ) end +--- Defines the heading of the overall formation of the new spawned group. +-- The heading can be given as one fixed degree, or can be randomized between minimum and maximum degrees. +-- The Group's formation as laid out in its template will be rotated around the first unit in the group +-- Group individual units facings will rotate to match. If InitHeading is also applied to this SPAWN then that will take precedence for individual unit facings. +-- Note that InitGroupHeading does *not* rotate the groups route; only its initial facing! +-- @param #SPAWN self +-- @param #number HeadingMin The minimum or fixed heading in degrees. +-- @param #number HeadingMax (optional) The maximum heading in degrees. This there is no maximum heading, then the heading for the group will be HeadingMin. +-- @param #number unitVar (optional) Individual units within the group will have their heading randomized by +/- unitVar degrees. Default is zero. +-- @return #SPAWN self +-- @usage +-- +-- mySpawner = SPAWN:New( ... ) +-- +-- -- Spawn the Group with the formation rotated +100 degrees around unit #1, compared to the mission template. +-- mySpawner:InitGroupHeading( 100 ) +-- +-- Spawn the Group with the formation rotated units between +100 and +150 degrees around unit #1, compared to the mission template, and with individual units varying by +/- 10 degrees from their templated facing. +-- mySpawner:InitGroupHeading( 100, 150, 10 ) +-- +-- Spawn the Group with the formation rotated -60 degrees around unit #1, compared to the mission template, but with all units facing due north regardless of how they were laid out in the template. +-- mySpawner:InitGroupHeading(-60):InitHeading(0) +-- or +-- mySpawner:InitHeading(0):InitGroupHeading(-60) +-- +function SPAWN:InitGroupHeading( HeadingMin, HeadingMax, unitVar ) + self:F({HeadingMin=HeadingMin, HeadingMax=HeadingMax, unitVar=unitVar}) + + self.SpawnInitGroupHeadingMin = HeadingMin + self.SpawnInitGroupHeadingMax = HeadingMax + self.SpawnInitGroupUnitVar = unitVar + return self +end + + --- Sets the coalition of the spawned group. Note that it might be necessary to also set the country explicitly! -- @param #SPAWN self -- @param DCS#coalition.side Coalition Coalition of the group as number of enumerator: @@ -1239,21 +1274,72 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) end end - -- Get correct heading. - local function _Heading(course) + -- Get correct heading in Radians. + local function _Heading(courseDeg) local h - if course<=180 then - h=math.rad(course) + if courseDeg<=180 then + h=math.rad(courseDeg) else - h=-math.rad(360-course) + h=-math.rad(360-courseDeg) end return h end + + local Rad180 = math.rad(180) + local function _HeadingRad(courseRad) + if courseRad<=Rad180 then + return courseRad + else + return -((2*Rad180)-courseRad) + end + end + + -- Generate a random value somewhere between two floating point values. + local function _RandomInRange ( min, max ) + if min and max then + return min + ( math.random()*(max-min) ) + else + return min + end + end + + -- Apply InitGroupHeading rotation if requested. + -- We do this before InitHeading unit rotation so that can take precedence + -- NOTE: Does *not* rotate the groups route; only its initial facing. + if self.SpawnInitGroupHeadingMin and #SpawnTemplate.units > 0 then + + local pivotX = SpawnTemplate.units[1].x -- unit #1 is the pivot point + local pivotY = SpawnTemplate.units[1].y + + local headingRad = math.rad(_RandomInRange(self.SpawnInitGroupHeadingMin or 0,self.SpawnInitGroupHeadingMax)) + local cosHeading = math.cos(headingRad) + local sinHeading = math.sin(headingRad) + + local unitVarRad = math.rad(self.SpawnInitGroupUnitVar or 0) + + for UnitID = 1, #SpawnTemplate.units do + + if UnitID > 1 then -- don't rotate position of unit #1 + local unitXOff = SpawnTemplate.units[UnitID].x - pivotX -- rotate position offset around unit #1 + local unitYOff = SpawnTemplate.units[UnitID].y - pivotY + + SpawnTemplate.units[UnitID].x = pivotX + (unitXOff*cosHeading) - (unitYOff*sinHeading) + SpawnTemplate.units[UnitID].y = pivotY + (unitYOff*cosHeading) + (unitXOff*sinHeading) + end + + -- adjust heading of all units, including unit #1 + local unitHeading = SpawnTemplate.units[UnitID].heading + headingRad -- add group rotation to units default rotation + SpawnTemplate.units[UnitID].heading = _HeadingRad(_RandomInRange(unitHeading-unitVarRad, unitHeading+unitVarRad)) + SpawnTemplate.units[UnitID].psi = -SpawnTemplate.units[UnitID].heading + + end + + end - -- If Heading is given, point all the units towards the given Heading. + -- If Heading is given, point all the units towards the given Heading. Overrides any heading set in InitGroupHeading above. if self.SpawnInitHeadingMin then for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].heading = _Heading(self.SpawnInitHeadingMax and math.random( self.SpawnInitHeadingMin, self.SpawnInitHeadingMax ) or self.SpawnInitHeadingMin) + SpawnTemplate.units[UnitID].heading = _Heading(_RandomInRange(self.SpawnInitHeadingMin, self.SpawnInitHeadingMax)) SpawnTemplate.units[UnitID].psi = -SpawnTemplate.units[UnitID].heading end end From 7cf73af7079d17e4fd1a4f0eeefe1c14b084bfee Mon Sep 17 00:00:00 2001 From: Michael Barnes Date: Thu, 5 Mar 2020 13:05:25 +1000 Subject: [PATCH 445/485] Added some protection against unexpectedly-nil values. --- Moose Development/Moose/Wrapper/Group.lua | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 3fc5e672a..484dc160b 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -316,9 +316,12 @@ function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3 local DCSPositionable = self:GetDCSObject() if DCSPositionable then - local PositionablePosition = DCSPositionable:getUnits()[1]:getPosition().p + local unit = DCSPositionable:getUnits()[1] + if unit then + local PositionablePosition = unit:getPosition().p self:T3( PositionablePosition ) return PositionablePosition + end end return nil @@ -366,9 +369,11 @@ function GROUP:IsActive() local DCSGroup = self:GetDCSObject() -- DCS#Group if DCSGroup then - - local GroupIsActive = DCSGroup:getUnit(1):isActive() + local unit = DCSGroup:getUnit(1) + if unit then + local GroupIsActive = unit:isActive() return GroupIsActive + end end return nil From 71b5140e69761cd2b7e6cf0fe3d7c828b2a2e081 Mon Sep 17 00:00:00 2001 From: Thor Vik Date: Wed, 11 Mar 2020 10:11:59 +0100 Subject: [PATCH 446/485] Fix for Issue #1286, partly tested. --- Moose Development/Moose/Core/Spawn.lua | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index a2f03ca50..24027b2b7 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -326,6 +326,7 @@ function SPAWN:New( SpawnTemplatePrefix ) self.SpawnInitRadio = nil -- No radio comms setting. self.SpawnInitModex = nil self.SpawnInitAirbase = nil + self.TweakedTemplate = false -- Check if the user is using self made template. self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -379,7 +380,8 @@ function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) self.SpawnInitRadio = nil -- No radio comms setting. self.SpawnInitModex = nil self.SpawnInitAirbase = nil - + self.TweakedTemplate = false -- Check if the user is using self made template. + 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 .. "'" ) @@ -435,6 +437,7 @@ function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPr self.SpawnInitRadio = nil -- No radio comms setting. self.SpawnInitModex = nil self.SpawnInitAirbase = nil + self.TweakedTemplate = true -- Check if the user is using self made template. self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -2977,9 +2980,14 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 -- self.SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) -- end - local SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) - --local SpawnTemplate = self.SpawnTemplate - SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) +local SpawnTemplate +if self.TweakedTemplate ~= nil and self.TweakedTemplate == true then + BASE:I("WARNING: You are using a tweaked template.") + SpawnTemplate = self.SpawnTemplate +else + SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) +end + SpawnTemplate.groupId = nil --SpawnTemplate.lateActivation = false From a704693abfdf1b1e8c84fd3aa74ac87347741580 Mon Sep 17 00:00:00 2001 From: Thor Vik Date: Wed, 11 Mar 2020 15:28:36 +0100 Subject: [PATCH 447/485] Made parameter SpawnAliasPrefix mandatory for SpawnFromTemplate function. --- Moose Development/Moose/Core/Spawn.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 24027b2b7..0776d3362 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -409,7 +409,11 @@ end function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix ) local self = BASE:Inherit( self, BASE:New() ) self:F( { SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix } ) - + if SpawnAliasPrefix == nil or SpawnAliasPrefix == "" then + BASE:I("ERROR: in function NewFromTemplate, required paramter SpawnAliasPrefix is not set") + return nil + end + if SpawnTemplate then self.SpawnTemplate = SpawnTemplate -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! self.SpawnTemplatePrefix = SpawnTemplatePrefix From 36666df76fb59802f2967d4657b9086b1af83c34 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 14 Mar 2020 19:19:55 +0100 Subject: [PATCH 448/485] Temp --- Moose Development/Moose/Core/Database.lua | 4 +-- Moose Development/Moose/Core/Spawn.lua | 30 ++++++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index f691fb143..a882aae66 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -187,7 +187,7 @@ end function DATABASE:AddUnit( DCSUnitName ) if not self.UNITS[DCSUnitName] then - self:T( { "Add UNIT:", DCSUnitName } ) + self:I( { "Add UNIT:", DCSUnitName } ) local UnitRegister = UNIT:Register( DCSUnitName ) self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) @@ -512,7 +512,7 @@ end function DATABASE:AddGroup( GroupName ) if not self.GROUPS[GroupName] then - self:T( { "Add GROUP:", GroupName } ) + self:I( { "Add GROUP:", GroupName } ) self.GROUPS[GroupName] = GROUP:Register( GroupName ) end diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 0776d3362..ed6720066 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1246,13 +1246,18 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) if self:_GetSpawnIndex( SpawnIndex ) then if self.SpawnGroups[self.SpawnIndex].Visible then + env.info("FF visible ==> activate") self.SpawnGroups[self.SpawnIndex].Group:Activate() else + + env.info("FF get template") local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate self:T( SpawnTemplate.name ) if SpawnTemplate then + + env.info("FF found template") local PointVec3 = POINT_VEC3:New( SpawnTemplate.route.points[1].x, SpawnTemplate.route.points[1].alt, SpawnTemplate.route.points[1].y ) self:T( { "Current point of ", self.SpawnTemplatePrefix, PointVec3 } ) @@ -1423,6 +1428,14 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) SpawnGroup:SetAIOnOff( self.AIOnOff ) end + + env.info("FF Returning spawn group") + if SpawnGroup then + + else + + end + self:T3( SpawnTemplate.name ) @@ -1435,12 +1448,16 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) --if self.Repeat then -- _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) --end + + --else + -- self:E("ERROR: No spawn template.") end + self.SpawnGroups[self.SpawnIndex].Spawned = true return self.SpawnGroups[self.SpawnIndex].Group else - --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) + self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) end return nil @@ -1941,8 +1958,19 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT SpawnTemplate.uncontrolled = self.SpawnUnControlled + env.info("FF spawning group with index "..self.SpawnIndex) + -- Spawn group. local GroupSpawned = self:SpawnWithIndex( self.SpawnIndex ) + + if GroupSpawned then + env.info("ERROR: spawn group is there!") + GroupSpawned:SmokeRed() + local n=#GroupSpawned:GetUnits() + env.info("FF n="..tostring(n)) + else + env.info("ERROR: spawn group not there!") + end -- When spawned in the air, we need to generate a Takeoff Event. if Takeoff == GROUP.Takeoff.Air then From ceaae5421c923346c2c276e290ee8099e2a58429 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 14 Mar 2020 20:28:32 +0100 Subject: [PATCH 449/485] SPAWN corrected spawn name --- Moose Development/Moose/Core/Database.lua | 4 +-- Moose Development/Moose/Core/Spawn.lua | 41 +++++------------------ 2 files changed, 11 insertions(+), 34 deletions(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index a882aae66..f691fb143 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -187,7 +187,7 @@ end function DATABASE:AddUnit( DCSUnitName ) if not self.UNITS[DCSUnitName] then - self:I( { "Add UNIT:", DCSUnitName } ) + self:T( { "Add UNIT:", DCSUnitName } ) local UnitRegister = UNIT:Register( DCSUnitName ) self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) @@ -512,7 +512,7 @@ end function DATABASE:AddGroup( GroupName ) if not self.GROUPS[GroupName] then - self:I( { "Add GROUP:", GroupName } ) + self:T( { "Add GROUP:", GroupName } ) self.GROUPS[GroupName] = GROUP:Register( GroupName ) end diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index ed6720066..02e2b3c60 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1246,18 +1246,13 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) if self:_GetSpawnIndex( SpawnIndex ) then if self.SpawnGroups[self.SpawnIndex].Visible then - env.info("FF visible ==> activate") self.SpawnGroups[self.SpawnIndex].Group:Activate() else - - env.info("FF get template") local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate self:T( SpawnTemplate.name ) if SpawnTemplate then - - env.info("FF found template") local PointVec3 = POINT_VEC3:New( SpawnTemplate.route.points[1].x, SpawnTemplate.route.points[1].alt, SpawnTemplate.route.points[1].y ) self:T( { "Current point of ", self.SpawnTemplatePrefix, PointVec3 } ) @@ -1428,14 +1423,6 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) SpawnGroup:SetAIOnOff( self.AIOnOff ) end - - env.info("FF Returning spawn group") - if SpawnGroup then - - else - - end - self:T3( SpawnTemplate.name ) @@ -1457,7 +1444,7 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) self.SpawnGroups[self.SpawnIndex].Spawned = true return self.SpawnGroups[self.SpawnIndex].Group else - self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) + --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) end return nil @@ -1958,19 +1945,8 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT SpawnTemplate.uncontrolled = self.SpawnUnControlled - env.info("FF spawning group with index "..self.SpawnIndex) - -- Spawn group. local GroupSpawned = self:SpawnWithIndex( self.SpawnIndex ) - - if GroupSpawned then - env.info("ERROR: spawn group is there!") - GroupSpawned:SmokeRed() - local n=#GroupSpawned:GetUnits() - env.info("FF n="..tostring(n)) - else - env.info("ERROR: spawn group not there!") - end -- When spawned in the air, we need to generate a Takeoff Event. if Takeoff == GROUP.Takeoff.Air then @@ -3012,13 +2988,14 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 -- self.SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) -- end -local SpawnTemplate -if self.TweakedTemplate ~= nil and self.TweakedTemplate == true then - BASE:I("WARNING: You are using a tweaked template.") - SpawnTemplate = self.SpawnTemplate -else - SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) -end + local SpawnTemplate + if self.TweakedTemplate ~= nil and self.TweakedTemplate == true then + BASE:I("WARNING: You are using a tweaked template.") + SpawnTemplate = self.SpawnTemplate + else + SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) + SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) + end SpawnTemplate.groupId = nil From 9b0f474edfb5a65005abba2cb852cfb260f0c157 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 14 Mar 2020 20:30:58 +0100 Subject: [PATCH 450/485] Update Spawn.lua --- Moose Development/Moose/Core/Spawn.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 02e2b3c60..34756b61c 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1435,9 +1435,6 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) --if self.Repeat then -- _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) --end - - --else - -- self:E("ERROR: No spawn template.") end From ff8c45475237318dd1c779164623e72d873123a2 Mon Sep 17 00:00:00 2001 From: Thor Vik Date: Mon, 16 Mar 2020 17:52:14 +0100 Subject: [PATCH 451/485] Applied @Warlords fix on swapped Airbases in ATC_Ground.lua PG map Solves Issue #1291 --- Moose Development/Moose/Functional/ATC_Ground.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index 9ded69874..5fcb27e25 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -2456,7 +2456,7 @@ end ATC_GROUND_PERSIANGULF = { ClassName = "ATC_GROUND_PERSIANGULF", Airbases = { - [AIRBASE.PersianGulf.Al_Dhafra_AB] = { ---FIX by Wingthor Changed from Abu_Musa_Island_Airport + [AIRBASE.PersianGulf.Abu_Musa_Island_Airport] = { PointsRunways = { [1] = { [1]={["y"]=-122813.71002344,["x"]=-31689.936027827,}, @@ -2475,7 +2475,7 @@ ATC_GROUND_PERSIANGULF = { }, }, }, - [AIRBASE.PersianGulf.Abu_Musa_Island_Airport] = { + [AIRBASE.PersianGulf.Al_Dhafra_AB] = { PointsRunways = { [1] = { [1]={["y"]=-174672.06004916,["x"]=-209880.97145616,}, From 850951bcb434eb2e6b6ef0ab3274f5c606636bfc Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 20 Mar 2020 16:35:28 +0100 Subject: [PATCH 452/485] Update Airbase.lua --- Moose Development/Moose/Wrapper/Airbase.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 0c538cd4f..9f3517a60 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -469,6 +469,7 @@ function AIRBASE:GetID(unique) --env.info(string.format("FF airbase=%s id=%s category=%s", tostring(AirbaseName), tostring(airbaseID), tostring(airbaseCategory))) -- No way AFIK to get the DCS version. So we check if the event exists. That should tell us if we are on DCS 2.5.6 or prior to that. + --[[ if world.event.S_EVENT_KILL and world.event.S_EVENT_KILL>0 and airbaseCategory==Airbase.Category.AIRDROME then -- We have to take the key value of this loop! @@ -479,6 +480,7 @@ function AIRBASE:GetID(unique) airbaseID=airbaseID+11 end end + ]] if AirbaseName==self.AirbaseName then if airbaseCategory==Airbase.Category.SHIP then From a796c7a5941af5028508397c8fe7ec9d9bdeae30 Mon Sep 17 00:00:00 2001 From: Pikes Date: Sat, 28 Mar 2020 18:56:14 +0000 Subject: [PATCH 453/485] Urgent fix for Ford_AB name change. this breaks Moose completely until pushed. ATC table was changed to the correct name for the enumerator string of "Ford" to "Ford_AB" The main enumerator table was not also changed so the two mismatched. One has to agree with the other else Moose stops with an index error. However, there could be minor risk if someone refers to the old enumerator. Whilst we dont care about the string name, the AB was changed by ED anyway. The actual way to keep is to have the string equal the map name. Either way is damned if you do, damned if you don't but I'd rather keep the convention and blame ED than keep it wrong and inconsistent. --- Moose Development/Moose/Wrapper/Airbase.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 9f3517a60..b27fd5f07 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -183,7 +183,7 @@ AIRBASE.Nevada = { -- * AIRBASE.Normandy.Needs_Oar_Point -- * AIRBASE.Normandy.Funtington -- * AIRBASE.Normandy.Tangmere --- * AIRBASE.Normandy.Ford +-- * AIRBASE.Normandy.Ford_AF -- @field Normandy AIRBASE.Normandy = { ["Saint_Pierre_du_Mont"] = "Saint Pierre du Mont", @@ -216,7 +216,7 @@ AIRBASE.Normandy = { ["Needs_Oar_Point"] = "Needs Oar Point", ["Funtington"] = "Funtington", ["Tangmere"] = "Tangmere", - ["Ford"] = "Ford", + ["Ford_AF"] = "Ford_AF", ["Goulet"] = "Goulet", ["Argentan"] = "Argentan", ["Vrigny"] = "Vrigny", From bfb3ae33f4e45e667687203139a12e1b2a255490 Mon Sep 17 00:00:00 2001 From: Pikes Date: Thu, 2 Apr 2020 13:26:03 +0100 Subject: [PATCH 454/485] Changing default altitude to BARO to avoid DCS AI stopping engaging or even flying int he right direction. --- Moose Development/Moose/AI/AI_A2A_Patrol.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index 45fef37cf..229e15328 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -127,7 +127,7 @@ AI_A2A_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#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to BARO -- @return #AI_A2A_PATROL self -- @usage -- -- Define a new AI_A2A_PATROL Object. This PatrolArea will patrol a Group within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. @@ -151,8 +151,8 @@ function AI_A2A_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCei self.PatrolMinSpeed = PatrolMinSpeed self.PatrolMaxSpeed = PatrolMaxSpeed - -- defafult PatrolAltType to "RADIO" if not specified - self.PatrolAltType = PatrolAltType or "RADIO" + -- defafult PatrolAltType to "BARO" if not specified + self.PatrolAltType = PatrolAltType or "BARO" self:AddTransition( { "Started", "Airborne", "Refuelling" }, "Patrol", "Patrolling" ) From 6d75b25f579a392872047dedcffab30558d34b59 Mon Sep 17 00:00:00 2001 From: Pikes Date: Thu, 2 Apr 2020 13:50:41 +0100 Subject: [PATCH 455/485] change default to "BARO" addition on AI_A2A_Patrol already done --- Moose Development/Moose/AI/AI_Patrol.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/AI/AI_Patrol.lua b/Moose Development/Moose/AI/AI_Patrol.lua index dcb94b99d..767dc4400 100644 --- a/Moose Development/Moose/AI/AI_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Patrol.lua @@ -178,8 +178,8 @@ function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltit self.PatrolMinSpeed = PatrolMinSpeed self.PatrolMaxSpeed = PatrolMaxSpeed - -- defafult PatrolAltType to "RADIO" if not specified - self.PatrolAltType = PatrolAltType or "RADIO" + -- defafult PatrolAltType to "BARO" if not specified + self.PatrolAltType = PatrolAltType or "BARO" self:SetRefreshTimeInterval( 30 ) From 861f2da4d6d223136d1000969886981bc8e8c768 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 4 Apr 2020 23:08:23 +0200 Subject: [PATCH 456/485] ATIS and WAREHOUSE --- .../Moose/Functional/Warehouse.lua | 1478 +++++++++++------ Moose Development/Moose/Ops/ATIS.lua | 312 +++- 2 files changed, 1173 insertions(+), 617 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 6c359763c..6fe3be455 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -46,6 +46,7 @@ -- @type WAREHOUSE -- @field #string ClassName Name of the class. -- @field #boolean Debug If true, send debug messages to all. +-- @field #string wid Identifier of the warehouse printed before other output to DCS.log file. -- @field #boolean Report If true, send status messages to coalition. -- @field Wrapper.Static#STATIC warehouse The phyical warehouse structure. -- @field #string alias Alias of the warehouse. Name its called when sending messages. @@ -55,8 +56,7 @@ -- @field Core.Point#COORDINATE road Closest point to warehouse on road. -- @field Core.Point#COORDINATE rail Closest point to warehouse on rail. -- @field Core.Zone#ZONE spawnzone Zone in which assets are spawned. --- @field #string wid Identifier of the warehouse printed before other output to DCS.log file. --- @field #number uid Unit identifier of the warehouse. Derived from id of warehouse static element. +-- @field #number uid Unique ID of the warehouse. -- @field #number markerid ID of the warehouse marker at the airbase. -- @field #number dTstatus Time interval in seconds of updating the warehouse status and processing new events. Default 30 seconds. -- @field #number queueid Unit id of each request in the queue. Essentially a running number starting at one and incremented when a new request is added. @@ -79,6 +79,7 @@ -- @field #number lowfuelthresh Low fuel threshold. Triggers the event AssetLowFuel if for any unit fuel goes below this number. -- @field #boolean respawnafterdestroyed If true, warehouse is respawned after it was destroyed. Assets are kept. -- @field #number respawndelay Delay before respawn in seconds. +-- @field #boolean markerOn If true, markers are displayed on the F10 map. -- @extends Core.Fsm#FSM --- Have your assets at the right place at the right time - or not! @@ -1547,6 +1548,7 @@ WAREHOUSE = { ClassName = "WAREHOUSE", Debug = false, + lid = nil, Report = true, warehouse = nil, alias = nil, @@ -1556,7 +1558,6 @@ WAREHOUSE = { road = nil, rail = nil, spawnzone = nil, - wid = nil, uid = nil, markerid = nil, dTstatus = 30, @@ -1603,6 +1604,11 @@ WAREHOUSE = { -- @field DCS#AI.Skill skill Skill of AI unit. -- @field #string livery Livery of the asset. -- @field #string assignment Assignment of the asset. This could, e.g., be used in the @{#WAREHOUSE.OnAfterNewAsset) function. +-- @field #boolean spawned If true, asset was spawned into the cruel world. If false, it is still in stock. +-- @field #string spawngroupname Name of the spawned group. +-- @field #boolean iscargo If true, asset is cargo. If false asset is transport. Nil if in stock. +-- @field #number rid The request ID of this asset. +-- @field #boolean arrived If true, asset arrived at its destination. --- Item of the warehouse queue table. -- @type WAREHOUSE.Queueitem @@ -1646,12 +1652,14 @@ WAREHOUSE = { -- @field #string ATTRIBUTE Generalized attribute @{#WAREHOUSE.Attribute}. -- @field #string CATEGORY Asset category of type DCS#Group.Category, i.e. GROUND, AIRPLANE, HELICOPTER, SHIP, TRAIN. -- @field #string ASSIGNMENT Assignment of asset when it was added. +-- @field #string ASSETLIST List of specific assets gives as a table of assets. Mind the curly brackets {}. WAREHOUSE.Descriptor = { GROUPNAME="templatename", UNITTYPE="unittype", ATTRIBUTE="attribute", CATEGORY="category", ASSIGNMENT="assignment", + ASSETLIST="assetlist," } --- Generalized asset attributes. Can be used to request assets with certain general characteristics. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit. @@ -1756,7 +1764,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.9.6" +WAREHOUSE.version="1.0.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1764,7 +1772,7 @@ WAREHOUSE.version="0.9.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? +-- NOGO: 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: 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. @@ -1828,7 +1836,7 @@ function WAREHOUSE:New(warehouse, alias) local warehousename=warehouse warehouse=UNIT:FindByName(warehousename) if warehouse==nil then - env.info(string.format("No warehouse unit with name %s found trying static.", tostring(warehousename))) + --env.info(string.format("No warehouse unit with name %s found trying static.", tostring(warehousename))) warehouse=STATIC:FindByName(warehousename, true) self.isunit=false else @@ -1852,7 +1860,7 @@ function WAREHOUSE:New(warehouse, alias) local self = BASE:Inherit(self, FSM:New()) -- #WAREHOUSE -- Set some string id for output to DCS.log file. - self.wid=string.format("WAREHOUSE %s | ", self.alias) + self.lid=string.format("WAREHOUSE %s | ", self.alias) -- Set some variables. self.warehouse=warehouse @@ -1863,18 +1871,18 @@ function WAREHOUSE:New(warehouse, alias) -- Set unique ID for this warehouse. self.uid=_WAREHOUSEDB.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. + -- Closest of the same coalition but within 5 km range. local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition()) - if _airbase and _airbase:GetCoordinate():Get2DDistance(self:GetCoordinate()) < 3000 then + if _airbase and _airbase:GetCoordinate():Get2DDistance(self:GetCoordinate()) <= 5000 then self:SetAirbase(_airbase) end -- Define warehouse and default spawn zone. self.zone=ZONE_RADIUS:New(string.format("Warehouse zone %s", self.warehouse:GetName()), warehouse:GetVec2(), 500) self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone", self.warehouse:GetName()), warehouse:GetVec2(), 250) + + -- Defaults + self:SetMarker(true) -- Add warehouse to database. _WAREHOUSEDB.Warehouses[self.uid]=self @@ -1890,18 +1898,26 @@ function WAREHOUSE:New(warehouse, alias) -- From State --> Event --> To State self:AddTransition("NotReadyYet", "Load", "Loaded") -- Load the warehouse state from scatch. self:AddTransition("Stopped", "Load", "Loaded") -- Load the warehouse state stopped state. + self:AddTransition("NotReadyYet", "Start", "Running") -- Start the warehouse from scratch. self:AddTransition("Loaded", "Start", "Running") -- Start the warehouse when loaded from disk. + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "AddAsset", "*") -- Add asset to warehouse stock. self:AddTransition("*", "NewAsset", "*") -- New asset was added to warehouse stock. + self:AddTransition("*", "AddRequest", "*") -- New request from other warehouse. self:AddTransition("Running", "Request", "*") -- Process a request. Only in running mode. + self:AddTransition("Running", "RequestSpawned", "*") -- Assets of request were spawned. self:AddTransition("Attacked", "Request", "*") -- Process a request. Only in running mode. + self:AddTransition("*", "Unloaded", "*") -- Cargo has been unloaded from the carrier (unused ==> unnecessary?). self:AddTransition("*", "AssetSpawned", "*") -- Asset has been spawned into the world. self:AddTransition("*", "AssetLowFuel", "*") -- Asset is low on fuel. + self:AddTransition("*", "Arrived", "*") -- Cargo or transport group has arrived. + self:AddTransition("*", "Delivered", "*") -- All cargo groups of a request have been delivered to the requesting warehouse. self:AddTransition("Running", "SelfRequest", "*") -- Request to warehouse itself. Requested assets are only spawned but not delivered anywhere. self:AddTransition("Attacked", "SelfRequest", "*") -- Request to warehouse itself. Also possible when warehouse is under attack! @@ -2085,6 +2101,22 @@ function WAREHOUSE:New(warehouse, alias) -- @param #number Delay Delay in seconds. -- @param #WAREHOUSE.Queueitem Request Information table of the request. + --- On before "Request" user function. The necessary cargo and transport assets will be spawned. Time to set some additional asset parameters. + -- @function [parent=#WAREHOUSE] OnBeforeRequest + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Queueitem Request Information table of the request. + + --- On after "Request" user function. The necessary cargo and transport assets were spawned. + -- @function [parent=#WAREHOUSE] OnAfterRequest + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Queueitem Request Information table of the request. + --- Triggers the FSM event "Arrived" when a group has arrived at the destination warehouse. -- This function should always be called from the sending and not the receiving warehouse. @@ -2549,6 +2581,37 @@ function WAREHOUSE:SetAutoDefenceOff() return self end +--- Set valid parking spot IDs. +-- @param #WAREHOUSE self +-- @param #table ParkingIDs Table of numbers. +-- @return #WAREHOUSE self +function WAREHOUSE:SetParkingIDs(ParkingIDs) + if type(ParkingIDs)~="table" then + ParkingIDs={ParkingIDs} + end + self.parkingIDs=ParkingIDs + return self +end + +--- Check parking ID. +-- @param #WAREHOUSE self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot. +-- @return #boolean If true, parking is valid. +function WAREHOUSE:_CheckParkingValid(spot) + if self.parkingIDs==nil then + return true + end + + for _,id in pairs(self.parkingIDs or {}) do + if spot.TerminalID==id then + return true + end + end + + return false +end + + --- Enable auto save of warehouse assets at mission end event. -- @param #WAREHOUSE self -- @param #string path Path where to save the asset data file. @@ -2561,6 +2624,19 @@ function WAREHOUSE:SetSaveOnMissionEnd(path, filename) return self end +--- Show or don't show markers on the F10 map displaying the Warehouse stock and road/rail connections. +-- @param #WAREHOUSE self +-- @param #boolean switch If true (or nil), markers are on. If false, markers are not displayed. +-- @return #WAREHOUSE self +function WAREHOUSE:SetMarker(switch) + if switch==false then + self.markerOn=false + else + self.markerOn=true + end + return self +end + --- Set respawn after destroy. -- @param #WAREHOUSE self -- @return #WAREHOUSE self @@ -2699,7 +2775,7 @@ function WAREHOUSE:AddOffRoadPath(remotewarehouse, group, oneway) local path=self:_NewLane(group, startcoord, finalcoord) if path==nil then - self:E(self.wid.."ERROR: Offroad path could not be added. Group present in ME?") + self:E(self.lid.."ERROR: Offroad path could not be added. Group present in ME?") return end @@ -3102,7 +3178,7 @@ function WAREHOUSE:FindNearestWarehouse(MinAssets, Descriptor, DescriptorValue, -- Get number of assets. Whole stock is returned if no descriptor/value is given. local nassets=warehouse:GetNumberOfAssets(Descriptor, DescriptorValue) - --env.info(string.format(" FF warehouse %s nassets = %d for %s=%s", warehouse.alias, nassets, tostring(Descriptor), tostring(DescriptorValue))) + --env.info(string.format("FF warehouse %s nassets = %d for %s=%s", warehouse.alias, nassets, tostring(Descriptor), tostring(DescriptorValue))) -- Assume we have enough. local enough=true @@ -3190,7 +3266,7 @@ function WAREHOUSE:onafterStart(From, Event, To) end end -- Mark point at road connection. - if self.road then + if self.road and self.markerOn then self.markroad=self.road:MarkToCoalition(string.format("%s road connection.",self.alias), self:GetCoalition(), true) end @@ -3204,7 +3280,7 @@ function WAREHOUSE:onafterStart(From, Event, To) end end -- Mark point at rail connection. - if self.rail then + if self.rail and self.markerOn then self.markrail=self.rail:MarkToCoalition(string.format("%s rail connection.", self.alias), self:GetCoalition(), true) end @@ -3236,7 +3312,7 @@ end -- @param #string To To state. function WAREHOUSE:onafterRestart(From, Event, To) - self:I(self.wid..string.format("Restarting Warehouse %s.", self.alias)) + self:I(self.lid..string.format("Restarting Warehouse %s.", self.alias)) -- Handle events: self:HandleEvent(EVENTS.Birth, self._OnEventBirth) @@ -3298,7 +3374,7 @@ end -- @param #string Event Event. -- @param #string To To state. function WAREHOUSE:onafterPause(From, Event, To) - self:I(self.wid..string.format("Warehouse %s paused! Queued requests are not processed in this state.", self.alias)) + self:I(self.lid..string.format("Warehouse %s paused! Queued requests are not processed in this state.", self.alias)) end --- On after "Unpause" event. Unpauses the warehouse, i.e. requests in queue are processed again. @@ -3307,7 +3383,7 @@ end -- @param #string Event Event. -- @param #string To To state. function WAREHOUSE:onafterUnpause(From, Event, To) - self:I(self.wid..string.format("Warehouse %s unpaused! Processing of requests is resumed.", self.alias)) + self:I(self.lid..string.format("Warehouse %s unpaused! Processing of requests is resumed.", self.alias)) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3318,7 +3394,9 @@ end -- @param #string Event Event. -- @param #string To To state. function WAREHOUSE:onafterStatus(From, Event, To) - self:I(self.wid..string.format("Checking status of warehouse %s. Current FSM state %s. Global warehouse assets = %d.", self.alias, self:GetState(), #_WAREHOUSEDB.Assets)) + + -- Info. + self:I(self.lid..string.format("State=%s, Assets=%d, Requests: waiting=%d, pending=%d", self:GetState(), #self.stock, #self.queue, #self.pending)) -- Check if any pending jobs are done and can be deleted from the queue. self:_JobDone() @@ -3403,7 +3481,7 @@ function WAREHOUSE:_JobDone() local text=string.format("Request id=%d: Cargo: Ntot=%d, Nalive=%d, Ndelivered=%d, Ndead=%d | Transport: Ntot=%d, Nalive=%d, Nhome=%d, Ndead=%d", request.uid, ncargotot, ncargo, ncargodelivered, ncargodead, ntransporttot, ntransport, ntransporthome, ntransportdead) - self:T(self.wid..text) + self:T(self.lid..text) -- Handle different cases depending on what asset are still around. @@ -3478,7 +3556,7 @@ function WAREHOUSE:_JobDone() -- Debug text. local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s", group:GetName(), speed, tostring(onground), airbase, tostring(inspawnzone), tostring(ishome)) - self:T(self.wid..text) + self:I(self.lid..text) if ishome then @@ -3640,7 +3718,8 @@ end -- @param DCS#AI.Skill skill Skill of the asset. -- @param #table liveries Table of livery names. When the asset is spawned one livery is chosen randomly. -- @param #string assignment A free to choose string specifying an assignment for the asset. This can be used with the @{#WAREHOUSE.OnAfterNewAsset} function. -function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, skill, liveries, assignment) +-- @param #table other (Optional) Table of other useful data. Can be collected via WAREHOUSE.OnAfterNewAsset() function for example +function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, skill, liveries, assignment, other) self:T({group=group, ngroups=ngroups, forceattribute=forceattribute, forcecargobay=forcecargobay, forceweight=forceweight}) -- Set default. @@ -3661,28 +3740,36 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu local wid,aid,rid=self:_GetIDsFromGroup(group) if wid and aid and rid then + --------------------------- -- This is a KNOWN asset -- --------------------------- -- Get the original warehouse this group belonged to. local warehouse=self:FindWarehouseInDB(wid) + if warehouse then + local request=warehouse:_GetRequestOfGroup(group, warehouse.pending) + if request then -- Increase number of cargo delivered and transports home. local istransport=warehouse:_GroupIsTransport(group,request) + if istransport==true then request.ntransporthome=request.ntransporthome+1 request.transportgroupset:Remove(group:GetName(), true) - self:T3(warehouse.wid..string.format("Transport %d of %s returned home.", request.ntransporthome, tostring(request.ntransport))) + local ntrans=request.transportgroupset:Count() + self:T2(warehouse.lid..string.format("Transport %d of %s returned home. TransportSet=%d", request.ntransporthome, tostring(request.ntransport), ntrans)) elseif istransport==false then request.ndelivered=request.ndelivered+1 - request.cargogroupset:Remove(self:_GetNameWithOut(group), true) - self:T3(warehouse.wid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset))) + local namewo=self:_GetNameWithOut(group) + request.cargogroupset:Remove(namewo, true) + local ncargo=request.cargogroupset:Count() + self:T2(warehouse.lid..string.format("Cargo %s: %d of %s delivered. CargoSet=%d", namewo, request.ndelivered, tostring(request.nasset), ncargo)) else - self:T(warehouse.wid..string.format("WARNING: Group %s is neither cargo nor transport!", group:GetName())) + self:E(warehouse.lid..string.format("WARNING: Group %s is neither cargo nor transport! Need to investigate...", group:GetName())) end -- If no assignment was given we take the assignment of the request if there is any. @@ -3696,57 +3783,85 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu -- Get the asset from the global DB. local asset=self:FindAssetInDB(group) - -- Set livery. - if liveries then - asset.livery=liveries[math.random(#liveries)] - end - - -- Set skill. - asset.skill=skill - -- Note the group is only added once, i.e. the ngroups parameter is ignored here. -- This is because usually these request comes from an asset that has been transfered from another warehouse and hence should only be added once. if asset~=nil then self:_DebugMessage(string.format("Warehouse %s: Adding KNOWN asset uid=%d with attribute=%s to stock.", self.alias, asset.uid, asset.attribute), 5) + + -- Set livery. + if liveries then + if type(liveries)=="table" then + asset.livery=liveries[math.random(#liveries)] + else + asset.livery=liveries + end + end + + -- Set skill. + asset.skill=skill or asset.skill + + -- Asset now belongs to this warehouse. Set warehouse ID. + asset.wid=self.uid + + -- No request associated with this asset. + asset.rid=nil + + -- Asset is not spawned. + asset.spawned=false + asset.iscargo=nil + asset.arrived=nil + + -- Add asset to stock. table.insert(self.stock, asset) - self:NewAsset(asset, assignment or "") + + -- Trigger New asset event. + self:__NewAsset(0.1, asset, assignment or "") else self:_ErrorMessage(string.format("ERROR: Known asset could not be found in global warehouse db!"), 0) end else + ------------------------- -- This is a NEW asset -- ------------------------- -- Debug info. - self:_DebugMessage(string.format("Warehouse %s: Adding %d NEW assets of group %s to stock.", self.alias, n, tostring(group:GetName())), 5) + self:_DebugMessage(string.format("Warehouse %s: Adding %d NEW assets of group %s to stock", self.alias, n, tostring(group:GetName())), 5) -- This is a group that is not in the db yet. Add it n times. local assets=self:_RegisterAsset(group, n, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill, assignment) -- Add created assets to stock of this warehouse. for _,asset in pairs(assets) do + + -- Asset belongs to this warehouse. Set warehouse ID. + asset.wid=self.uid + + -- No request associated with this asset. + asset.rid=nil + + -- Add asset to stock. table.insert(self.stock, asset) - self:NewAsset(asset, assignment or "") + + -- Trigger NewAsset event. Delay a bit for OnAfterNewAsset functions to work properly. + self:__NewAsset(0.1, asset, assignment or "") end end -- Destroy group if it is alive. if group:IsAlive()==true then - self:_DebugMessage(string.format("Destroying group %s.", group:GetName()), 5) + self:_DebugMessage(string.format("Removing group %s", group:GetName()), 5) -- Setting parameter to false, i.e. creating NO dead or remove unit event, seems to not confuse the dispatcher logic. group:Destroy(false) end else - self:E(self.wid.."ERROR: Unknown group added as asset!") + self:E(self.lid.."ERROR: Unknown group added as asset!") self:E({unknowngroup=group}) end - -- Update status. - --self:__Status(-1) end --- Register new asset in globase warehouse data base. @@ -3812,7 +3927,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, -- Calcuate cargo bay limit value. cargomax=massmax-massfuel-massempty - self:T3(self.wid..string.format("Unit name=%s: mass empty=%.1f kg, fuel=%.1f kg, max=%.1f kg ==> cargo=%.1f kg", unit:GetName(), unitweight, massfuel, massmax, cargomax)) + self:T3(self.lid..string.format("Unit name=%s: mass empty=%.1f kg, fuel=%.1f kg, max=%.1f kg ==> cargo=%.1f kg", unit:GetName(), unitweight, massfuel, massmax, cargomax)) -- Cargo bay size. local bay=forcecargobay or unit:GetCargoBayFreeWeight() @@ -3864,6 +3979,8 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, end asset.skill=skill asset.assignment=assignment + asset.spawned=false + asset.spawngroupname=string.format("%s_AID-%d", templategroupname, asset.uid) if i==1 then self:_AssetItemInfo(asset) @@ -3885,21 +4002,22 @@ end function WAREHOUSE:_AssetItemInfo(asset) -- Info about asset: local text=string.format("\nNew asset with id=%d for warehouse %s:\n", asset.uid, self.alias) - text=text..string.format("Template name = %s\n", asset.templatename) - text=text..string.format("Unit type = %s\n", asset.unittype) - text=text..string.format("Attribute = %s\n", asset.attribute) - text=text..string.format("Category = %d\n", asset.category) - text=text..string.format("Units # = %d\n", asset.nunits) - text=text..string.format("Speed max = %5.2f km/h\n", asset.speedmax) - text=text..string.format("Range max = %5.2f km\n", asset.range/1000) - text=text..string.format("Size max = %5.2f m\n", asset.size) - text=text..string.format("Weight total = %5.2f kg\n", asset.weight) - text=text..string.format("Cargo bay tot = %5.2f kg\n", asset.cargobaytot) - text=text..string.format("Cargo bay max = %5.2f kg\n", asset.cargobaymax) - text=text..string.format("Load radius = %s m\n", tostring(asset.loadradius)) - text=text..string.format("Skill = %s\n", tostring(asset.skill)) - text=text..string.format("Livery = %s", tostring(asset.livery)) - self:T(self.wid..text) + text=text..string.format("Spawngroup name= %s\n", asset.spawngroupname) + text=text..string.format("Template name = %s\n", asset.templatename) + text=text..string.format("Unit type = %s\n", asset.unittype) + text=text..string.format("Attribute = %s\n", asset.attribute) + text=text..string.format("Category = %d\n", asset.category) + text=text..string.format("Units # = %d\n", asset.nunits) + text=text..string.format("Speed max = %5.2f km/h\n", asset.speedmax) + text=text..string.format("Range max = %5.2f km\n", asset.range/1000) + text=text..string.format("Size max = %5.2f m\n", asset.size) + text=text..string.format("Weight total = %5.2f kg\n", asset.weight) + text=text..string.format("Cargo bay tot = %5.2f kg\n", asset.cargobaytot) + text=text..string.format("Cargo bay max = %5.2f kg\n", asset.cargobaymax) + text=text..string.format("Load radius = %s m\n", tostring(asset.loadradius)) + text=text..string.format("Skill = %s\n", tostring(asset.skill)) + text=text..string.format("Livery = %s", tostring(asset.livery)) + self:I(self.lid..text) self:T({DCSdesc=asset.DCSdesc}) self:T3({Template=asset.template}) end @@ -3910,9 +4028,9 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param #WAREHOUSE.Assetitem asset The asset that has just been added. --- @parma #string assignment The (optional) assignment for the asset. +-- @param #string assignment The (optional) assignment for the asset. function WAREHOUSE:onafterNewAsset(From, Event, To, asset, assignment) - self:T(self.wid..string.format("New asset %s id=%d with assignment %s.", asset.templatename, asset.uid, assignment)) + self:T(self.lid..string.format("New asset %s id=%d with assignment %s.", tostring(asset.templatename), asset.uid, tostring(assignment))) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3984,6 +4102,13 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a string!", 5) okay=false end + + elseif AssetDescriptor==WAREHOUSE.Descriptor.ASSETLIST then + + if type(AssetDescriptorValue)~="table" then + self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a table!", 5) + okay=false + end else self:_ErrorMessage("ERROR: Invalid request. Asset descriptor is not ATTRIBUTE, CATEGORY, GROUPNAME, UNITTYPE or ASSIGNMENT!", 5) @@ -4017,7 +4142,7 @@ end -- @param #WAREHOUSE.TransportType TransportType Type of transport. -- @param #number nTransport Number of transport units requested. -- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low. --- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. +-- @param #string Assignment A keyword or text that can later be used to identify this request and postprocess the assets. function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Prio, Assignment) -- Defaults. @@ -4067,8 +4192,6 @@ function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor self.alias, warehouse.alias, request.assetdesc, tostring(request.assetdescval), tostring(request.nasset), request.transporttype, tostring(request.ntransport)) self:_DebugMessage(text, 5) - -- Update status - --self:__Status(-1) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4105,8 +4228,10 @@ function WAREHOUSE:onbeforeRequest(From, Event, To, Request) self:_InfoMessage(text, 10) -- Delete request from queue because it will never be possible. - --TODO: Unless(!) this is a moving warehouse which could, e.g., be an aircraft carrier. - --self:_DeleteQueueItem(Request, self.queue) + -- Unless(!) at least one is a moving warehouse, which could, e.g., be an aircraft carrier. + if not (self.isunit or Request.warehouse.isunit) then + self:_DeleteQueueItem(Request, self.queue) + end return false end @@ -4117,7 +4242,7 @@ function WAREHOUSE:onbeforeRequest(From, Event, To, Request) end ---- On after "Request" event. Initiates the transport of the assets to the requesting warehouse. +--- On after "Request" event. Spawns the necessary cargo and transport assets. -- @param #WAREHOUSE self -- @param #string From From state. -- @param #string Event Event. @@ -4127,7 +4252,7 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Info message. local text=string.format("Warehouse %s: Processing request id=%d from warehouse %s.\n", self.alias, Request.uid, Request.warehouse.alias) - text=text..string.format("Requested %s assets of %s=%s.\n", tostring(Request.nasset), Request.assetdesc, Request.assetdescval) + text=text..string.format("Requested %s assets of %s=%s.\n", tostring(Request.nasset), Request.assetdesc, Request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST and "Asset list" or Request.assetdescval) text=text..string.format("Transports %s of type %s.", tostring(Request.ntransport), tostring(Request.transporttype)) self:_InfoMessage(text, 5) @@ -4135,177 +4260,22 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Cargo assets. ------------------------------------------------------------------------------------------------------------------------------------ - -- Pending request. Add cargo groups to request. - local Pending=Request --#WAREHOUSE.Pendingitem - -- Set time stamp. - Pending.timestamp=timer.getAbsTime() - - -- Init problem table. - Pending.assetproblem={} + Request.timestamp=timer.getAbsTime() -- Spawn assets of this request. - local _spawngroups=self:_SpawnAssetRequest(Pending) --Core.Set#SET_GROUP - - -- Check if any group was spawned. If not, delete the request. - if _spawngroups:Count()==0 then - self:_DebugMessage(string.format("ERROR: Groups or request %d could not be spawned. Request is rejected and deleted from queue!", Request.uid)) - -- Delete request from queue. - self:_DeleteQueueItem(Request, self.queue) - return - end - - -- General type and category. - local _cargotype=Request.cargoattribute --#WAREHOUSE.Attribute - local _cargocategory=Request.cargocategory --DCS#Group.Category - - -- Add groups to pending item. - Pending.cargogroupset=_spawngroups + self:_SpawnAssetRequest(Request) ------------------------------------------------------------------------------------------------------------------------------------ - -- Self request: assets are spawned at warehouse but not transported anywhere. + -- Transport assets ------------------------------------------------------------------------------------------------------------------------------------ - -- Self request! Assets are only spawned but not routed or transported anywhere. - if Request.toself then - self:_DebugMessage(string.format("Selfrequest! Current status %s", self:GetState())) - - -- Add request to pending queue. - table.insert(self.pending, Pending) - - -- Delete request from queue. - self:_DeleteQueueItem(Request, self.queue) - - -- Start self request. - self:__SelfRequest(1,_spawngroups, Pending) - - return - end - - ------------------------------------------------------------------------------------------------------------------------------------ - -- Self propelled: assets go to the requesting warehouse by themselfs. - ------------------------------------------------------------------------------------------------------------------------------------ - - -- No transport unit requested. Assets go by themselfes. - if Request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then - self:T2(self.wid..string.format("Got selfpropelled request for %d assets.",_spawngroups:Count())) - - for _,_spawngroup in pairs(_spawngroups:GetSetObjects()) do - - -- Group intellisense. - local group=_spawngroup --Wrapper.Group#GROUP - - - -- Route cargo to their destination. - if _cargocategory==Group.Category.GROUND then - self:T2(self.wid..string.format("Route ground group %s.", group:GetName())) - - -- Random place in the spawn zone of the requesting warehouse. - local ToCoordinate=Request.warehouse.spawnzone:GetRandomCoordinate() - - -- Debug marker. - if self.Debug then - ToCoordinate:MarkToAll(string.format("Destination of group %s", group:GetName())) - end - - -- Route ground. - self:_RouteGround(group, Request) - - elseif _cargocategory==Group.Category.AIRPLANE or _cargocategory==Group.Category.HELICOPTER then - self:T2(self.wid..string.format("Route airborne group %s.", group:GetName())) - - -- Route plane to the requesting warehouses airbase. - -- Actually, the route is already set. We only need to activate the uncontrolled group. - self:_RouteAir(group) - - elseif _cargocategory==Group.Category.SHIP then - self:T2(self.wid..string.format("Route naval group %s.", group:GetName())) - - -- Route plane to the requesting warehouses airbase. - self:_RouteNaval(group, Request) - - elseif _cargocategory==Group.Category.TRAIN then - self:T2(self.wid..string.format("Route train group %s.", group:GetName())) - - -- Route train to the rail connection of the requesting warehouse. - self:_RouteTrain(group, Request.warehouse.rail) - - else - self:E(self.wid..string.format("ERROR: unknown category %s for self propelled cargo %s!", tostring(_cargocategory), tostring(group:GetName()))) - end - - end - - -- Add request to pending queue. - table.insert(self.pending, Pending) - - -- Delete request from queue. - self:_DeleteQueueItem(Request, self.queue) - - -- No cargo transport necessary. - return - end - - ------------------------------------------------------------------------------------------------------------------------------------ - -- Prepare cargo groups for transport - ------------------------------------------------------------------------------------------------------------------------------------ - - -- Add groups to cargo if they don't go by themselfs. - local CargoGroups --Core.Set#SET_CARGO - - -- Board radius, i.e. when the cargo will begin to board the carrier - local _boardradius=500 - - if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then - _boardradius=5000 - elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then - --_loadradius=1000 - --_boardradius=nil - elseif Request.transporttype==WAREHOUSE.TransportType.APC then - --_boardradius=nil - end - - -- Empty cargo group set. - CargoGroups = SET_CARGO:New() - - local function getasset(group) - local _,aid,_=self:_GetIDsFromGroup(group) - self:FindAssetInDB(group) - end - - -- Add cargo groups to set. - for _i,_group in pairs(_spawngroups:GetSetObjects()) do - - -- Find asset belonging to this group. - local asset=self:FindAssetInDB(_group) - - -- New cargo group object. - local cargogroup=CARGO_GROUP:New(_group, _cargotype,_group:GetName(),_boardradius, asset.loadradius) - - -- Set weight for this group. - cargogroup:SetWeight(asset.weight) - - -- Add group to group set. - CargoGroups:AddCargo(cargogroup) - end - - ------------------------------------------------------------------------------------------------------------------------------------ - -- Transport assets and dispatchers - ------------------------------------------------------------------------------------------------------------------------------------ - - -- Set of cargo carriers. - local TransportSet = SET_GROUP:New() - -- Shortcut to transport assets. local _assetstock=Request.transportassets - -- General type and category. - local _transporttype=Request.transportattribute - local _transportcategory=Request.transportcategory - -- Now we try to find all parking spots for all cargo groups in advance. Due to the for loop, the parking spots do not get updated while spawning. local Parking={} - if _transportcategory==Group.Category.AIRPLANE or _transportcategory==Group.Category.HELICOPTER then + if Request.transportcategory==Group.Category.AIRPLANE or Request.transportcategory==Group.Category.HELICOPTER then Parking=self:_FindParkingForAssets(self.airbase,_assetstock) end @@ -4322,24 +4292,32 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Get stock item. local _assetitem=_assetstock[i] --#WAREHOUSE.Assetitem - -- Create an alias name with the UIDs for the sending warehouse, asset and request. - local _alias=self:_alias(_assetitem.unittype, self.uid, _assetitem.uid, Request.uid) + -- Spawn group name + local _alias=_assetitem.spawngroupname + + -- Set Request ID. + _assetitem.rid=Request.uid + + -- Asset is transport. + _assetitem.spawned=false + _assetitem.iscargo=false + _assetitem.arrived=false local spawngroup=nil --Wrapper.Group#GROUP + + -- Add asset by id to all assets table. + Request.assets[_assetitem.uid]=_assetitem -- Spawn assets depending on type. if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then -- Spawn plane at airport in uncontrolled state. Will get activated when cargo is loaded. - spawngroup=self:_SpawnAssetAircraft(_alias,_assetitem, Pending, Parking[_assetitem.uid], true) + spawngroup=self:_SpawnAssetAircraft(_alias,_assetitem, Request, Parking[_assetitem.uid], true) elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then -- Spawn helos at airport in controlled state. They need to fly to the spawn zone. - spawngroup=self:_SpawnAssetAircraft(_alias,_assetitem, Pending, Parking[_assetitem.uid], false) - - -- Activate helo randomly within the next 10 seconds. - --spawngroup:StartUncontrolled(math.random(10)) + spawngroup=self:_SpawnAssetAircraft(_alias,_assetitem, Request, Parking[_assetitem.uid], false) elseif Request.transporttype==WAREHOUSE.TransportType.APC then @@ -4366,59 +4344,141 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) return end - -- Check if group was spawned. - if spawngroup then - -- Set state of warehouse so we can retrieve it later. - spawngroup:SetState(spawngroup, "WAREHOUSE", self) - - -- Add group to transportset. - TransportSet:AddGroup(spawngroup) - - -- Add assets to pending request. - Pending.assets[_assetitem.uid]=_assetitem - - -- Add transport assets. - table.insert(_transportassets,_assetitem) - - -- Asset spawned FSM function. - self:__AssetSpawned(1, spawngroup, _assetitem, Request) - end end - -- Delete spawned items from warehouse stock. - for _,_item in pairs(_transportassets) do - self:_DeleteStockItem(_item) - end - - -- Add transport groups, i.e. the carriers to request. - Pending.transportgroupset=TransportSet - - -- Add cargo group set. - Pending.transportcargoset=CargoGroups + -- Init problem table. + Request.assetproblem={} -- Add request to pending queue. - table.insert(self.pending, Pending) + table.insert(self.pending, Request) -- Delete request from queue. self:_DeleteQueueItem(Request, self.queue) - -- Adjust carrier units. This has to come AFTER the dispatchers have been defined because they set the cargobay free weight! - Pending.carriercargo={} - for _,carriergroup in pairs(TransportSet:GetSetObjects()) do - local asset=self:FindAssetInDB(carriergroup) - for _i,_carrierunit in pairs(carriergroup:GetUnits()) do - local carrierunit=_carrierunit --Wrapper.Unit#UNIT +end - -- Create empty tables which will be filled with the cargo groups of each carrier unit. Needed in case a carrier unit dies. - Pending.carriercargo[carrierunit:GetName()]={} +--- On after "RequestSpawned" event. Initiates the transport of the assets to the requesting warehouse. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE.Pendingitem Request Information table of the request. +-- @param Core.Set#SET_GROUP CargoGroupSet Set of cargo groups. +-- @param Core.Set#SET_GROUP TransportGroupSet Set of transport groups if any. +function WAREHOUSE:onafterRequestSpawned(From, Event, To, Request, CargoGroupSet, TransportGroupSet) - -- Adjust cargo bay of carrier unit. - local cargobay=asset.cargobay[_i] - carrierunit:SetCargoBayWeightLimit(cargobay) + -- General type and category. + local _cargotype=Request.cargoattribute --#WAREHOUSE.Attribute + local _cargocategory=Request.cargocategory --DCS#Group.Category + + -- Add groups to pending item. + --Request.cargogroupset=CargoGroupSet + + ------------------------------------------------------------------------------------------------------------------------------------ + -- Self request: assets are spawned at warehouse but not transported anywhere. + ------------------------------------------------------------------------------------------------------------------------------------ + + -- Self request! Assets are only spawned but not routed or transported anywhere. + if Request.toself then + self:_DebugMessage(string.format("Selfrequest! Current status %s", self:GetState())) + + -- Start self request. + self:__SelfRequest(1, CargoGroupSet, Request) + + return + end + + ------------------------------------------------------------------------------------------------------------------------------------ + -- Self propelled: assets go to the requesting warehouse by themselfs. + ------------------------------------------------------------------------------------------------------------------------------------ + + -- No transport unit requested. Assets go by themselfes. + if Request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then + self:T2(self.lid..string.format("Got selfpropelled request for %d assets.", CargoGroupSet:Count())) + + for _,_group in pairs(CargoGroupSet:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + -- Route cargo to their destination. + if _cargocategory==Group.Category.GROUND then + self:T2(self.lid..string.format("Route ground group %s.", group:GetName())) + + -- Random place in the spawn zone of the requesting warehouse. + local ToCoordinate=Request.warehouse.spawnzone:GetRandomCoordinate() + + -- Debug marker. + if self.Debug then + ToCoordinate:MarkToAll(string.format("Destination of group %s", group:GetName())) + end + + -- Route ground. + self:_RouteGround(group, Request) + + elseif _cargocategory==Group.Category.AIRPLANE or _cargocategory==Group.Category.HELICOPTER then + self:T2(self.lid..string.format("Route airborne group %s.", group:GetName())) + + -- Route plane to the requesting warehouses airbase. + -- Actually, the route is already set. We only need to activate the uncontrolled group. + self:_RouteAir(group) + + elseif _cargocategory==Group.Category.SHIP then + self:T2(self.lid..string.format("Route naval group %s.", group:GetName())) + + -- Route plane to the requesting warehouses airbase. + self:_RouteNaval(group, Request) + + elseif _cargocategory==Group.Category.TRAIN then + self:T2(self.lid..string.format("Route train group %s.", group:GetName())) + + -- Route train to the rail connection of the requesting warehouse. + self:_RouteTrain(group, Request.warehouse.rail) + + else + self:E(self.lid..string.format("ERROR: unknown category %s for self propelled cargo %s!", tostring(_cargocategory), tostring(group:GetName()))) + end - -- Debug info. - self:T2(self.wid..string.format("Cargo bay weight limit of carrier unit %s: %.1f kg.", carrierunit:GetName(), carrierunit:GetCargoBayFreeWeight())) end + + -- Transport group set. + Request.transportgroupset=TransportGroupSet + + -- No cargo transport necessary. + return + end + + ------------------------------------------------------------------------------------------------------------------------------------ + -- Prepare cargo groups for transport + ------------------------------------------------------------------------------------------------------------------------------------ + + -- Board radius, i.e. when the cargo will begin to board the carrier + local _boardradius=500 + + if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then + _boardradius=5000 + elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then + --_loadradius=1000 + --_boardradius=nil + elseif Request.transporttype==WAREHOUSE.TransportType.APC then + --_boardradius=nil + end + + -- Empty cargo group set. + local CargoGroups=SET_CARGO:New() + + -- Add cargo groups to set. + for _,_group in pairs(CargoGroupSet:GetSetObjects()) do + + -- Find asset belonging to this group. + local asset=self:FindAssetInDB(_group) + + -- New cargo group object. + local cargogroup=CARGO_GROUP:New(_group, _cargotype,_group:GetName(),_boardradius, asset.loadradius) + + -- Set weight for this group. + cargogroup:SetWeight(asset.weight) + + -- Add group to group set. + CargoGroups:AddCargo(cargogroup) end ------------------------ @@ -4435,7 +4495,7 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) local DeployAirbaseSet = SET_ZONE:New():AddZone(ZONE_AIRBASE:New(Request.airbase:GetName())) -- Define dispatcher for this task. - CargoTransport = AI_CARGO_DISPATCHER_AIRPLANE:New(TransportSet, CargoGroups, PickupAirbaseSet, DeployAirbaseSet) + CargoTransport = AI_CARGO_DISPATCHER_AIRPLANE:New(TransportGroupSet, CargoGroups, PickupAirbaseSet, DeployAirbaseSet) -- Set home zone. CargoTransport:SetHomeZone(ZONE_AIRBASE:New(self.airbase:GetName())) @@ -4447,7 +4507,7 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) local DeployZoneSet = SET_ZONE:New():AddZone(Request.warehouse.spawnzone) -- Define dispatcher for this task. - CargoTransport = AI_CARGO_DISPATCHER_HELICOPTER:New(TransportSet, CargoGroups, PickupZoneSet, DeployZoneSet) + CargoTransport = AI_CARGO_DISPATCHER_HELICOPTER:New(TransportGroupSet, CargoGroups, PickupZoneSet, DeployZoneSet) -- Home zone. CargoTransport:SetHomeZone(self.spawnzone) @@ -4459,13 +4519,13 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) local DeployZoneSet = SET_ZONE:New():AddZone(Request.warehouse.spawnzone) -- Define dispatcher for this task. - CargoTransport = AI_CARGO_DISPATCHER_APC:New(TransportSet, CargoGroups, PickupZoneSet, DeployZoneSet, 0) + CargoTransport = AI_CARGO_DISPATCHER_APC:New(TransportGroupSet, CargoGroups, PickupZoneSet, DeployZoneSet, 0) -- Set home zone. CargoTransport:SetHomeZone(self.spawnzone) else - self:E(self.wid.."ERROR: Unknown transporttype!") + self:E(self.lid.."ERROR: Unknown transporttype!") end -- Set pickup and deploy radii. @@ -4485,6 +4545,26 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) CargoTransport:SetPickupRadius(pickupouter, pickupinner) CargoTransport:SetDeployRadius(deployouter, deployinner) + + -- Adjust carrier units. This has to come AFTER the dispatchers have been defined because they set the cargobay free weight! + Request.carriercargo={} + for _,carriergroup in pairs(TransportGroupSet:GetSetObjects()) do + local asset=self:FindAssetInDB(carriergroup) + for _i,_carrierunit in pairs(carriergroup:GetUnits()) do + local carrierunit=_carrierunit --Wrapper.Unit#UNIT + + -- Create empty tables which will be filled with the cargo groups of each carrier unit. Needed in case a carrier unit dies. + Request.carriercargo[carrierunit:GetName()]={} + + -- Adjust cargo bay of carrier unit. + local cargobay=asset.cargobay[_i] + carrierunit:SetCargoBayWeightLimit(cargobay) + + -- Debug info. + self:T2(self.lid..string.format("Cargo bay weight limit of carrier unit %s: %.1f kg.", carrierunit:GetName(), carrierunit:GetCargoBayFreeWeight())) + end + end + -------------------------------- -- Dispatcher Event Functions -- -------------------------------- @@ -4497,7 +4577,7 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Debug message. local text=string.format("Carrier group %s picked up at pickup zone %s.", Carrier:GetName(), PickupZone:GetName()) - warehouse:T(warehouse.wid..text) + warehouse:T(warehouse.lid..text) end @@ -4510,7 +4590,7 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Debug message. -- TODO: Depoloy zone is nil! --local text=string.format("Carrier group %s deployed at deploy zone %s.", Carrier:GetName(), DeployZone:GetName()) - --warehouse:T(warehouse.wid..text) + --warehouse:T(warehouse.lid..text) end @@ -4522,7 +4602,7 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Debug message. local text=string.format("Carrier group %s going home to zone %s.", Carrier:GetName(), HomeZone:GetName()) - warehouse:T(warehouse.wid..text) + warehouse:T(warehouse.lid..text) end @@ -4534,7 +4614,7 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Debug message. local text=string.format("Carrier group %s loaded cargo %s into unit %s in pickup zone %s", Carrier:GetName(), Cargo:GetName(), CarrierUnit:GetName(), PickupZone:GetName()) - warehouse:T(warehouse.wid..text) + warehouse:T(warehouse.lid..text) -- Get cargo group object. local group=Cargo:GetObject() --Wrapper.Group#GROUP @@ -4558,7 +4638,7 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Debug message. local text=string.format("Cargo group %s was unloaded from carrier unit %s.", tostring(group:GetName()), tostring(CarrierUnit:GetName())) - warehouse:T(warehouse.wid..text) + warehouse:T(warehouse.lid..text) -- Load the cargo in the warehouse. --Cargo:Load(warehouse.warehouse) @@ -4580,7 +4660,7 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Debug info. local text=string.format("Carrier %s is back home at warehouse %s.", tostring(Carrier:GetName()), tostring(warehouse.warehouse:GetName())) MESSAGE:New(text, 5):ToAllIf(warehouse.Debug) - warehouse:I(warehouse.wid..text) + warehouse:I(warehouse.lid..text) -- Call arrived event for carrier. warehouse:__Arrived(1, Carrier) @@ -4631,10 +4711,32 @@ function WAREHOUSE:onafterUnloaded(From, Event, To, group) end else - self:E(self.wid..string.format("ERROR unloaded Cargo group is not alive!")) + self:E(self.lid..string.format("ERROR unloaded Cargo group is not alive!")) end end +--- On before "Arrived" event. Triggered when a group has arrived at its destination warehouse. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group that was delivered. +function WAREHOUSE:onbeforeArrived(From, Event, To, group) + + local asset=self:FindAssetInDB(group) + + if asset then + if asset.arrived==true then + -- Asset already arrived (e.g. if multiple units trigger the event via landing). + return false + else + asset.arrived=true --ensure this is not called again from the same asset group. + return true + end + end + +end + --- On after "Arrived" event. Triggered when a group has arrived at its destination warehouse. -- The routine should be called by the warehouse sending this asset and not by the receiving warehouse. -- It is checked if this asset is cargo (or self propelled) or transport. If it is cargo it is put into the stock of receiving warehouse. @@ -4665,7 +4767,7 @@ function WAREHOUSE:onafterArrived(From, Event, To, group) elseif istransport==false then warehouse=request.warehouse else - self:E(self.wid..string.format("ERROR: Group %s is neither cargo nor transport", group:GetName())) + self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport", group:GetName())) return end @@ -4676,21 +4778,22 @@ function WAREHOUSE:onafterArrived(From, Event, To, group) if group:IsGround() and group:GetSpeedMax()>1 then group:RouteGroundTo(warehouse:GetCoordinate(), group:GetSpeedMax()*0.3, "Off Road") end - - -- Increase number of cargo delivered and transports home. - local istransport=warehouse:_GroupIsTransport(group,request) + + -- NOTE: This is done in the AddAsset() function. Dont know, why we do it also here. + --[[ if istransport==true then request.ntransporthome=request.ntransporthome+1 request.transportgroupset:Remove(group:GetName(), true) - self:T2(warehouse.wid..string.format("Transport %d of %s returned home.", request.ntransporthome, tostring(request.ntransport))) + self:T2(warehouse.lid..string.format("Transport %d of %s returned home.", request.ntransporthome, tostring(request.ntransport))) elseif istransport==false then request.ndelivered=request.ndelivered+1 request.cargogroupset:Remove(self:_GetNameWithOut(group), true) - self:T2(warehouse.wid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset))) + self:T2(warehouse.lid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset))) else - self:E(warehouse.wid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) + self:E(warehouse.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) end - + ]] + -- Move asset from pending queue into new warehouse. warehouse:__AddAsset(60, group) end @@ -4796,7 +4899,7 @@ function WAREHOUSE:onafterAttacked(From, Event, To, Coalition, Country) self:_InfoMessage(text) else local text=string.format("Warehouse auto defence inactive.") - self:I(self.wid..text) + self:I(self.lid..text) end end @@ -4877,7 +4980,7 @@ function WAREHOUSE:onafterRespawn(From, Event, To) -- Respawn warehouse. self.warehouse:ReSpawn() - + end --- On after "ChangeCountry" event. Warehouse is respawned with the specified country. All queued requests are deleted and the owned airbase is reset if the coalition is changed by changing the @@ -4998,6 +5101,64 @@ function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition) end +--- On before "AssetSpawned" event. Checks whether the asset was already set to "spawned" for groups with multiple units. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group spawned. +-- @param #WAREHOUSE.Assetitem asset The asset that is dead. +-- @param #WAREHOUSE.Pendingitem request The request of the dead asset. +function WAREHOUSE:onbeforeAssetSpawned(From, Event, To, group, asset, request) + if asset.spawned then + --return false + else + --return true + end + + return true +end + +--- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group spawned. +-- @param #WAREHOUSE.Assetitem asset The asset that is dead. +-- @param #WAREHOUSE.Pendingitem request The request of the dead asset. +function WAREHOUSE:onafterAssetSpawned(From, Event, To, group, asset, request) + local text=string.format("Asset %s from request id=%d was spawned!", asset.spawngroupname, request.uid) + self:I(self.lid..text) + + -- Sete asset state to spawned. + asset.spawned=true + + -- Check if all assets groups are spawned and trigger events. + local n=0 + for _,_asset in pairs(request.assets) do + local assetitem=_asset --#WAREHOUSE.Assetitem + + -- Debug info. + self:T2(self.lid..string.format("Asset %s spawned %s as %s", assetitem.templatename, tostring(assetitem.spawned), tostring(assetitem.spawngroupname))) + + if assetitem.spawned then + n=n+1 + else + self:E(self.lid.."FF What?! This should not happen!") + end + + end + + -- Trigger event. + if n==request.nasset+request.ntransport then + self:T3(self.lid..string.format("All assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned. Calling RequestSpawned", n, request.nasset, request.ntransport, request.uid)) + self:RequestSpawned(request, request.cargogroupset, request.transportgroupset) + else + self:T3(self.lid..string.format("Not all assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned YET", n, request.nasset, request.ntransport, request.uid)) + end + +end --- On after "AssetDead" event triggered when an asset group died. -- @param #WAREHOUSE self @@ -5008,7 +5169,7 @@ end -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. function WAREHOUSE:onafterAssetDead(From, Event, To, asset, request) local text=string.format("Asset %s from request id=%d is dead!", asset.templatename, request.uid) - self:T(self.wid..text) + self:T(self.lid..text) self:_DebugMessage(text) end @@ -5025,24 +5186,36 @@ function WAREHOUSE:onafterDestroyed(From, Event, To) self:_InfoMessage(text) if self.respawnafterdestroyed then - + if self.respawndelay then self:Pause() self:__Respawn(self.respawndelay) else self:Respawn() end - + else - + -- Remove all table entries from waiting queue and stock. for k,_ in pairs(self.queue) do self.queue[k]=nil end + for k,_ in pairs(self.stock) do - self.stock[k]=nil + --self.stock[k]=nil end + for k=#self.stock,1,-1 do + --local asset=self.stock[k] --#WAREHOUSE.Assetitem + --self:AssetDead(asset, nil) + self.stock[k]=nil + end + + --self.queue=nil + --self.queue={} + + --self.stock=nil + --self.stock={} end end @@ -5074,7 +5247,7 @@ function WAREHOUSE:onafterSave(From, Event, To, path, filename) -- Info local text=string.format("Saving warehouse assets to file %s", filename) MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) - self:I(self.wid..text) + self:I(self.lid..text) local warehouseassets="" warehouseassets=warehouseassets..string.format("coalition=%d\n", self:GetCoalition()) @@ -5179,7 +5352,7 @@ function WAREHOUSE:onafterLoad(From, Event, To, path, filename) -- Info local text=string.format("Loading warehouse assets from file %s", filename) MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) - self:I(self.wid..text) + self:I(self.lid..text) -- Load asset data from file. local data=_loadfile(filename) @@ -5246,7 +5419,7 @@ function WAREHOUSE:onafterLoad(From, Event, To, path, filename) -- Respawn warehouse with prev coalition if necessary. if Country~=self:GetCountry() then - self:T(self.wid..string.format("Changing warehouse country %d-->%d on loading assets.", self:GetCountry(), Country)) + self:T(self.lid..string.format("Changing warehouse country %d-->%d on loading assets.", self:GetCountry(), Country)) self:ChangeCountry(Country) end @@ -5270,102 +5443,79 @@ end --- Spawns requested assets at warehouse or associated airbase. -- @param #WAREHOUSE self -- @param #WAREHOUSE.Queueitem Request Information table of the request. --- @return Core.Set#SET_GROUP Set of groups that were spawned. function WAREHOUSE:_SpawnAssetRequest(Request) self:F2({requestUID=Request.uid}) -- Shortcut to cargo assets. - local _assetstock=Request.cargoassets - - -- General type and category. - local _cargotype=Request.cargoattribute --#WAREHOUSE.Attribute - local _cargocategory=Request.cargocategory --DCS#Group.Category + local cargoassets=Request.cargoassets -- Now we try to find all parking spots for all cargo groups in advance. Due to the for loop, the parking spots do not get updated while spawning. local Parking={} - if _cargocategory==Group.Category.AIRPLANE or _cargocategory==Group.Category.HELICOPTER then - Parking=self:_FindParkingForAssets(self.airbase,_assetstock) or {} + if Request.cargocategory==Group.Category.AIRPLANE or Request.cargocategory==Group.Category.HELICOPTER then + Parking=self:_FindParkingForAssets(self.airbase, cargoassets) or {} end -- Spawn aircraft in uncontrolled state. local UnControlled=true - -- Create an empty group set. - local _groupset=SET_GROUP:New() - - -- Table for all spawned assets. - local _assets={} - -- Loop over cargo requests. - for i=1,#_assetstock do + for i=1,#cargoassets do -- Get stock item. - local _assetitem=_assetstock[i] --#WAREHOUSE.Assetitem + local asset=cargoassets[i] --#WAREHOUSE.Assetitem - -- Alias of the group. - local _alias=self:_Alias(_assetitem, Request) + -- Set asset status to not spawned until we capture its birth event. + asset.spawned=false + asset.iscargo=true + + -- Set request ID. + asset.rid=Request.uid + + -- Spawn group name. + local _alias=asset.spawngroupname + + --Request add asset by id. + Request.assets[asset.uid]=asset -- Spawn an asset group. local _group=nil --Wrapper.Group#GROUP - if _assetitem.category==Group.Category.GROUND then + if asset.category==Group.Category.GROUND then -- Spawn ground troops. - _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.spawnzone) + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone) - elseif _assetitem.category==Group.Category.AIRPLANE or _assetitem.category==Group.Category.HELICOPTER then + elseif asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then -- Spawn air units. - if Parking[_assetitem.uid] then - _group=self:_SpawnAssetAircraft(_alias,_assetitem, Request, Parking[_assetitem.uid], UnControlled) + if Parking[asset.uid] then + _group=self:_SpawnAssetAircraft(_alias, asset, Request, Parking[asset.uid], UnControlled) else - _group=self:_SpawnAssetAircraft(_alias,_assetitem, Request, nil, UnControlled) + _group=self:_SpawnAssetAircraft(_alias, asset, Request, nil, UnControlled) end - elseif _assetitem.category==Group.Category.TRAIN then + elseif asset.category==Group.Category.TRAIN then -- 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) + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone) end - --self:E(self.wid.."ERROR: Spawning of TRAIN assets not possible yet!") + --self:E(self.lid.."ERROR: Spawning of TRAIN assets not possible yet!") - elseif _assetitem.category==Group.Category.SHIP then + elseif asset.category==Group.Category.SHIP then -- Spawn naval assets. - _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.portzone) + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.portzone) else - self:E(self.wid.."ERROR: Unknown asset category!") - end - - -- Add group to group set and asset list. - if _group then - _groupset:AddGroup(_group) - table.insert(_assets, _assetitem) - - -- Call FSM function. - self:__AssetSpawned(1,_group,_assetitem, Request) - else - self:E(self.wid.."ERROR: Cargo asset could not be spawned!") + self:E(self.lid.."ERROR: Unknown asset category!") end end - -- Delete spawned items from warehouse stock. - for _,_asset in pairs(_assets) do - local asset=_asset --#WAREHOUSE.Assetitem - Request.assets[asset.uid]=asset - self:_DeleteStockItem(asset) - end - - -- Overwrite the assets with the actually spawned ones. - Request.cargoassets=_assets - - return _groupset end @@ -5417,7 +5567,7 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof end if asset.skill then unit.skill= asset.skill - end + end end @@ -5465,7 +5615,12 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then -- Get flight path if the group goes to another warehouse by itself. - template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase) + if request.toself then + local wp=self.airbase:GetCoordinate():WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, 0, false, self.airbase, {}, "Parking") + template.route.points={wp} + else + template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase) + end else @@ -5542,10 +5697,22 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol if asset.livery then unit.livery_id = asset.livery end + if asset.skill then unit.skill= asset.skill end + if asset.payload then + unit.payload=asset.payload.pylons + end + + if asset.modex then + unit.onboard_num=asset.modex[i] + end + if asset.callsign then + unit.callsign=asset.callsign[i] + end + end -- And template position. @@ -5568,9 +5735,6 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol -- Spawn group. local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP - -- Activate group - should only be necessary for late activated groups. - --group:Activate() - return group end @@ -5598,13 +5762,16 @@ function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) -- Nillify the group ID. template.groupId=nil - -- For group units, visible needs to be false. - if asset.category==Group.Category.GROUND then - --template.visible=false - end - -- No late activation. template.lateActivation=false + + if asset.missionTask then + self:I(self.lid..string.format("Setting mission task to %s", tostring(asset.missionTask))) + template.task=asset.missionTask + end + + -- No predefined task. + --template.taskSelected=false -- Set and empty route. template.route = {} @@ -5757,7 +5924,7 @@ function WAREHOUSE:_RouteNaval(group, request) else -- This should not happen! Existance of shipping lane was checked before executing this request. - self:E(self.wid..string.format("ERROR: No shipping lane defined for Naval asset!")) + self:E(self.lid..string.format("ERROR: No shipping lane defined for Naval asset!")) end end @@ -5773,14 +5940,15 @@ function WAREHOUSE:_RouteAir(aircraft) if aircraft and aircraft:IsAlive()~=nil then -- Debug info. - self:T2(self.wid..string.format("RouteAir aircraft group %s alive=%s", aircraft:GetName(), tostring(aircraft:IsAlive()))) + self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s", aircraft:GetName(), tostring(aircraft:IsAlive()))) -- Give start command to activate uncontrolled aircraft within the next 60 seconds. local starttime=math.random(60) + aircraft:StartUncontrolled(starttime) -- Debug info. - self:T2(self.wid..string.format("RouteAir aircraft group %s alive=%s (after start command)", aircraft:GetName(), tostring(aircraft:IsAlive()))) + self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s (after start command)", aircraft:GetName(), tostring(aircraft:IsAlive()))) -- Set ROE and alaram state. aircraft:OptionROEReturnFire() @@ -5835,8 +6003,8 @@ end -- @param Wrapper.Group#GROUP group The group that arrived. -- @param #number n Waypoint passed. -- @param #number N Final waypoint. -function WAREHOUSE:_PassingWaypoint(group,n,N) - self:T(self.wid..string.format("Group %s passing waypoint %d of %d!", tostring(group:GetName()), n, N)) +function WAREHOUSE:_PassingWaypoint(group, n, N) + self:T(self.lid..string.format("Group %s passing waypoint %d of %d!", tostring(group:GetName()), n, N)) -- Final waypoint reached. if n==N then @@ -5849,21 +6017,115 @@ end -- Event handler functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get a warehouse asset from its unique id. +-- @param #WAREHOUSE self +-- @param #number id Asset ID. +-- @return #WAREHOUSE.Assetitem The warehouse asset. +function WAREHOUSE:GetAssetByID(id) + if id then + return _WAREHOUSEDB.Assets[id] + else + return nil + end +end + +--- Get a warehouse asset from its name. +-- @param #WAREHOUSE self +-- @param #string GroupName Spawn group name. +-- @return #WAREHOUSE.Assetitem The warehouse asset. +function WAREHOUSE:GetAssetByName(GroupName) + + local name=self:_GetNameWithOut(GroupName) + local _,aid,_=self:_GetIDsFromGroup(GROUP:FindByName(name)) + + if aid then + return _WAREHOUSEDB.Assets[aid] + else + return nil + end +end + +--- Get a warehouse request from its unique id. +-- @param #WAREHOUSE self +-- @param #number id Request ID. +-- @return #WAREHOUSE.Pendingitem The warehouse requested - either queued or pending. +-- @return #boolean If *true*, request is queued, if *false*, request is pending, if *nil*, request could not be found. +function WAREHOUSE:GetRequestByID(id) + + if id then + + for _,_request in pairs(self.queue) do + local request=_request --#WAREHOUSE.Queueitem + if request.uid==id then + return request, true + end + end + + for _,_request in pairs(self.pending) do + local request=_request --#WAREHOUSE.Pendingitem + if request.uid==id then + return request, false + end + end + + end + + return nil,nil +end + --- Warehouse event function, handling the birth of a unit. -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventBirth(EventData) - self:T3(self.wid..string.format("Warehouse %s (id=%s) captured event birth!", self.alias, self.uid)) + self:T3(self.lid..string.format("Warehouse %s (id=%s) captured event birth!", self.alias, self.uid)) if EventData and EventData.IniGroup then local group=EventData.IniGroup + -- Note: Remember, group:IsAlive might(?) not return true here. local wid,aid,rid=self:_GetIDsFromGroup(group) + if wid==self.uid then - self:T(self.wid..string.format("Warehouse %s captured event birth of its asset unit %s.", self.alias, EventData.IniUnitName)) + + -- Get asset and request from id. + local asset=self:GetAssetByID(aid) + local request=self:GetRequestByID(rid) + + -- Debug message. + self:T(self.lid..string.format("Warehouse %s captured event birth of its asset unit %s. spawned=%s", self.alias, EventData.IniUnitName, tostring(asset.spawned))) + + -- Birth is triggered for each unit. We need to make sure not to call this too often! + if not asset.spawned then + + -- Remove asset from stock. + self:_DeleteStockItem(asset) + + -- Set spawned switch. + asset.spawned=true + asset.spawngroupname=group:GetName() + + -- Add group. + if asset.iscargo==true then + request.cargogroupset=request.cargogroupset or SET_GROUP:New() + request.cargogroupset:AddGroup(group) + else + request.transportgroupset=request.transportgroupset or SET_GROUP:New() + request.transportgroupset:AddGroup(group) + end + + -- Set warehouse state. + group:SetState(group, "WAREHOUSE", self) + + -- Asset spawned FSM function. + --self:__AssetSpawned(1, group, asset, request) + self:AssetSpawned(group, asset, request) + + end + else --self:T3({wid=wid, uid=self.uid, match=(wid==self.uid), tw=type(wid), tu=type(self.uid)}) end + end end @@ -5873,13 +6135,13 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventEngineStartup(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event engine startup!",self.alias)) + self:T3(self.lid..string.format("Warehouse %s captured event engine startup!",self.alias)) if EventData and EventData.IniGroup then local group=EventData.IniGroup local wid,aid,rid=self:_GetIDsFromGroup(group) if wid==self.uid then - self:T(self.wid..string.format("Warehouse %s captured event engine startup of its asset unit %s.", self.alias, EventData.IniUnitName)) + self:T(self.lid..string.format("Warehouse %s captured event engine startup of its asset unit %s.", self.alias, EventData.IniUnitName)) end end end @@ -5890,13 +6152,13 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventTakeOff(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event takeoff!",self.alias)) + self:T3(self.lid..string.format("Warehouse %s captured event takeoff!",self.alias)) if EventData and EventData.IniGroup then local group=EventData.IniGroup local wid,aid,rid=self:_GetIDsFromGroup(group) if wid==self.uid then - self:T(self.wid..string.format("Warehouse %s captured event takeoff of its asset unit %s.", self.alias, EventData.IniUnitName)) + self:T(self.lid..string.format("Warehouse %s captured event takeoff of its asset unit %s.", self.alias, EventData.IniUnitName)) end end end @@ -5907,7 +6169,7 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventLanding(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event landing!", self.alias)) + self:T3(self.lid..string.format("Warehouse %s captured event landing!", self.alias)) if EventData and EventData.IniGroup then local group=EventData.IniGroup @@ -5919,7 +6181,7 @@ function WAREHOUSE:_OnEventLanding(EventData) if wid~=nil and wid==self.uid then -- Debug info. - self:T(self.wid..string.format("Warehouse %s captured event landing of its asset unit %s.", self.alias, EventData.IniUnitName)) + self:T(self.lid..string.format("Warehouse %s captured event landing of its asset unit %s.", self.alias, EventData.IniUnitName)) end end @@ -5931,13 +6193,13 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventEngineShutdown(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event engine shutdown!", self.alias)) + self:T3(self.lid..string.format("Warehouse %s captured event engine shutdown!", self.alias)) if EventData and EventData.IniGroup then local group=EventData.IniGroup local wid,aid,rid=self:_GetIDsFromGroup(group) if wid==self.uid then - self:T(self.wid..string.format("Warehouse %s captured event engine shutdown of its asset unit %s.", self.alias, EventData.IniUnitName)) + self:T(self.lid..string.format("Warehouse %s captured event engine shutdown of its asset unit %s.", self.alias, EventData.IniUnitName)) end end end @@ -5970,28 +6232,34 @@ function WAREHOUSE:_OnEventArrived(EventData) if self.uid==wid then local request=self:_GetRequestOfGroup(group, self.pending) - + + -- Better check that the request still exists, because for a group with more units, the if request then - local istransport=self:_GroupIsTransport(group,request) - + + local istransport=self:_GroupIsTransport(group, request) + + -- Get closest airbase. + -- Note, this crashed at somepoint when the Tarawa was in the mission. Don't know why. Deleting the Tarawa and adding it again solved the problem. + local closest=group:GetCoordinate():GetClosestAirbase() + -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations. - local rightairbase=group:GetCoordinate():GetClosestAirbase():GetName()==request.warehouse:GetAirbase():GetName() - + local rightairbase=closest:GetName()==request.warehouse:GetAirbase():GetName() + -- Check that group is cargo and not transport. if istransport==false and rightairbase then - + -- Debug info. local text=string.format("Air asset group %s from warehouse %s arrived at its destination.", group:GetName(), self.alias) self:_InfoMessage(text) - + -- Trigger arrived event for this group. Note that each unit of a group will trigger this event. So the onafterArrived function needs to take care of that. -- Actually, we only take the first unit of the group that arrives. If it does, we assume the whole group arrived, which might not be the case, since -- some units might still be taxiing or whatever. Therefore, we add 10 seconds for each additional unit of the group until the first arrived event is triggered. local nunits=#group:GetUnits() local dt=10*(nunits-1)+1 -- one unit = 1 sec, two units = 11 sec, three units = 21 sec before we call the group arrived. self:__Arrived(dt, group) - end + end end @@ -6009,7 +6277,7 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventCrashOrDead(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event dead or crash!", self.alias)) + self:T3(self.lid..string.format("Warehouse %s captured event dead or crash!", self.alias)) if EventData then @@ -6024,7 +6292,7 @@ function WAREHOUSE:_OnEventCrashOrDead(EventData) end end - --self:I(self.wid..string.format("Warehouse %s captured event dead or crash or unit %s.", self.alias, tostring(EventData.IniUnitName))) + --self:I(self.lid..string.format("Warehouse %s captured event dead or crash or unit %s.", self.alias, tostring(EventData.IniUnitName))) -- Check if an asset unit was destroyed. if EventData.IniGroup then @@ -6039,7 +6307,7 @@ function WAREHOUSE:_OnEventCrashOrDead(EventData) if wid==self.uid then -- Debug message. - self:T(self.wid..string.format("Warehouse %s captured event dead or crash of its asset unit %s.", self.alias, EventData.IniUnitName)) + self:T(self.lid..string.format("Warehouse %s captured event dead or crash of its asset unit %s.", self.alias, EventData.IniUnitName)) -- Loop over all pending requests and get the one belonging to this unit. for _,request in pairs(self.pending) do @@ -6088,7 +6356,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Group is dead! if groupdead then - self:T(self.wid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request)))) + self:T(self.lid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request)))) if self.Debug then group:SmokeWhite() end @@ -6098,7 +6366,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) end - -- Not sure what this does actually and if it would be better to set it to true. + -- Dont trigger a Remove event for the group sets. local NoTriggerEvent=true if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then @@ -6110,7 +6378,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Remove dead group from cargo group set. if groupdead==true then request.cargogroupset:Remove(groupname, NoTriggerEvent) - self:T(self.wid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) + self:T(self.lid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) end else @@ -6134,7 +6402,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Loop over all groups inside the destroyed carrier ==> all dead. for _,cargoname in pairs(cargogroupnames) do request.cargogroupset:Remove(cargoname, NoTriggerEvent) - self:T(self.wid..string.format("Removed transported cargo %s inside dead carrier %s: ncargo=%d", cargoname, unitname, request.cargogroupset:Count())) + self:T(self.lid..string.format("Removed transported cargo %s inside dead carrier %s: ncargo=%d", cargoname, unitname, request.cargogroupset:Count())) end end @@ -6142,7 +6410,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Whole carrier group is dead. Remove it from the carrier group set. if groupdead then request.transportgroupset:Remove(groupname, NoTriggerEvent) - self:T(self.wid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) + self:T(self.lid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) end elseif istransport==false then @@ -6151,13 +6419,13 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Remove dead group from cargo group set. if groupdead==true then request.cargogroupset:Remove(groupname, NoTriggerEvent) - self:T(self.wid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count())) + self:T(self.lid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count())) -- This as well? --request.transportcargoset:RemoveCargosByName(RemoveCargoNames) end else - self:E(self.wid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) + self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) end end @@ -6170,7 +6438,7 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventBaseCaptured(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event base captured!",self.alias)) + self:T3(self.lid..string.format("Warehouse %s captured event base captured!",self.alias)) -- This warehouse does not have an airbase and never had one. So it could not have been captured. if self.airbasename==nil then @@ -6189,7 +6457,7 @@ function WAREHOUSE:_OnEventBaseCaptured(EventData) local NewCoalitionAirbase=airbase:GetCoalition() -- Debug info - self:T(self.wid..string.format("Airbase of warehouse %s (coalition ID=%d) was captured! New owner coalition ID=%d.",self.alias, self:GetCoalition(), NewCoalitionAirbase)) + self:T(self.lid..string.format("Airbase of warehouse %s (coalition ID=%d) was captured! New owner coalition ID=%d.",self.alias, self:GetCoalition(), NewCoalitionAirbase)) -- So what can happen? -- Warehouse is blue, airbase is blue and belongs to warehouse and red captures it ==> self.airbase=nil @@ -6215,7 +6483,7 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventMissionEnd(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event mission end!",self.alias)) + self:T3(self.lid..string.format("Warehouse %s captured event mission end!",self.alias)) if self.autosave then self:Save(self.autosavepath, self.autosavefile) @@ -6260,7 +6528,7 @@ function WAREHOUSE:_CheckConquered() local _country=unit:GetCountry() -- Debug info. - self:T2(self.wid..string.format("Unit %s in warehouse zone of radius=%d m. Coalition=%d, country=%d. Distance = %d m.",unit:GetName(), radius,_coalition,_country, distance)) + self:T2(self.lid..string.format("Unit %s in warehouse zone of radius=%d m. Coalition=%d, country=%d. Distance = %d m.",unit:GetName(), radius,_coalition,_country, distance)) -- Add up units for each side. if _coalition==coalition.side.BLUE then @@ -6279,7 +6547,7 @@ function WAREHOUSE:_CheckConquered() end -- Debug info. - self:T(self.wid..string.format("Ground troops in warehouse zone: blue=%d, red=%d, neutral=%d", Nblue, Nred, Nneutral)) + self:T(self.lid..string.format("Ground troops in warehouse zone: blue=%d, red=%d, neutral=%d", Nblue, Nred, Nneutral)) -- Figure out the new coalition if any. @@ -6371,7 +6639,7 @@ end -- @param #table queue The queue which is holding the requests to check. -- @return #boolean If true, request can be executed. If false, something is not right. function WAREHOUSE:_CheckRequestConsistancy(queue) - self:T3(self.wid..string.format("Number of queued requests = %d", #queue)) + self:T3(self.lid..string.format("Number of queued requests = %d", #queue)) -- Requests to delete. local invalid={} @@ -6380,47 +6648,47 @@ function WAREHOUSE:_CheckRequestConsistancy(queue) local request=_request --#WAREHOUSE.Queueitem -- Debug info. - self:T2(self.wid..string.format("Checking request id=%d.", request.uid)) + self:T2(self.lid..string.format("Checking request id=%d.", request.uid)) -- Let's assume everything is fine. local valid=true -- Check if at least one asset was requested. if request.nasset==0 then - self:E(self.wid..string.format("ERROR: INVALID request. Request for zero assets not possible. Can happen when, e.g. \"all\" ground assets are requests but none in stock.")) + self:E(self.lid..string.format("ERROR: INVALID request. Request for zero assets not possible. Can happen when, e.g. \"all\" ground assets are requests but none in stock.")) valid=false end -- Request from enemy coalition? if self:GetCoalition()~=request.warehouse:GetCoalition() then - self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is of wrong coaltion! Own coalition %s != %s of requesting warehouse.", self:GetCoalitionName(), request.warehouse:GetCoalitionName())) + self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is of wrong coaltion! Own coalition %s != %s of requesting warehouse.", self:GetCoalitionName(), request.warehouse:GetCoalitionName())) valid=false end -- Is receiving warehouse stopped? if request.warehouse:IsStopped() then - self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is stopped!")) + self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is stopped!")) valid=false end -- Is receiving warehouse destroyed? if request.warehouse:IsDestroyed() and not self.respawnafterdestroyed then - self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is destroyed!")) + self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is destroyed!")) valid=false end -- Add request as unvalid and delete it later. if valid==false then - self:E(self.wid..string.format("Got invalid request id=%d.", request.uid)) + self:E(self.lid..string.format("Got invalid request id=%d.", request.uid)) table.insert(invalid, request) else - self:T3(self.wid..string.format("Got valid request id=%d.", request.uid)) + self:T3(self.lid..string.format("Got valid request id=%d.", request.uid)) end end -- Delete invalid requests. for _,_request in pairs(invalid) do - self:E(self.wid..string.format("Deleting INVALID request id=%d.",_request.uid)) + self:E(self.lid..string.format("Deleting INVALID request id=%d.",_request.uid)) self:_DeleteQueueItem(_request, self.queue) end @@ -6513,8 +6781,8 @@ 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_dep=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + local termtype_des=asset.terminalType or self:_GetTerminal(asset.attribute, request.warehouse:GetAirbaseCategory()) -- Get number of parking spots. local np_departure=self.airbase:GetParkingSpotsNumber(termtype_dep) @@ -6658,7 +6926,7 @@ function WAREHOUSE:_CheckRequestValid(request) if request.transporttype==WAREHOUSE.TransportType.AIRPLANE or request.transporttype==WAREHOUSE.TransportType.HELICOPTER then -- Check if number of requested assets is in stock. - local _assets,_nassets,_enough=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype, request.ntransport) + local _assets,_nassets,_enough=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype, request.ntransport, true) -- Convert relative to absolute number if necessary. local nasset=request.ntransport @@ -6677,12 +6945,12 @@ function WAREHOUSE:_CheckRequestValid(request) local np_departure=self.airbase:GetParkingSpotsNumber(termtype) -- Debug info. - self:T(self.wid..string.format("Transport attribute = %s, terminal type = %d, spots at departure = %d.", request.transporttype, termtype, np_departure)) + self:T(self.lid..string.format("Transport attribute = %s, terminal type = %d, spots at departure = %d.", request.transporttype, termtype, np_departure)) -- 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(self.wid..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(self.lid..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 @@ -6694,7 +6962,7 @@ function WAREHOUSE:_CheckRequestValid(request) local np_destination=request.airbase:GetParkingSpotsNumber(termtype) -- Debug info. - self:T(self.wid..string.format("Transport attribute = %s: total # of spots (type=%d) at destination = %d.", asset.attribute, termtype, np_destination)) + self:T(self.lid..string.format("Transport attribute = %s: total # of spots (type=%d) at destination = %d.", asset.attribute, termtype, np_destination)) -- No parking at requesting warehouse. if np_destination == 0 then @@ -6710,9 +6978,9 @@ function WAREHOUSE:_CheckRequestValid(request) -- Add request as unvalid and delete it later. if valid==false then - self:E(self.wid..string.format("ERROR: Got invalid request id=%d.", request.uid)) + self:E(self.lid..string.format("ERROR: Got invalid request id=%d.", request.uid)) else - self:T3(self.wid..string.format("Request id=%d valid :)", request.uid)) + self:T3(self.lid..string.format("Request id=%d valid :)", request.uid)) end return valid @@ -6748,8 +7016,8 @@ function WAREHOUSE:_CheckRequestNow(request) if not _enough then local text=string.format("Warehouse %s: Request ID=%d denied! Not enough (cargo) assets currently available.", self.alias, request.uid) self:_InfoMessage(text, 5) - text=string.format("Enough=%s, #_assets=%d, _nassets=%d, request.nasset=%s", tostring(_enough), #_assets,_nassets, tostring(request.nasset)) - self:T(self.wid..text) + text=string.format("Enough=%s, #assets=%d, nassets=%d, request.nasset=%s", tostring(_enough), #_assets,_nassets, tostring(request.nasset)) + self:T(self.lid..text) return false end @@ -6852,7 +7120,7 @@ function WAREHOUSE:_CheckRequestNow(request) local asset=_asset --#WAREHOUSE.Assetitem text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d",_i, asset.templatename, asset.unittype, asset.category, asset.nunits) end - self:T(self.wid..text) + self:T(self.lid..text) if request.transporttype ~= WAREHOUSE.TransportType.SELFPROPELLED then @@ -6868,7 +7136,7 @@ function WAREHOUSE:_CheckRequestNow(request) local asset=_asset --#WAREHOUSE.Assetitem text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d\n",_i, asset.templatename, asset.unittype, asset.category, asset.nunits) end - self:T(self.wid..text) + self:T(self.lid..text) end @@ -6881,7 +7149,7 @@ end function WAREHOUSE:_GetTransportsForAssets(request) -- Get all transports of the requested type in stock. - local transports=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype) + local transports=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype, nil, true) -- Copy asset. local cargoassets=UTILS.DeepCopy(request.cargoassets) @@ -6906,26 +7174,26 @@ function WAREHOUSE:_GetTransportsForAssets(request) table.sort(cargoassets, sort_cargoassets) -- Total cargo bay size of all groups. - self:T2(self.wid.."Transport capability:") + self:T2(self.lid.."Transport capability:") local totalbay=0 for i=1,#transports do local transport=transports[i] --#WAREHOUSE.Assetitem for j=1,transport.nunits do totalbay=totalbay+transport.cargobay[j] - self:T2(self.wid..string.format("Cargo bay = %d (unit=%d)", transport.cargobay[j], j)) + self:T2(self.lid..string.format("Cargo bay = %d (unit=%d)", transport.cargobay[j], j)) end end - self:T2(self.wid..string.format("Total capacity = %d", totalbay)) + self:T2(self.lid..string.format("Total capacity = %d", totalbay)) -- Total cargo weight of all assets to transports. - self:T2(self.wid.."Cargo weight:") + self:T2(self.lid.."Cargo weight:") local totalcargoweight=0 for i=1,#cargoassets do local asset=cargoassets[i] --#WAREHOUSE.Assetitem totalcargoweight=totalcargoweight+asset.weight - self:T2(self.wid..string.format("weight = %d", asset.weight)) + self:T2(self.lid..string.format("weight = %d", asset.weight)) end - self:T2(self.wid..string.format("Total weight = %d", totalcargoweight)) + self:T2(self.lid..string.format("Total weight = %d", totalcargoweight)) -- Transports used. local used_transports={} @@ -6956,13 +7224,13 @@ function WAREHOUSE:_GetTransportsForAssets(request) local delta=cargobay-asset.weight --env.info(string.format("k=%d, j=%d delta=%d cargobay=%d weight=%d", k, j, delta, cargobay, asset.weight)) - --self:E(self.wid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) + --self:E(self.lid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) -- Cargo fits into carrier if delta>=0 then -- Reduce remaining cargobay. cargobay=cargobay-asset.weight - self:T3(self.wid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) + self:T3(self.lid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) -- Remember this cargo and remove it so it does not get loaded into other carriers. table.insert(putintocarrier, j) @@ -6970,7 +7238,7 @@ function WAREHOUSE:_GetTransportsForAssets(request) -- This transport group is used. used=true else - self:T2(self.wid..string.format("Carrier unit %s too small for cargo asset %s ==> cannot be used! Cargo bay - asset weight = %d kg", transport.templatename, asset.templatename, delta)) + self:T2(self.lid..string.format("Carrier unit %s too small for cargo asset %s ==> cannot be used! Cargo bay - asset weight = %d kg", transport.templatename, asset.templatename, delta)) end end -- loop over assets @@ -6986,7 +7254,7 @@ function WAREHOUSE:_GetTransportsForAssets(request) -- TODO: This might need to be improved but is working okay so far. if cargo then -- Remove this group because it was used. - self:T2(self.wid..string.format("Cargo id=%d assigned for carrier id=%d", cargo.uid, transport.uid)) + self:T2(self.lid..string.format("Cargo id=%d assigned for carrier id=%d", cargo.uid, transport.uid)) table.remove(cargoassets, nput) end end @@ -7053,7 +7321,7 @@ function WAREHOUSE:_QuantityRel2Abs(relative, ntot) nabs=relative end - self:T2(self.wid..string.format("Relative %s: tot=%d, abs=%.2f", tostring(relative), ntot, nabs)) + self:T2(self.lid..string.format("Relative %s: tot=%d, abs=%.2f", tostring(relative), ntot, nabs)) return nabs end @@ -7096,7 +7364,7 @@ function WAREHOUSE:_CheckQueue() -- Delete invalid requests. for _,_request in pairs(invalid) do - self:T(self.wid..string.format("Deleting invalid request id=%d.",_request.uid)) + self:T(self.lid..string.format("Deleting invalid request id=%d.",_request.uid)) self:_DeleteQueueItem(_request, self.queue) end @@ -7222,6 +7490,28 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) return safe end + -- Get client coordinates. + local function _clients() + local clients=_DATABASE.CLIENTS + local coords={} + for clientname, client in pairs(clients) do + local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) + local units=template.units + for i,unit in pairs(units) do + local coord=COORDINATE:New(unit.x, unit.alt, unit.y) + coords[unit.name]=coord + --[[ + local airbase=coord:GetClosestAirbase() + local _,TermID, dist, spot=coord:GetClosestParkingSpot(airbase) + if dist<=10 then + env.info(string.format("Found client %s on parking spot %d at airbase %s", unit.name, TermID, airbase:GetName())) + end + ]] + end + end + return coords + end + -- Get parking spot data table. This contains all free and "non-free" spots. local parkingdata=airbase:GetParkingSpotsTable() @@ -7249,15 +7539,10 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) end -- Check all clients. - --[[ - for _,_unit in pairs(_units) do - local unit=_unit --Wrapper.Unit#UNIT - local _coord=unit:GetCoordinate() - local _size=self:_GetObjectSize(unit:GetDCSObject()) - local _name=unit:GetName() - table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="client"}) + local clientcoords=_clients() + for clientname,_coord in pairs(clientcoords) do + table.insert(obstacles, {coord=_coord, size=15, name=clientname, type="client"}) end - ]] -- Check all statics. for _,static in pairs(_statics) do @@ -7287,7 +7572,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=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) -- Asset specific parking. parking[_asset.uid]={} @@ -7301,7 +7586,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot -- Check correct terminal type for asset. We don't want helos in shelters etc. - if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) then + if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) and self:_CheckParkingValid(parkingspot) then -- Coordinate of the parking spot. local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE @@ -7314,9 +7599,10 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local problem=nil -- Safe parking using TO_AC from DCS result. + self:I(self.lid..string.format("Parking spot %d TOAC=%s (safe park=%s).", _termid, tostring(_toac), tostring(self.safeparking))) if self.safeparking and _toac then free=false - self:T("Parking spot %d is occupied by other aircraft taking off or landing.", _termid) + self:I(self.lid..string.format("Parking spot %d is occupied by other aircraft taking off (TOAC).", _termid)) end -- Loop over all obstacles. @@ -7345,7 +7631,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Add parkingspot for this asset unit. table.insert(parking[_asset.uid], parkingspot) - self:T(self.wid..string.format("Parking spot #%d is free for asset id=%d!", _termid, _asset.uid)) + self:I(self.lid..string.format("Parking spot %d is free for asset id=%d!", _termid, _asset.uid)) -- Add the unit as obstacle so that this spot will not be available for the next unit. table.insert(obstacles, {coord=_spot, size=_asset.size, name=_asset.templatename, type="asset"}) @@ -7356,7 +7642,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) else -- Debug output for occupied spots. - self:T(self.wid..string.format("Parking spot #%d is occupied or not big enough!", _termid)) + self:I(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) if self.Debug then local coord=problem.coord --Core.Point#COORDINATE local text=string.format("Obstacle blocking spot #%d is %s type %s with size=%.1f m and distance=%.1f m.", _termid, problem.name, problem.type, problem.size, problem.dist) @@ -7370,7 +7656,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- No parking spot for at least one asset :( if not gotit then - self:T(self.wid..string.format("WARNING: No free parking spot for asset id=%d",_asset.uid)) + self:I(self.lid..string.format("WARNING: No free parking spot for asset id=%d",_asset.uid)) return nil end end -- loop over asset units @@ -7407,25 +7693,32 @@ end -- @return #boolean True if group is transport, false if group is cargo and nil otherwise. function WAREHOUSE:_GroupIsTransport(group, request) - -- Name of the group under question. - local groupname=self:_GetNameWithOut(group) + local asset=self:FindAssetInDB(group) + + if asset and asset.iscargo~=nil then + return not asset.iscargo + else - if request.transportgroupset then - local transporters=request.transportgroupset:GetSetObjects() - - for _,transport in pairs(transporters) do - if transport:GetName()==groupname then - return true + -- Name of the group under question. + local groupname=self:_GetNameWithOut(group) + + if request.transportgroupset then + local transporters=request.transportgroupset:GetSetObjects() + + for _,transport in pairs(transporters) do + if transport:GetName()==groupname then + return true + end end end - end - - if request.cargogroupset then - local cargos=request.cargogroupset:GetSetObjects() - - for _,cargo in pairs(cargos) do - if self:_GetNameWithOut(cargo)==groupname then - return false + + if request.cargogroupset then + local cargos=request.cargogroupset:GetSetObjects() + + for _,cargo in pairs(cargos) do + if self:_GetNameWithOut(cargo)==groupname then + return false + end end end end @@ -7434,54 +7727,21 @@ function WAREHOUSE:_GroupIsTransport(group, request) end ---- Creates a unique name for spawned assets. From the group name the original warehouse, global asset and the request can be derived. --- @param #WAREHOUSE self --- @param #WAREHOUSE.Assetitem _assetitem Asset for which the name is created. --- @param #WAREHOUSE.Queueitem _queueitem (Optional) Request specific name. --- @return #string Alias name "UnitType\_WID-%d\_AID-%d\_RID-%d" -function WAREHOUSE:_Alias(_assetitem,_queueitem) - return self:_alias(_assetitem.unittype, self.uid, _assetitem.uid,_queueitem.uid) -end - ---- Creates a unique name for spawned assets. From the group name the original warehouse, global asset and the request can be derived. --- @param #WAREHOUSE self --- @param #string unittype Type of unit. --- @param #number wid Warehouse id. --- @param #number aid Asset item id. --- @param #number qid Queue/request item id. --- @return #string Alias name "UnitType\_WID-%d\_AID-%d\_RID-%d" -function WAREHOUSE:_alias(unittype, wid, aid, qid) - local _alias=string.format("%s_WID-%d_AID-%d", unittype, wid, aid) - if qid then - _alias=_alias..string.format("_RID-%d", qid) - end - return _alias -end - --- Get group name without any spawn or cargo suffix #CARGO etc. -- @param #WAREHOUSE self -- @param Wrapper.Group#GROUP group The group from which the info is gathered. -- @return #string Name of the object without trailing #... function WAREHOUSE:_GetNameWithOut(group) - if group then - local name - if type(group)=="string" then - name=group - else - name=group:GetName() - end - local namewithout=UTILS.Split(name, "#")[1] - if namewithout then - return namewithout - else - return name - end - end - if type(group)=="string" then - return group + + local groupname=type(group)=="string" and group or group:GetName() + + if groupname:find("CARGO") then + local name=groupname:gsub("#CARGO", "") + return name else - return group:GetName() + return groupname end + end @@ -7527,14 +7787,23 @@ function WAREHOUSE:_GetIDsFromGroup(group) -- Group name local name=group:GetName() - -- Get ids + -- Get asset id from group name. local wid,aid,rid=analyse(name) + -- Get Asset. + local asset=self:GetAssetByID(aid) + + -- Get warehouse and request id from asset table. + if asset then + wid=asset.wid + rid=asset.rid + end + -- Debug info - self:T3(self.wid..string.format("Group Name = %s", tostring(name))) - self:T3(self.wid..string.format("Warehouse ID = %s", tostring(wid))) - self:T3(self.wid..string.format("Asset ID = %s", tostring(aid))) - self:T3(self.wid..string.format("Request ID = %s", tostring(rid))) + self:T(self.lid..string.format("Group Name = %s", tostring(name))) + self:T(self.lid..string.format("Warehouse ID = %s", tostring(wid))) + self:T(self.lid..string.format("Asset ID = %s", tostring(aid))) + self:T(self.lid..string.format("Request ID = %s", tostring(rid))) return wid,aid,rid else @@ -7543,6 +7812,78 @@ function WAREHOUSE:_GetIDsFromGroup(group) end + +--- Get warehouse id, asset id and request id from group name (alias). +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @return #number Warehouse ID. +-- @return #number Asset ID. +-- @return #number Request ID. +function WAREHOUSE:_GetIDsFromGroupOLD(group) + + ---@param #string text The text to analyse. + local function analyse(text) + + -- Get rid of #0001 tail from spawn. + local unspawned=UTILS.Split(text, "#")[1] + + -- Split keywords. + local keywords=UTILS.Split(unspawned, "_") + local _wid=nil -- warehouse UID + local _aid=nil -- asset UID + local _rid=nil -- request UID + + -- Loop over keys. + for _,keys in pairs(keywords) do + local str=UTILS.Split(keys, "-") + local key=str[1] + local val=str[2] + if key:find("WID") then + _wid=tonumber(val) + elseif key:find("AID") then + _aid=tonumber(val) + elseif key:find("RID") then + _rid=tonumber(val) + end + end + + return _wid,_aid,_rid + end + + if group then + + -- Group name + local name=group:GetName() + + -- Get ids + local wid,aid,rid=analyse(name) + + -- Debug info + self:T3(self.lid..string.format("Group Name = %s", tostring(name))) + self:T3(self.lid..string.format("Warehouse ID = %s", tostring(wid))) + self:T3(self.lid..string.format("Asset ID = %s", tostring(aid))) + self:T3(self.lid..string.format("Request ID = %s", tostring(rid))) + + return wid,aid,rid + else + self:E("WARNING: Group not found in GetIDsFromGroup() function!") + end + +end + +--- Filter stock assets by descriptor and attribute. +-- @param #WAREHOUSE self +-- @param #string descriptor Descriptor describing the filtered assets. +-- @param attribute Value of the descriptor. +-- @param #number nmax (Optional) Maximum number of items that will be returned. Default nmax=nil is all matching items are returned. +-- @param #boolean mobile (Optional) If true, filter only mobile assets. +-- @return #table Filtered assets in stock with the specified descriptor value. +-- @return #number Total number of (requested) assets available. +-- @return #boolean If true, enough assets are available. +function WAREHOUSE:FilterStock(descriptor, attribute, nmax, mobile) + return self:_FilterStock(self.stock, descriptor, attribute, nmax, mobile) +end + --- Filter stock assets by table entry. -- @param #WAREHOUSE self -- @param #table stock Table holding all assets in stock of the warehouse. Each entry is of type @{#WAREHOUSE.Assetitem}. @@ -7563,6 +7904,25 @@ function WAREHOUSE:_FilterStock(stock, descriptor, attribute, nmax, mobile) -- Filtered array. local filtered={} + + -- A specific list of assets was required. + if descriptor==WAREHOUSE.Descriptor.ASSETLIST then + + -- Count total number in stock. + local ntot=0 + for _,_rasset in pairs(attribute) do + local rasset=_rasset --#WAREHOUSE.Assetitem + for _,_asset in ipairs(stock) do + local asset=_asset --#WAREHOUSE.Assetitem + if rasset.uid==asset.uid then + table.insert(filtered, asset) + break + end + end + end + + return filtered, #filtered, #filtered>=#attribute + end -- Count total number in stock. local ntot=0 @@ -7798,7 +8158,23 @@ function WAREHOUSE:_DeleteQueueItem(qitem, queue) for i=1,#queue do local _item=queue[i] --#WAREHOUSE.Queueitem if _item.uid==qitem.uid then - self:T(self.wid..string.format("Deleting queue item id=%d.", qitem.uid)) + self:T(self.lid..string.format("Deleting queue item id=%d.", qitem.uid)) + table.remove(queue,i) + break + end + end +end + +--- Delete item from queue. +-- @param #WAREHOUSE self +-- @param #number qitemID ID of queue item to be removed. +-- @param #table queue The queue from which the item should be deleted. +function WAREHOUSE:_DeleteQueueItemByID(qitemID, queue) + + for i=1,#queue do + local _item=queue[i] --#WAREHOUSE.Queueitem + if _item.uid==qitemID then + self:T(self.lid..string.format("Deleting queue item id=%d.", qitemID)) table.remove(queue,i) break end @@ -7828,20 +8204,20 @@ function WAREHOUSE:_CheckFuel() local group=_group --Wrapper.Group#GROUP if group and group:IsAlive() then - + -- Get min fuel of group. local fuel=group:GetFuelMin() -- Debug info. - self:T2(self.wid..string.format("Transport group %s min fuel state = %.2f", group:GetName(), fuel)) + self:T2(self.lid..string.format("Transport group %s min fuel state = %.2f", group:GetName(), fuel)) -- Check if fuel is below threshold for first time. if fuel0 then - local attribute=tostring(UTILS.Split(_attribute, "_")[2]) - text=text..string.format("%s=%d, ", attribute,_count) + -- Create a mark with the current assets in stock. + if self.markerid~=nil then + trigger.action.removeMark(self.markerid) end + + -- Get assets in stock. + local _data=self:GetStockInfo(self.stock) + + -- Text. + local text=string.format("Warehouse state: %s\nTotal assets in stock %d:\n", self:GetState(), #self.stock) + + for _attribute,_count in pairs(_data) do + if _count>0 then + local attribute=tostring(UTILS.Split(_attribute, "_")[2]) + text=text..string.format("%s=%d, ", attribute,_count) + end + end + + -- Create/update marker at warehouse in F10 map. + self.markerid=self:GetCoordinate():MarkToCoalition(text, self:GetCoalition(), true) + end - - -- Create/update marker at warehouse in F10 map. - self.markerid=self:GetCoordinate():MarkToCoalition(text, self:GetCoalition(), true) + end --- Display stock items of warehouse. @@ -8031,7 +8415,7 @@ end -- @param #table stock Table holding all assets in stock of the warehouse. Each entry is of type @{#WAREHOUSE.Assetitem}. function WAREHOUSE:_DisplayStockItems(stock) - local text=self.wid..string.format("Warehouse %s stock assets:", self.alias) + local text=self.lid..string.format("Warehouse %s stock assets:", self.alias) for _i,_stock in pairs(stock) do local mystock=_stock --#WAREHOUSE.Assetitem local name=mystock.templatename @@ -8077,7 +8461,7 @@ function WAREHOUSE:_InfoMessage(text, duration) if duration>0 then MESSAGE:New(text, duration):ToCoalitionIf(self:GetCoalition(), self.Debug or self.Report) end - self:I(self.wid..text) + self:I(self.lid..text) end @@ -8090,7 +8474,7 @@ function WAREHOUSE:_DebugMessage(text, duration) if duration>0 then MESSAGE:New(text, duration):ToAllIf(self.Debug) end - self:T(self.wid..text) + self:I(self.lid..text) end --- Error message. Message send to all (if duration > 0). Text self:E(text) added to DCS.log file. @@ -8102,7 +8486,7 @@ function WAREHOUSE:_ErrorMessage(text, duration) if duration>0 then MESSAGE:New(text, duration):ToAll() end - self:E(self.wid..text) + self:E(self.lid..text) end @@ -8373,7 +8757,7 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) text=text..string.format("FL max = %.3f km\n", FLmax/1000) text=text..string.format("Ceiling = %.3f km\n", ceiling/1000) text=text..string.format("Max range = %.3f km\n", Range/1000) - self:T(self.wid..text) + self:T(self.lid..text) -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. if d_cruise<0 then diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 7dbb616dd..0c34562c6 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -4,19 +4,20 @@ -- -- **Main Features:** -- --- * Wind direction and speed, --- * Visibility, --- * Cloud coverage, base and ceiling, --- * Temprature, --- * Pressure QNH/QFE, --- * Weather phenomena: rain, thunderstorm, fog, dust, --- * Active runway based on wind direction, --- * Tower frequencies, --- * More than 180 voice overs, --- * Airbase names pronounced in locale accent (russian, US, french, arabic), --- * Option to present information in imperial or metric units, --- * Runway length and airfield elevation (optional), --- * Frequencies/channels of nav aids (ILS, VOR, NDB, TACAN, PRMG, RSBN) (optional). +-- * Wind direction and speed +-- * Visibility +-- * Cloud coverage, base and ceiling +-- * Temprature +-- * Dew point (approximate as there is no relative humidity in DCS yet) +-- * Pressure QNH/QFE +-- * Weather phenomena: rain, thunderstorm, fog, dust +-- * Active runway based on wind direction +-- * Tower frequencies +-- * More than 180 voice overs +-- * Airbase names pronounced in locale accent (russian, US, french, arabic) +-- * Option to present information in imperial or metric units +-- * Runway length and airfield elevation (optional) +-- * Frequencies/channels of nav aids (ILS, VOR, NDB, TACAN, PRMG, RSBN) (optional) -- -- === -- @@ -84,6 +85,7 @@ -- @field #boolean altimeterQNH Report altimeter QNH. -- @field #boolean usemarker Use mark on the F10 map. -- @field #number markerid Numerical ID of the F10 map mark point. +-- @field #number relHumidity Relative humidity (used to approximately calculate the dew point). -- @extends Core.Fsm#FSM --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde @@ -244,9 +246,9 @@ -- ![Banner Image](..\Presentations\ATIS\ATIS_SoundFolder.png) -- -- **Note** that the default folder name is *ATIS Soundfiles/*. If you want to change it, you can use the @{#ATIS.SetSoundfilesPath}(*path*), where *path* is the path of the directory. This must end with a slash "/"! --- +-- -- # Marks on the F10 Map --- +-- -- You can place marks on the F10 map via the @{#ATIS.SetMapMarks}() function. These will contain info about the ATIS frequency, the currently active runway and some basic info about the weather (wind, pressure and temperature). -- -- # Examples @@ -318,6 +320,7 @@ ATIS = { altimeterQNH = nil, usemarker = nil, markerid = nil, + relHumidity = nil, } --- NATO alphabet. @@ -380,6 +383,7 @@ ATIS.RunwayM2T={ --- Sound files. -- @type ATIS.Sound -- @field #ATIS.Soundfile ActiveRunway +-- @field #ATIS.Soundfile AdviceOnInitial -- @field #ATIS.Soundfile Airport -- @field #ATIS.Soundfile Altimeter -- @field #ATIS.Soundfile At @@ -394,6 +398,7 @@ ATIS.RunwayM2T={ -- @field #ATIS.Soundfile Decimal -- @field #ATIS.Soundfile DegreesCelsius -- @field #ATIS.Soundfile DegreesFahrenheit +-- @field #ATIS.Soundfile DewPoint -- @field #ATIS.Soundfile Dust -- @field #ATIS.Soundfile Elevation -- @field #ATIS.Soundfile EndOfInformation @@ -448,6 +453,7 @@ ATIS.RunwayM2T={ -- @field #ATIS.Soundfile VORFrequency ATIS.Sound = { ActiveRunway={filename="ActiveRunway.ogg", duration=0.99}, + AdviceOnInitial={filename="AdviceOnInitial.ogg", duration=3.00}, Airport={filename="Airport.ogg", duration=0.66}, Altimeter={filename="Altimeter.ogg", duration=0.68}, At={filename="At.ogg", duration=0.41}, @@ -462,6 +468,7 @@ ATIS.Sound = { Decimal={filename="Decimal.ogg", duration=0.54}, DegreesCelsius={filename="DegreesCelsius.ogg", duration=1.27}, DegreesFahrenheit={filename="DegreesFahrenheit.ogg", duration=1.23}, + DewPoint={filename="DewPoint.ogg", duration=0.65}, Dust={filename="Dust.ogg", duration=0.54}, Elevation={filename="Elevation.ogg", duration=0.78}, EndOfInformation={filename="EndOfInformation.ogg", duration=1.15}, @@ -524,15 +531,19 @@ _ATIS={} --- ATIS class version. -- @field #string version -ATIS.version="0.6.4" +ATIS.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Add stop/pause FMS functions. +-- TODO: Add new Normany airfields. +-- TODO: Zulu time --> Zulu in output. -- TODO: Correct fog for elevation. --- TODO: Use local time. +-- DONE: Add text report for output. +-- DONE: Add stop FMS functions. +-- NOGO: Use local time. Not realisitc! +-- DONE: Dew point. Approx. done. -- DONE: Metric units. -- DONE: Set UTC correction. -- DONE: Set magnetic variation. @@ -545,7 +556,7 @@ ATIS.version="0.6.4" -- @param #ATIS self -- @param #string airbasename Name of the airbase. -- @param #number frequency Radio frequency in MHz. Default 143.00 MHz. --- @param #number modulation 0=AM, 1=FM. Default 0=AM. +-- @param #number modulation Radio modulation: 0=AM, 1=FM. Default 0=AM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators -- @return #ATIS self function ATIS:New(airbasename, frequency, modulation) @@ -557,6 +568,7 @@ function ATIS:New(airbasename, frequency, modulation) if self.airbase==nil then self:E("ERROR: Airbase %s for ATIS could not be found!", tostring(airbasename)) + return nil end -- Default freq and modulation. @@ -568,7 +580,7 @@ function ATIS:New(airbasename, frequency, modulation) -- Set some string id for output to DCS.log file. self.lid=string.format("ATIS %s | ", self.airbasename) - + -- This is just to hinder the garbage collector deallocating the ATIS object. _ATIS[#_ATIS+1]=self @@ -580,6 +592,7 @@ function ATIS:New(airbasename, frequency, modulation) self:SetRadioPower() self:SetAltimeterQNH(true) self:SetMapMarks(false) + self:SetRelativeHumidity() -- Start State. self:SetStartState("Stopped") @@ -587,9 +600,11 @@ function ATIS:New(airbasename, frequency, modulation) -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") -- Start FSM. - self:AddTransition("*", "Status", "*") -- Update status. - self:AddTransition("*", "Broadcast", "*") -- Broadcast ATIS message. - self:AddTransition("*", "CheckQueue", "*") -- Check if radio queue is empty. + self:AddTransition("*", "Status", "*") -- Update status. + self:AddTransition("*", "Broadcast", "*") -- Broadcast ATIS message. + self:AddTransition("*", "CheckQueue", "*") -- Check if radio queue is empty. + self:AddTransition("*", "Report", "*") -- Report ATIS text. + self:AddTransition("*", "Stop", "Stopped") -- Stop. ------------------------ --- Pseudo Functions --- @@ -604,7 +619,9 @@ function ATIS:New(airbasename, frequency, modulation) -- @param #ATIS self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Stop". Stops the ATIS. + -- @function [parent=#ATIS] Stop -- @param #ATIS self --- Triggers the FSM event "Stop" after a delay. @@ -612,6 +629,57 @@ function ATIS:New(airbasename, frequency, modulation) -- @param #ATIS self -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#ATIS] Status + -- @param #ATIS self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#ATIS] __Status + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Broadcast". + -- @function [parent=#ATIS] Broadcast + -- @param #ATIS self + + --- Triggers the FSM event "Broadcast" after a delay. + -- @function [parent=#ATIS] __Broadcast + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "CheckQueue". + -- @function [parent=#ATIS] CheckQueue + -- @param #ATIS self + + --- Triggers the FSM event "CheckQueue" after a delay. + -- @function [parent=#ATIS] __CheckQueue + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Report". + -- @function [parent=#ATIS] Report + -- @param #ATIS self + -- @param #string Text Report text. + + --- Triggers the FSM event "Report" after a delay. + -- @function [parent=#ATIS] __Report + -- @param #ATIS self + -- @param #number delay Delay in seconds. + -- @param #string Text Report text. + + --- On after "Report" event user function. + -- @function [parent=#ATIS] OnAfterReport + -- @param #ATIS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Text Report text. + + -- Debug trace. if false then self.Debug=true @@ -724,25 +792,25 @@ function ATIS:SetRunwayHeadingsMagnetic(headings) end for _,heading in pairs(headings) do - + if type(heading)=="number" then heading=string.format("%02d", heading) end - + -- Add runway heading to table. self:I(self.lid..string.format("Adding user specified magnetic runway heading %s", heading)) table.insert(self.runwaymag, heading) local h=self:GetRunwayWithoutLR(heading) - - local head2=tonumber(h)-18 + + local head2=tonumber(h)-18 if head2<0 then head2=head2+36 end - + -- Convert to string. head2=string.format("%02d", head2) - + -- Append "L" or "R" if necessary. local left=self:GetRunwayLR(heading) if left==true then @@ -801,18 +869,28 @@ function ATIS:SetTemperatureFahrenheit() return self end +--- Set relative humidity. This is used to approximately calculate the dew point. +-- Note that the dew point is only an artificial information as DCS does not have an atmospheric model that includes humidity (yet). +-- @param #ATIS self +-- @param #number Humidity Relative Humidity, i.e. a number between 0 and 100 %. Default is 50 %. +-- @return #ATIS self +function ATIS:SetRelativeHumidity(Humidity) + self.relHumidity=Humidity or 50 + return self +end + --- Report altimeter QNH. -- @param #ATIS self -- @param #boolean switch If true or nil, report altimeter QHN. If false, report QFF. -- @return #ATIS self function ATIS:SetAltimeterQNH(switch) - + if switch==true or switch==nil then self.altimeterQNH=true else self.altimeterQNH=false end - + return self end @@ -976,6 +1054,9 @@ end --- Start ATIS FSM. -- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. function ATIS:onafterStart(From, Event, To) -- Check that this is an airdrome. @@ -995,7 +1076,7 @@ function ATIS:onafterStart(From, Event, To) -- Set relay unit if we have one. self.radioqueue:SetSenderUnitName(self.relayunitname) - + -- Set radio power. self.radioqueue:SetRadioPower(self.power) @@ -1021,11 +1102,14 @@ end --- Update status. -- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. function ATIS:onafterStatus(From, Event, To) -- Get FSM state. local fsmstate=self:GetState() - + local relayunitstatus="N/A" if self.relayunitname then local ru=UNIT:FindByName(self.relayunitname) @@ -1047,6 +1131,9 @@ end --- Check if radio queue is empty. If so, start broadcasting the message again. -- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. function ATIS:onafterCheckQueue(From, Event, To) if #self.radioqueue.queue==0 then @@ -1062,6 +1149,9 @@ end --- Broadcast ATIS radio message. -- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. function ATIS:onafterBroadcast(From, Event, To) -- Get current coordinate. @@ -1077,32 +1167,32 @@ function ATIS:onafterBroadcast(From, Event, To) -- Pressure in hPa. local qfe=coord:GetPressure(height) local qnh=coord:GetPressure(0) - + if self.altimeterQNH then - + -- Some constants. local L=-0.0065 --[K/m] local R= 8.31446 --[J/mol/K] local g= 9.80665 --[m/s^2] local M= 0.0289644 --[kg/mol] local T0=coord:GetTemperature(0)+273.15 --[K] Temp at sea level. - local TS=288.15 -- Standard Temperature assumed by Altimeter is 15°C + local TS=288.15 -- Standard Temperature assumed by Altimeter is 15°C local q=qnh*100 - + -- Calculate Pressure. local P=q*(1+L*height/T0)^(-g*M/(R*L)) -- Pressure at sea level local Q=P/(1+L*height/TS)^(-g*M/(R*L)) -- Altimeter QNH local A=(T0/L)*((P/q)^(((-R*L)/(g*M)))-1) -- Altitude check - - + + -- Debug aoutput self:T2(self.lid..string.format("height=%.1f, A=%.1f, T0=%.1f, QFE=%.1f, QNH=%.1f, P=%.1f, Q=%.1f hPa = %.2f", height, A, T0-273.15, qfe, qnh, P/100, Q/100, UTILS.hPa2inHg(Q/100))) - + -- Set QNH value in hPa. qnh=Q/100 - + end - + -- Convert to inHg. if self.PmmHg then @@ -1143,7 +1233,7 @@ function ATIS:onafterBroadcast(From, Event, To) local WINDFROM=string.format("%03d", windFrom-magvar) local WINDSPEED=string.format("%d", UTILS.MpsToKnots(windSpeed)) - + if WINDFROM=="000" then WINDFROM="360" end @@ -1194,19 +1284,24 @@ function ATIS:onafterBroadcast(From, Event, To) self:T3(string.format("ZULU =%s", tostring(ZULU))) self:T3(string.format("NATO =%s", tostring(NATO))) - ------------------- - --- Temperature --- - ------------------- + --------------------------------- + --- Temperature and Dew Point --- + --------------------------------- - -- Temperature in °C (or °F). + -- Temperature in °C. local temperature=coord:GetTemperature(height+5) + + -- Dew point in °C. + local dewpoint=temperature-(100-self.relHumidity)/5 -- Convert to °F. if self.TDegF then temperature=UTILS.CelciusToFarenheit(temperature) + dewpoint=UTILS.CelciusToFarenheit(dewpoint) end local TEMPERATURE=string.format("%d", math.abs(temperature)) + local DEWPOINT=string.format("%d", math.abs(dewpoint)) --------------- --- Weather --- @@ -1325,17 +1420,20 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=subtitle.." Airport" end self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, self.subduration) + local alltext=subtitle -- Information tag subtitle=string.format("Information %s", NATO) local _INFORMATION=subtitle self:Transmission(ATIS.Sound.Information, 0.5, subtitle) self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + alltext=alltext..";\n"..subtitle -- Zulu Time - subtitle=string.format("%s Zulu Time", ZULU) + subtitle=string.format("%s Zulu", ZULU) self.radioqueue:Number2Transmission(ZULU, nil, 0.5) self:Transmission(ATIS.Sound.TimeZulu, 0.2, subtitle) + alltext=alltext..";\n"..subtitle -- Visibility if self.metric then @@ -1350,6 +1448,7 @@ function ATIS:onafterBroadcast(From, Event, To) else self:Transmission(ATIS.Sound.NauticalMiles, 0.2) end + alltext=alltext..";\n"..subtitle -- Cloud base self:Transmission(CloudCover, 1.0, CLOUDSsub) @@ -1385,6 +1484,7 @@ function ATIS:onafterBroadcast(From, Event, To) self:Transmission(ATIS.Sound.Feet, 0.1) end end + alltext=alltext..";\n"..subtitle -- Weather phenomena local wp=false @@ -1438,6 +1538,7 @@ function ATIS:onafterBroadcast(From, Event, To) if dust then self:Transmission(ATIS.Sound.Dust, 0.5) end + alltext=alltext..";\n"..subtitle end -- Altimeter QNH/QFE. @@ -1469,6 +1570,7 @@ function ATIS:onafterBroadcast(From, Event, To) self:Transmission(ATIS.Sound.InchesOfMercury, 0.1) end end + alltext=alltext..";\n"..subtitle -- Temperature if self.TDegF then @@ -1495,6 +1597,34 @@ function ATIS:onafterBroadcast(From, Event, To) else self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) end + alltext=alltext..";\n"..subtitle + + -- Dew point + if self.TDegF then + if dewpoint<0 then + subtitle=string.format("Dew point -%s °F", DEWPOINT) + else + subtitle=string.format("Dew point %s °F", DEWPOINT) + end + else + if dewpoint<0 then + subtitle=string.format("Dew point -%s °C", DEWPOINT) + else + subtitle=string.format("Dew point %s °C", DEWPOINT) + end + end + local _DEWPOINT=subtitle + self:Transmission(ATIS.Sound.DewPoint, 1.0, subtitle) + if dewpoint<0 then + self:Transmission(ATIS.Sound.Minus, 0.2) + end + self.radioqueue:Number2Transmission(DEWPOINT) + if self.TDegF then + self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + else + self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + end + alltext=alltext..";\n"..subtitle -- Wind if self.metric then @@ -1518,6 +1648,7 @@ function ATIS:onafterBroadcast(From, Event, To) if turbulence>0 then self:Transmission(ATIS.Sound.Gusting, 0.2) end + alltext=alltext..";\n"..subtitle -- Active runway. local subtitle=string.format("Active runway %s", runway) @@ -1534,6 +1665,7 @@ function ATIS:onafterBroadcast(From, Event, To) elseif rwyLeft==false then self:Transmission(ATIS.Sound.Right, 0.2) end + alltext=alltext..";\n"..subtitle -- Runway length. if self.rwylength then @@ -1571,8 +1703,9 @@ function ATIS:onafterBroadcast(From, Event, To) self:Transmission(ATIS.Sound.Feet, 0.1) end + alltext=alltext..";\n"..subtitle end - + -- Airfield elevation if self.elevation then @@ -1608,6 +1741,7 @@ function ATIS:onafterBroadcast(From, Event, To) self:Transmission(ATIS.Sound.Feet, 0.1) end + alltext=alltext..";\n"..subtitle end -- Tower frequency. @@ -1631,6 +1765,8 @@ function ATIS:onafterBroadcast(From, Event, To) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) end + + alltext=alltext..";\n"..subtitle end -- ILS @@ -1646,6 +1782,8 @@ function ATIS:onafterBroadcast(From, Event, To) self.radioqueue:Number2Transmission(f[2]) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) + + alltext=alltext..";\n"..subtitle end -- Outer NDB @@ -1661,6 +1799,8 @@ function ATIS:onafterBroadcast(From, Event, To) self.radioqueue:Number2Transmission(f[2]) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) + + alltext=alltext..";\n"..subtitle end -- Inner NDB @@ -1676,6 +1816,8 @@ function ATIS:onafterBroadcast(From, Event, To) self.radioqueue:Number2Transmission(f[2]) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) + + alltext=alltext..";\n"..subtitle end -- VOR @@ -1690,6 +1832,8 @@ function ATIS:onafterBroadcast(From, Event, To) self.radioqueue:Number2Transmission(f[2]) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) + + alltext=alltext..";\n"..subtitle end -- TACAN @@ -1698,6 +1842,8 @@ function ATIS:onafterBroadcast(From, Event, To) self:Transmission(ATIS.Sound.TACANChannel, 1.0, subtitle) self.radioqueue:Number2Transmission(tostring(self.tacan), nil, 0.2) self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2) + + alltext=alltext..";\n"..subtitle end -- RSBN @@ -1705,6 +1851,8 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=string.format("RSBN channel %d", self.rsbn) self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) + + alltext=alltext..";\n"..subtitle end -- PRMG @@ -1713,13 +1861,27 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=string.format("PRMG channel %d", ndb.frequency) self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) + + alltext=alltext..";\n"..subtitle end - - -- End of Information Alpha, Bravo, ... + + --[[ + -- End of Information Alpha, Bravo, ... subtitle=string.format("End of information %s", NATO) self:Transmission(ATIS.Sound.EndOfInformation, 0.5, subtitle) self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + --]] + -- Advice on initial... + subtitle=string.format("Advise on initial contact, you have information %s", NATO) + self:Transmission(ATIS.Sound.AdviceOnInitial, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + + alltext=alltext..";\n"..subtitle + + -- Report ATIS text. + self:Report(alltext) + -- Update F10 marker. if self.usemarker then self:UpdateMarker(_INFORMATION, _RUNACT, _WIND, _ALTIMETER, _TEMPERATURE) @@ -1727,6 +1889,20 @@ function ATIS:onafterBroadcast(From, Event, To) end +--- Text report of ATIS information. Information delimitor is a semicolon ";" and a line break "\n". +-- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Text Report text. +function ATIS:onafterReport(From, Event, To, Text) + self:T(self.lid..string.format("Report:\n%s", Text)) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Update F10 map marker. -- @param #ATIS self -- @param #string information Information tag text. @@ -1740,24 +1916,20 @@ function ATIS:UpdateMarker(information, runact, wind, altimeter, temperature) if self.markerid then self.airbase:GetCoordinate():RemoveMark(self.markerid) end - + local text=string.format("ATIS on %.3f %s, %s:\n", self.frequency, UTILS.GetModulationName(self.modulation), tostring(information)) text=text..string.format("%s\n", tostring(runact)) text=text..string.format("%s\n", tostring(wind)) text=text..string.format("%s\n", tostring(altimeter)) text=text..string.format("%s", tostring(temperature)) - -- More info is not displayed on the marker! - + -- More info is not displayed on the marker! + -- Place new mark self.markerid=self.airbase:GetCoordinate():MarkToAll(text, true) - + return self.markerid end -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Misc Functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- Get active runway runway. -- @param #ATIS self -- @return #string Active runway, e.g. "31" for 310 deg. @@ -1781,17 +1953,17 @@ function ATIS:GetActiveRunway() -- Check if user explicitly specified a runway. if self.activerunway then - + -- Get explicit runway heading if specified. local runwayno=self:GetRunwayWithoutLR(self.activerunway) if runwayno~="" then runway=runwayno end - + -- Was "L"eft or "R"ight given? rwyLeft=self:GetRunwayLR(self.activerunway) end - + return runway, rwyLeft end @@ -1804,7 +1976,7 @@ function ATIS:GetMagneticRunway(windfrom) local diffmin=nil local runway=nil for _,heading in pairs(self.runwaymag) do - + local hdg=self:GetRunwayWithoutLR(heading) local diff=UTILS.HdgDiff(windfrom, tonumber(hdg)*10) @@ -1829,18 +2001,18 @@ function ATIS:GetNavPoint(navpoints, runway, left) -- Loop over all defined nav aids. for _,_nav in pairs(navpoints or {}) do local nav=_nav --#ATIS.NavPoint - + if nav.runway==nil then -- No explicit runway data specified ==> data is valid for all runways. return nav else - + local navy=tonumber(self:GetRunwayWithoutLR(nav.runway))*10 local rwyy=tonumber(self:GetRunwayWithoutLR(runway))*10 - + local navL=self:GetRunwayLR(nav.runway) local hdgD=UTILS.HdgDiff(navy,rwyy) - + if hdgD<=15 then --We allow an error of +-15° here. if navL==nil or (navL==true and left==true) or (navL==false and left==false) then return nav @@ -1848,7 +2020,7 @@ function ATIS:GetNavPoint(navpoints, runway, left) end end end - + return nil end @@ -1871,7 +2043,7 @@ function ATIS:GetRunwayLR(runway) -- Get left/right if specified. local rwyL=runway:lower():find("l") local rwyR=runway:lower():find("r") - + if rwyL then return true elseif rwyR then @@ -1879,7 +2051,7 @@ function ATIS:GetRunwayLR(runway) else return nil end - + end --- Transmission via RADIOQUEUE. From 53fbec9c15e2115b11179bb774d290801aeea6b0 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 8 Apr 2020 00:09:47 +0200 Subject: [PATCH 457/485] events --- Moose Development/Moose/Core/Base.lua | 2 + Moose Development/Moose/Core/Event.lua | 55 ++++++++++++++++++++------ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index 381c6859f..319e42064 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -771,6 +771,8 @@ end function BASE:onEvent(event) --self:F( { BaseEventCodes[event.id], event } ) + env.info("FF BASE:onEvent id="..event.id) + if self then for EventID, EventObject in pairs( self.Events ) do if EventObject.EventEnabled then diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index eec351941..d86121b7a 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -555,6 +555,8 @@ function EVENT:Init( EventID, EventClass ) if not self.Events[EventID][EventPriority][EventClass] then self.Events[EventID][EventPriority][EventClass] = {} end + + return self.Events[EventID][EventPriority][EventClass] end @@ -641,7 +643,7 @@ end -- @param EventID -- @return #EVENT function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) - self:F2( { EventID } ) + self:F2( { EventID, EventClass, EventFunction } ) local EventData = self:Init( EventID, EventClass ) EventData.EventFunction = EventFunction @@ -960,13 +962,26 @@ function EVENT:onEvent( Event ) end if Event.IniObjectCategory == Object.Category.STATIC then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() + if Event.id==31 then + env.info("FF event 31") + -- Event.initiator is a Static object representing the pilot. But getName() error due to DCS bug. + Event.IniDCSUnit = Event.initiator + local ID=Event.initiator.id_ + Event.IniDCSUnitName = string.format("Ejected Pilot ID %s", tostring(ID)) + Event.IniUnitName = Event.IniDCSUnitName + Event.IniCoalition = 0 + Event.IniCategory = 0 + Event.IniUnit=STATIC:Register(Event.IniUnitName) + Event.IniTypeName = "Ejected Pilot" + else + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + end end if Event.IniObjectCategory == Object.Category.CARGO then @@ -1048,7 +1063,7 @@ function EVENT:onEvent( Event ) if Event.place then if Event.id==EVENTS.LandingAfterEjection then -- Place is here the UNIT of which the pilot ejected. - Event.Place=UNIT:Find(Event.place) + --Event.Place=UNIT:Find(Event.place) else Event.Place=AIRBASE:Find(Event.place) Event.PlaceName=Event.Place:GetName() @@ -1083,13 +1098,30 @@ function EVENT:onEvent( Event ) self:F( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) end + --self:E({Events=self.Events}) + --self:E({Events31=self.Events[31]}) + + env.info(string.format("FF %s all events:", EventMeta.Text)) + for id,data in pairs(self.Events) do + self:E({id=id, data=data}) + end + for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do + env.info("hallo 100 EventPriority="..EventPriority) + if self.Events[Event.id][EventPriority] then + env.info("hallo 200") + self:E({blub=EventMeta.Text, bla=self.Events[Event.id][EventPriority]}) + -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do + env.info("hallo 300") + self:E({eventclass=EventClass}) + self:E({eventdata=EventData}) + --if Event.IniObjectCategory ~= Object.Category.STATIC then -- self:E( { "Evaluating: ", EventClass:GetClassNameAndID() } ) --end @@ -1145,10 +1177,11 @@ function EVENT:onEvent( Event ) else -- The EventClass is not alive anymore, we remove it from the EventHandlers... self:RemoveEvent( EventClass, Event.id ) - end + end + else - -- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. + --- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. if EventData.EventGroup then -- So now the EventClass must be a GROUP class!!! We check if it is still "Alive". From cb4a44b512843cc32c6d62c46ddc9c3e1098db09 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 8 Apr 2020 01:10:43 +0200 Subject: [PATCH 458/485] Event --- Moose Development/Moose/Core/Base.lua | 2 -- Moose Development/Moose/Core/Event.lua | 23 +++-------------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index 319e42064..381c6859f 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -771,8 +771,6 @@ end function BASE:onEvent(event) --self:F( { BaseEventCodes[event.id], event } ) - env.info("FF BASE:onEvent id="..event.id) - if self then for EventID, EventObject in pairs( self.Events ) do if EventObject.EventEnabled then diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index d86121b7a..f34d3f393 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -963,7 +963,7 @@ function EVENT:onEvent( Event ) if Event.IniObjectCategory == Object.Category.STATIC then if Event.id==31 then - env.info("FF event 31") + --env.info("FF event 31") -- Event.initiator is a Static object representing the pilot. But getName() error due to DCS bug. Event.IniDCSUnit = Event.initiator local ID=Event.initiator.id_ @@ -971,7 +971,6 @@ function EVENT:onEvent( Event ) Event.IniUnitName = Event.IniDCSUnitName Event.IniCoalition = 0 Event.IniCategory = 0 - Event.IniUnit=STATIC:Register(Event.IniUnitName) Event.IniTypeName = "Ejected Pilot" else Event.IniDCSUnit = Event.initiator @@ -1063,6 +1062,7 @@ function EVENT:onEvent( Event ) if Event.place then if Event.id==EVENTS.LandingAfterEjection then -- Place is here the UNIT of which the pilot ejected. + --local name=Event.place:getName() -- This returns a DCS error "Airbase doesn't exit" :( --Event.Place=UNIT:Find(Event.place) else Event.Place=AIRBASE:Find(Event.place) @@ -1098,30 +1098,13 @@ function EVENT:onEvent( Event ) self:F( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) end - --self:E({Events=self.Events}) - --self:E({Events31=self.Events[31]}) - - env.info(string.format("FF %s all events:", EventMeta.Text)) - for id,data in pairs(self.Events) do - self:E({id=id, data=data}) - end - for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do - env.info("hallo 100 EventPriority="..EventPriority) - if self.Events[Event.id][EventPriority] then - env.info("hallo 200") - self:E({blub=EventMeta.Text, bla=self.Events[Event.id][EventPriority]}) - -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do - - env.info("hallo 300") - self:E({eventclass=EventClass}) - self:E({eventdata=EventData}) - + --if Event.IniObjectCategory ~= Object.Category.STATIC then -- self:E( { "Evaluating: ", EventClass:GetClassNameAndID() } ) --end From 4b369fae95bdefb1099c041dbfbd4de886197149 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 8 Apr 2020 11:54:09 +0200 Subject: [PATCH 459/485] A2G Dispatcher Fixes issue #1303 --- Moose Development/Moose/AI/AI_A2G_Dispatcher.lua | 2 +- Moose Development/Moose/Core/Event.lua | 1 + 2 files 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 d747bcf58..bdf05b150 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -4375,7 +4375,7 @@ do -- AI_A2G_DISPATCHER Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) ) for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do - Report:Add( string.format( " - %s - %d", DefenderSquadronName, DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount or "n/a" ) ) + Report:Add( string.format( " - %s - %s", DefenderSquadronName, DefenderSquadron.ResourceCount and tostring(DefenderSquadron.ResourceCount) or "n/a" ) ) end self:F( Report:Text( "\n" ) ) diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index f34d3f393..156e26153 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -1063,6 +1063,7 @@ function EVENT:onEvent( Event ) if Event.id==EVENTS.LandingAfterEjection then -- Place is here the UNIT of which the pilot ejected. --local name=Event.place:getName() -- This returns a DCS error "Airbase doesn't exit" :( + -- However, this is not a big thing, as the aircraft the pilot ejected from is usually long crashed before the ejected pilot touches the ground. --Event.Place=UNIT:Find(Event.place) else Event.Place=AIRBASE:Find(Event.place) From 0502e0a020de43e94c46c95efc08092fe44f3284 Mon Sep 17 00:00:00 2001 From: Pikes Date: Wed, 22 Apr 2020 14:09:31 +0100 Subject: [PATCH 460/485] Spawn Documentation fixes no code touched no code touched I went through removing where 6-7 year old docs used a non existent method "Schedule" and replaced it with SpawnSchedule() wher ethat worked or Spawn() where I couldnt understand the ancient demo. Also added warnings on IniLimit and InitCleanUp() the two worst offenders of misconfiguration. Couple of Belgian typos corrected. --- Moose Development/Moose/Core/Spawn.lua | 37 +++++++++++++------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 13dc85702..ebcfb1f99 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -455,7 +455,8 @@ function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPr end ---- Limits the Maximum amount of Units that can be alive at the same time, and the maximum amount of groups that can be spawned. +--- Stops any more repeat spawns from happening once the UNIT count of Alive units, spawned by the same SPAWN object, exceeds the first parameter. Also can stop spawns from happening once a total GROUP still alive is met. +-- Exceptionally powerful when combined with SpawnSchedule for Respawning. -- Note that this method is exceptionally important to balance the performance of the mission. Depending on the machine etc, a mission can only process a maximum amount of units. -- If the time interval must be short, but there should not be more Units or Groups alive than a maximum amount of units, then this method should be used... -- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. @@ -815,9 +816,9 @@ end -- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', -- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', -- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) function SPAWN:InitRandomizeTemplate( SpawnTemplatePrefixTable ) self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) @@ -851,9 +852,9 @@ end -- Spawn_US_PlatoonSet = SET_GROUP:New():FilterPrefixes( "US Tank Platoon Templates" ):FilterOnce() -- -- --- Now use the Spawn_US_PlatoonSet to define the templates using InitRandomizeTemplateSet. --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) function SPAWN:InitRandomizeTemplateSet( SpawnTemplateSet ) -- R2.3 self:F( { self.SpawnTemplatePrefix } ) @@ -884,9 +885,9 @@ end -- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and -- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. -- --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) function SPAWN:InitRandomizeTemplatePrefixes( SpawnTemplatePrefixes ) --R2.3 self:F( { self.SpawnTemplatePrefix } ) @@ -978,11 +979,9 @@ end -- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. -- SpawnRU_SU34 = SPAWN -- :New( 'Su-34' ) --- :Schedule( 2, 3, 1800, 0.4 ) --- :SpawnUncontrolled() -- :InitRandomizeRoute( 1, 1, 3000 ) -- :InitRepeatOnLanding() --- +-- :Spawn() function SPAWN:InitRepeatOnLanding() self:F( { self.SpawnTemplatePrefix } ) @@ -1002,11 +1001,10 @@ end -- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. -- SpawnRU_SU34 = SPAWN -- :New( 'Su-34' ) --- :Schedule( 2, 3, 1800, 0.4 ) -- :SpawnUncontrolled() -- :InitRandomizeRoute( 1, 1, 3000 ) -- :InitRepeatOnEngineShutDown() --- +-- :Spawn() function SPAWN:InitRepeatOnEngineShutDown() self:F( { self.SpawnTemplatePrefix } ) @@ -1018,7 +1016,8 @@ function SPAWN:InitRepeatOnEngineShutDown() end ---- CleanUp groups when they are still alive, but inactive. +--- Delete groups that have not moved for X seconds - AIR ONLY!!! +-- DO NOT USE ON GROUPS THAT DO NOT MOVE OR YOUR SERVER WILL BURN IN HELL (Pikes - April 2020) -- When groups are still alive and have become inactive due to damage and are unable to contribute anything, then this group will be removed at defined intervals in seconds. -- @param #SPAWN self -- @param #string SpawnCleanUpInterval The interval to check for inactive groups within seconds. @@ -1198,7 +1197,7 @@ function SPAWN:ReSpawn( SpawnIndex ) SpawnIndex = 1 end --- TODO: This logic makes DCS crash and i don't know why (yet). +-- TODO: This logic makes DCS crash and i don't know why (yet). -- ED (Pikes -- not in the least bit scary to see this, right?) local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) local WayPoints = SpawnGroup and SpawnGroup.WayPoints or nil if SpawnGroup then @@ -1463,7 +1462,7 @@ end -- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 -- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 -- -- Between these two values, a random amount of seconds will be choosen for each new spawn of the helicopters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):SpawnScheduled( 600, 0.5 ) function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation ) self:F( { SpawnTime, SpawnTimeVariation } ) @@ -2327,7 +2326,7 @@ function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex end end - -- Set gereral spawnpoint position. + -- Set general spawnpoint position. SpawnPoint.x = PointVec3.x SpawnPoint.y = PointVec3.z SpawnPoint.alt = PointVec3.y From 909c028e48de61c5164295f325854061e0281b0d Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 23 Apr 2020 16:04:50 +0200 Subject: [PATCH 461/485] Update AI_A2A_Patrol.lua Fixes issue #1307 --- Moose Development/Moose/AI/AI_A2A_Patrol.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index 45fef37cf..16b320b80 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -121,7 +121,7 @@ AI_A2A_PATROL = { --- Creates a new AI_A2A_PATROL object -- @param #AI_A2A_PATROL self --- @param Wrapper.Group#GROUP AIPatrol +-- @param Wrapper.Group#GROUP AIPatrol The patrol group object. -- @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. @@ -137,7 +137,7 @@ AI_A2A_PATROL = { function AI_A2A_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) local AI_Air = AI_AIR:New( AIPatrol ) - local AI_Air_Patrol = AI_A2A_PATROL:New( AI_Air, AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) local self = BASE:Inherit( self, AI_Air_Patrol ) -- #AI_A2A_PATROL self:SetFuelThreshold( .2, 60 ) From 4c3fd1d8670fc55258538242bd44b06d6a6b2d93 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 23 Apr 2020 16:44:38 +0200 Subject: [PATCH 462/485] Airboss v1.1.3 - Unicorn only when Tgroove 16-18 sec. - Recovery time can be given as relative seconds. - Time before turn can be specified by user. - Function to get next recovery time. - Removed some old code. --- Moose Development/Moose/Ops/Airboss.lua | 174 +++++++----------------- 1 file changed, 50 insertions(+), 124 deletions(-) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 83502fa80..6230b97cc 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1691,7 +1691,7 @@ AIRBOSS.MenuF10Root=nil --- Airboss class version. -- @field #string version -AIRBOSS.version="1.1.1" +AIRBOSS.version="1.1.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1880,6 +1880,9 @@ function AIRBOSS:New(carriername, alias) -- Default recovery case. This sets self.defaultcase and self.case. Default Case I. self:SetRecoveryCase() + -- Set time the turn starts before the window opens. + self:SetRecoveryTurnTime() + -- Set holding offset to 0 degrees. This set self.defaultoffset and self.holdingoffset. self:SetHoldingOffsetAngle() @@ -2433,6 +2436,15 @@ function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, tur -- Absolute mission time in seconds. local Tnow=timer.getAbsTime() + if starttime and type(starttime)=="number" then + starttime=UTILS.SecondsToClock(Tnow+starttime) + end + + if stoptime and type(stoptime)=="number" then + stoptime=UTILS.SecondsToClock(Tnow+stoptime) + end + + -- Input or now. starttime=starttime or UTILS.SecondsToClock(Tnow) @@ -2598,6 +2610,15 @@ function AIRBOSS:DeleteRecoveryWindow(window, delay) end end +--- Set time before carrier turns and recovery window opens. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 600 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetRecoveryTurnTime(interval) + self.dTturn=interval or 600 + 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. @@ -3226,6 +3247,27 @@ function AIRBOSS:SetDebugModeOFF() return self end +--- Get next time the carrier will start recovering aircraft. +-- @param #AIRBOSS self +-- @param #boolean InSeconds If true, abs. mission time seconds is returned. Default is a clock #string. +-- @return #string Clock start (or start time in abs. seconds). +-- @return #string Clock stop (or stop time in abs. seconds). +function AIRBOSS:GetNextRecoveryTime(InSeconds) + if self.recoverywindow then + if InSeconds then + return self.recoverywindow.START, self.recoverywindow.STOP + else + return UTILS.SecondsToClock(self.recoverywindow.START), UTILS.SecondsToClock(self.recoverywindow.STOP) + end + else + if InSeconds then + return -1, -1 + else + return "?", "?" + end + end +end + --- Check if carrier is recovering aircraft. -- @param #AIRBOSS self -- @return #boolean If true, time slot for recovery is open. @@ -3782,7 +3824,7 @@ function AIRBOSS:_CheckRecoveryTimes() 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 + if nextwindow.WIND and nextwindow.START-time 5° different from the current heading. local hdg=self:GetHeading() @@ -3805,7 +3847,7 @@ function AIRBOSS:_CheckRecoveryTimes() self:T(self.lid..string.format("Heading=%03d°, Wind=%03d° %.1f kts, Delta=%03d° ==> U-turn=%s", hdg, wind,UTILS.MpsToKnots(vwind), delta, tostring(uturn))) -- Time into the wind 1 day or if longer recovery time + the 5 min early. - local t=math.max(nextwindow.STOP-nextwindow.START+300, 60*60*24) + local t=math.max(nextwindow.STOP-nextwindow.START+self.dTturn, 60*60*24) -- Recovery wind on deck in knots. local v=UTILS.KnotsToMps(nextwindow.SPEED) @@ -5964,7 +6006,6 @@ 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 @@ -6070,7 +6111,6 @@ function AIRBOSS:_ScanCarrierZone() end - -- Find flights that are not in CCA. local remove={} for _,_flight in pairs(self.flights) do @@ -10769,53 +10809,6 @@ function AIRBOSS:_GetZoneCorridor(case, l) local zone=ZONE_POLYGON_BASE:New("CASE II/III Approach Corridor", p) return zone - - --[[ - -- OLD - - -- Angle between radial and offset in rad. - local alpha=math.rad(self.holdingoffset) - - -- 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) - - -- This is what we need. - local P=x1+b - local Q=x2-b - - -- Debug output. - 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)) - - -- Complicated case with an angle. - 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) - - -- 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) - ]] - end @@ -11314,20 +11307,6 @@ function AIRBOSS:_Lineup(unit, runway) --- local lineup=math.deg(math.atan2(z, x)) - --[[ - -- 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)) - ]] - return lineup end @@ -11946,9 +11925,13 @@ function AIRBOSS:_LSOgrade(playerData) local nS=count(G, '%(') local nN=N-nS-nL + -- Groove time 16-18 sec for a unicorn. + local Tgroove=playerData.Tgroove + local TgrooveUnicorn=Tgroove and (Tgroove>=16.0 and Tgroove<=18.0) or false + local grade local points - if N==0 then + if N==0 and TgrooveUnicorn then -- No deviations, should be REALLY RARE! grade="_OK_" points=5.0 @@ -13511,63 +13494,6 @@ function AIRBOSS:_InitWaypoints() return self end ---[[ - ---- Patrol carrier. --- @param #AIRBOSS self --- @param #number n Current waypoint. --- @return #AIRBOSS self -function AIRBOSS:_PatrolRoute(n) - - -- 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 - - -- Init array. - self.waypoints={} - - -- Set waypoint table. - for i,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 - - end - - -- Current waypoint is 1. - self.currentwp=n or 1 - - -- Route carrier group. - CarrierGroup:Route(Waypoints) - - return self -end - -]] - --- Patrol carrier. -- @param #AIRBOSS self -- @param #number n Next waypoint number. From 3c428723f8ea2f017bdb171cf91256142fc36d38 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 24 Apr 2020 00:16:18 +0200 Subject: [PATCH 463/485] Update AI_Air.lua Should fix issue #1205 --- Moose Development/Moose/AI/AI_Air.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 5fbd84349..213e58757 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -492,6 +492,7 @@ function AI_AIR:onafterStatus() OldAIControllable:SetTask( TimedOrbitTask, 10 ) self:Fuel() + RTB = true end else end From da05c41c27bd06d2a120c5d62d5dbfd370d72ebf Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 24 Apr 2020 00:40:54 +0200 Subject: [PATCH 464/485] Update AI_Air_Engage.lua Should fix #1206 --- Moose Development/Moose/AI/AI_Air_Engage.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/AI/AI_Air_Engage.lua b/Moose Development/Moose/AI/AI_Air_Engage.lua index 50b2e0988..2679d472d 100644 --- a/Moose Development/Moose/AI/AI_Air_Engage.lua +++ b/Moose Development/Moose/AI/AI_Air_Engage.lua @@ -374,7 +374,7 @@ end -- @param #string To The To State string. function AI_AIR_ENGAGE:onafterAccomplish( AIGroup, From, Event, To ) self.Accomplished = true - self:SetDetectionOff() + --self:SetDetectionOff() end --- @param #AI_AIR_ENGAGE self From df512f7a5873bc634f50a672098e001530e5d388 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 24 Apr 2020 01:19:04 +0200 Subject: [PATCH 465/485] Update RadioQueue.lua --- Moose Development/Moose/Core/RadioQueue.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Moose Development/Moose/Core/RadioQueue.lua b/Moose Development/Moose/Core/RadioQueue.lua index 756b5a967..2166c54fa 100644 --- a/Moose Development/Moose/Core/RadioQueue.lua +++ b/Moose Development/Moose/Core/RadioQueue.lua @@ -279,6 +279,10 @@ end -- @return #number Duration of the call in seconds. function RADIOQUEUE:Number2Transmission(number, delay, interval) + if not number then + self:E("ERROR: Number is nil!") + end + --- Split string into characters. local function _split(str) local chars={} From 7360c51a8f6561636c95c0c2f7bf1a0b993aab86 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 24 Apr 2020 16:03:28 +0200 Subject: [PATCH 466/485] Updates Misc --- Moose Development/Moose/Core/Point.lua | 22 + Moose Development/Moose/Core/RadioQueue.lua | 14 +- Moose Development/Moose/Core/Settings.lua | 732 +++++++++++------- Moose Development/Moose/DCS.lua | 24 +- Moose Development/Moose/Utilities/Enums.lua | 178 ++++- .../Moose/Wrapper/Controllable.lua | 432 ++++++----- Moose Development/Moose/Wrapper/Group.lua | 23 +- .../Moose/Wrapper/Identifiable.lua | 13 +- Moose Development/Moose/Wrapper/Static.lua | 18 +- Moose Development/Moose/Wrapper/Unit.lua | 87 ++- 10 files changed, 1026 insertions(+), 517 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 6358341ef..65022dec4 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -655,6 +655,28 @@ do -- COORDINATE return Angle end + --- Return an intermediate COORDINATE between this an another coordinate. + -- @param #COORDINATE self + -- @param #COORDINATE ToCoordinate The other coordinate. + -- @param #number Fraction The fraction (0,1) where the new coordinate is created. Default 0.5, i.e. in the middle. + -- @return #COORDINATE Coordinate between this and the other coordinate. + function COORDINATE:GetIntermediateCoordinate( ToCoordinate, Fraction ) + + local f=Fraction or 0.5 + + -- Get the vector from A to B + local vec=UTILS.VecSubstract(ToCoordinate, self) + + -- Scale the vector. + vec.x=f*vec.x + vec.y=f*vec.y + vec.z=f*vec.z + + -- Move the vector to start at the end of A. + vec=UTILS.VecAdd(self, vec) + + return self:New(vec.x,vec.y,vec.z) + end --- Return the 2D distance in meters between the target COORDINATE and the COORDINATE. -- @param #COORDINATE self diff --git a/Moose Development/Moose/Core/RadioQueue.lua b/Moose Development/Moose/Core/RadioQueue.lua index 2166c54fa..33857ba43 100644 --- a/Moose Development/Moose/Core/RadioQueue.lua +++ b/Moose Development/Moose/Core/RadioQueue.lua @@ -113,18 +113,20 @@ end -- @param #number dt (Optional) Time step in seconds for checking the queue. Default 0.01 sec. -- @return #RADIOQUEUE self The RADIOQUEUE object. function RADIOQUEUE:Start(delay, dt) - + + -- Delay before start. self.delay=delay or 1 + -- Time interval for queue check. self.dt=dt or 0.01 + -- Debug message. self:I(self.lid..string.format("Starting RADIOQUEUE %s on Frequency %.2f MHz [modulation=%d] in %.1f seconds (dt=%.3f sec)", self.alias, self.frequency/1000000, self.modulation, delay, dt)) - + -- Start Scheduler. if self.schedonce then - self:_CheckRadioQueueDelayed(self.delta) + self:_CheckRadioQueueDelayed(delay) else - --self.RQid=self.scheduler:Schedule(self, self._CheckRadioQueue, {}, delay, dt) self.RQid=self.scheduler:Schedule(nil, RADIOQUEUE._CheckRadioQueue, {self}, delay, dt) end @@ -279,10 +281,6 @@ end -- @return #number Duration of the call in seconds. function RADIOQUEUE:Number2Transmission(number, delay, interval) - if not number then - self:E("ERROR: Number is nil!") - end - --- Split string into characters. local function _split(str) local chars={} diff --git a/Moose Development/Moose/Core/Settings.lua b/Moose Development/Moose/Core/Settings.lua index fd1aca413..3a557de65 100644 --- a/Moose Development/Moose/Core/Settings.lua +++ b/Moose Development/Moose/Core/Settings.lua @@ -1,9 +1,9 @@ --- **Core** - Manages various settings for running missions, consumed by moose classes and provides a menu system for players to tweak settings in running missions. -- -- === --- +-- -- ## Features: --- +-- -- * Provide a settings menu system to the players. -- * Provide a player settings menu and an overall mission settings menu. -- * Mission settings provide default settings, while player settings override mission settings. @@ -11,19 +11,19 @@ -- * Provide a menu to select between different coordinate formats for A2A coordinates. -- * Provide a menu to select between different message time duration options. -- * Provide a menu to select between different metric systems. --- +-- -- === --- +-- -- The documentation of the SETTINGS class can be found further in this document. --- +-- -- === --- +-- -- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- ### Authors: --- +-- +-- ### Contributions: +-- +-- ### Authors: +-- -- * **FlightControl**: Design & Programming -- -- @module Core.Settings @@ -34,185 +34,187 @@ -- @extends Core.Base#BASE --- Takes care of various settings that influence the behaviour of certain functionalities and classes within the MOOSE framework. --- +-- -- === --- +-- -- The SETTINGS class takes care of various settings that influence the behaviour of certain functionalities and classes within the MOOSE framework. -- SETTINGS can work on 2 levels: --- --- - **Default settings**: A running mission has **Default settings**. +-- +-- - **Default settings**: A running mission has **Default settings**. -- - **Player settings**: For each player its own **Player settings** can be defined, overriding the **Default settings**. --- +-- -- So, when there isn't any **Player setting** defined for a player for a specific setting, or, the player cannot be identified, the **Default setting** will be used instead. --- +-- -- # 1) \_SETTINGS object --- +-- -- MOOSE defines by default a singleton object called **\_SETTINGS**. Use this object to modify all the **Default settings** for a running mission. -- For each player, MOOSE will automatically allocate also a **player settings** object, and will expose a radio menu to allow the player to adapt the settings to his own preferences. --- +-- -- # 2) SETTINGS Menu --- +-- -- Settings can be adapted by the Players and by the Mission Administrator through **radio menus, which are automatically available in the mission**. -- These menus can be found **on level F10 under "Settings"**. There are two kinds of menus generated by the system. --- +-- -- ## 2.1) Default settings menu --- +-- -- A menu is created automatically per Command Center that allows to modify the **Default** settings. -- So, when joining a CC unit, a menu will be available that allows to change the settings parameters **FOR ALL THE PLAYERS**! -- Note that the **Default settings** will only be used when a player has not choosen its own settings. --- +-- -- ## 2.2) Player settings menu --- +-- -- A menu is created automatically per Player Slot (group) that allows to modify the **Player** settings. -- So, when joining a slot, a menu wil be available that allows to change the settings parameters **FOR THE PLAYER ONLY**! -- Note that when a player has not chosen a specific setting, the **Default settings** will be used. --- +-- -- ## 2.3) Show or Hide the Player Setting menus --- +-- -- Of course, it may be requried not to show any setting menus. In this case, a method is available on the **\_SETTINGS object**. -- Use @{#SETTINGS.SetPlayerMenuOff}() to hide the player menus, and use @{#SETTINGS.SetPlayerMenuOn}() show the player menus. -- Note that when this method is used, any player already in a slot will not have its menus visibility changed. --- The option will only have effect when a player enters a new slot or changes a slot. --- +-- The option will only have effect when a player enters a new slot or changes a slot. +-- -- Example: --- +-- -- _SETTINGS:SetPlayerMenuOff() -- will disable the player menus. -- _SETTINGS:SetPlayerMenuOn() -- will enable the player menus. -- -- But only when a player exits and reenters the slot these settings will have effect! --- --- +-- +-- -- # 3) Settings --- +-- -- There are different settings that are managed and applied within the MOOSE framework. -- See below a comprehensive description of each. --- +-- -- ## 3.1) **A2G coordinates** display formatting --- +-- -- ### 3.1.1) A2G coordinates setting **types** --- +-- -- Will customize which display format is used to indicate A2G coordinates in text as part of the Command Center communications. --- +-- -- - A2G BR: [Bearing Range](https://en.wikipedia.org/wiki/Bearing_(navigation)). -- - A2G MGRS: The [Military Grid Reference System](https://en.wikipedia.org/wiki/Military_Grid_Reference_System). The accuracy can also be adapted. -- - A2G LL DMS: Lattitude Longitude [Degrees Minutes Seconds](https://en.wikipedia.org/wiki/Geographic_coordinate_conversion). The accuracy can also be adapted. -- - A2G LL DDM: Lattitude Longitude [Decimal Degrees Minutes](https://en.wikipedia.org/wiki/Decimal_degrees). The accuracy can also be adapted. --- +-- -- ### 3.1.2) A2G coordinates setting **menu** --- +-- -- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. --- +-- -- ### 3.1.3) A2G coordinates setting **methods** --- +-- -- There are different methods that can be used to change the **System settings** using the \_SETTINGS object. --- +-- -- - @{#SETTINGS.SetA2G_BR}(): Enable the BR display formatting by default. -- - @{#SETTINGS.SetA2G_MGRS}(): Enable the MGRS display formatting by default. Use @{SETTINGS.SetMGRS_Accuracy}() to adapt the accuracy of the MGRS formatting. --- - @{#SETTINGS.SetA2G_LL_DMS}(): Enable the LL DMS display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. +-- - @{#SETTINGS.SetA2G_LL_DMS}(): Enable the LL DMS display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. -- - @{#SETTINGS.SetA2G_LL_DDM}(): Enable the LL DDM display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. --- +-- -- ### 3.1.4) A2G coordinates setting - additional notes --- --- One additional note on BR. In a situation when a BR coordinate should be given, +-- +-- One additional note on BR. In a situation when a BR coordinate should be given, -- but there isn't any player context (no player unit to reference from), the MGRS formatting will be applied! --- +-- -- ## 3.2) **A2A coordinates** formatting --- +-- -- ### 3.2.1) A2A coordinates setting **types** --- +-- -- Will customize which display format is used to indicate A2A coordinates in text as part of the Command Center communications. --- +-- -- - A2A BRAA: [Bearing Range Altitude Aspect](https://en.wikipedia.org/wiki/Bearing_(navigation)). -- - A2A MGRS: The [Military Grid Reference System](https://en.wikipedia.org/wiki/Military_Grid_Reference_System). The accuracy can also be adapted. -- - A2A LL DMS: Lattitude Longitude [Degrees Minutes Seconds](https://en.wikipedia.org/wiki/Geographic_coordinate_conversion). The accuracy can also be adapted. -- - A2A LL DDM: Lattitude Longitude [Decimal Degrees and Minutes](https://en.wikipedia.org/wiki/Decimal_degrees). The accuracy can also be adapted. -- - A2A BULLS: [Bullseye](http://falcon4.wikidot.com/concepts:bullseye). --- +-- -- ### 3.2.2) A2A coordinates setting **menu** --- +-- -- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. --- +-- -- ### 3.2.3) A2A coordinates setting **methods** --- +-- -- There are different methods that can be used to change the **System settings** using the \_SETTINGS object. --- +-- -- - @{#SETTINGS.SetA2A_BRAA}(): Enable the BR display formatting by default. -- - @{#SETTINGS.SetA2A_MGRS}(): Enable the MGRS display formatting by default. Use @{SETTINGS.SetMGRS_Accuracy}() to adapt the accuracy of the MGRS formatting. --- - @{#SETTINGS.SetA2A_LL_DMS}(): Enable the LL DMS display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. +-- - @{#SETTINGS.SetA2A_LL_DMS}(): Enable the LL DMS display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. -- - @{#SETTINGS.SetA2A_LL_DDM}(): Enable the LL DDM display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. -- - @{#SETTINGS.SetA2A_BULLS}(): Enable the BULLSeye display formatting by default. --- +-- -- ### 3.2.4) A2A coordinates settings - additional notes --- --- One additional note on BRAA. In a situation when a BRAA coordinate should be given, +-- +-- One additional note on BRAA. In a situation when a BRAA coordinate should be given, -- but there isn't any player context (no player unit to reference from), the MGRS formatting will be applied! --- +-- -- ## 3.3) **Measurements** formatting --- +-- -- ### 3.3.1) Measurements setting **types** --- +-- -- Will customize the measurements system being used as part as part of the Command Center communications. --- +-- -- - **Metrics** system: Applies the [Metrics system](https://en.wikipedia.org/wiki/Metric_system) ... --- - **Imperial** system: Applies the [Imperial system](https://en.wikipedia.org/wiki/Imperial_units) ... --- +-- - **Imperial** system: Applies the [Imperial system](https://en.wikipedia.org/wiki/Imperial_units) ... +-- -- ### 3.3.2) Measurements setting **menu** --- +-- -- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. --- +-- -- ### 3.3.3) Measurements setting **methods** --- +-- -- There are different methods that can be used to change the **Default settings** using the \_SETTINGS object. --- +-- -- - @{#SETTINGS.SetMetric}(): Enable the Metric system. -- - @{#SETTINGS.SetImperial}(): Enable the Imperial system. --- +-- -- ## 3.4) **Message** display times --- +-- -- ### 3.4.1) Message setting **types** --- +-- -- There are various **Message Types** that will influence the duration how long a message will appear as part of the Command Center communications. --- +-- -- - **Update** message: A short update message. -- - **Information** message: Provides new information **while** executing a mission. -- - **Briefing** message: Provides a complete briefing **before** executing a mission. -- - **Overview report**: Provides a short report overview, the summary of the report. -- - **Detailed report**: Provides a complete report. --- +-- -- ### 3.4.2) Message setting **menu** --- +-- -- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. --- +-- -- Each Message Type has specific timings that will be applied when the message is displayed. -- The Settings Menu will provide for each Message Type a selection of proposed durations from which can be choosen. -- So the player can choose its own amount of seconds how long a message should be displayed of a certain type. -- Note that **Update** messages can be chosen not to be displayed at all! --- +-- -- ### 3.4.3) Message setting **methods** --- +-- -- There are different methods that can be used to change the **System settings** using the \_SETTINGS object. --- --- - @{#SETTINGS.SetMessageTime}(): Define for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. --- - @{#SETTINGS.GetMessageTime}(): Retrieves for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. --- +-- +-- - @{#SETTINGS.SetMessageTime}(): Define for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. +-- - @{#SETTINGS.GetMessageTime}(): Retrieves for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. +-- -- ## 3.5) **Era** of the battle --- +-- -- The threat level metric is scaled according the era of the battle. A target that is AAA, will pose a much greather threat in WWII than on modern warfare. -- Therefore, there are 4 era that are defined within the settings: --- +-- -- - **WWII** era: Use for warfare with equipment during the world war II time. -- - **Korea** era: Use for warfare with equipment during the Korea war time. --- - **Cold War** era: Use for warfare with equipment during the cold war time. +-- - **Cold War** era: Use for warfare with equipment during the cold war time. -- - **Modern** era: Use for warfare with modern equipment in the 2000s. --- --- There are different API defined that you can use with the _SETTINGS object to configure your mission script to work in one of the 4 era: +-- +-- There are different API defined that you can use with the _SETTINGS object to configure your mission script to work in one of the 4 era: -- @{#SETTINGS.SetEraWWII}(), @{#SETTINGS.SetEraKorea}(), @{#SETTINGS.SetEraCold}(), @{#SETTINGS.SetEraModern}() --- +-- -- === --- +-- -- @field #SETTINGS SETTINGS = { ClassName = "SETTINGS", ShowPlayerMenu = true, + MenuShort = false, + MenuStatic = false, } SETTINGS.__Enum = {} @@ -235,7 +237,7 @@ do -- SETTINGS --- SETTINGS constructor. -- @param #SETTINGS self -- @return #SETTINGS - function SETTINGS:Set( PlayerName ) + function SETTINGS:Set( PlayerName ) if PlayerName == nil then local self = BASE:Inherit( self, BASE:New() ) -- #SETTINGS @@ -260,14 +262,28 @@ do -- SETTINGS return Settings end end - - + + --- Set short text for menus on (*true*) or off (*false*). + -- Short text are better suited for, e.g., VR. + -- @param #SETTINGS self + -- @param #boolean onoff If *true* use short menu texts. If *false* long ones (default). + function SETTINGS:SetMenutextShort(onoff) + _SETTINGS.MenuShort = onoff + end + + --- Set menu to be static. + -- @param #SETTINGS self + -- @param #boolean onoff If *true* menu is static. If *false* menu will be updated after changes (default). + function SETTINGS:SetMenuStatic(onoff) + _SETTINGS.MenuStatic = onoff + end + --- Sets the SETTINGS metric. -- @param #SETTINGS self function SETTINGS:SetMetric() self.Metric = true end - + --- Gets if the SETTINGS is metric. -- @param #SETTINGS self -- @return #boolean true if metric. @@ -280,7 +296,7 @@ do -- SETTINGS function SETTINGS:SetImperial() self.Metric = false end - + --- Gets if the SETTINGS is imperial. -- @param #SETTINGS self -- @return #boolean true if imperial. @@ -317,7 +333,7 @@ do -- SETTINGS function SETTINGS:GetMGRS_Accuracy() return self.MGRS_Accuracy or _SETTINGS:GetMGRS_Accuracy() end - + --- Sets the SETTINGS Message Display Timing of a MessageType -- @param #SETTINGS self -- @param Core.Message#MESSAGE MessageType The type of the message. @@ -326,8 +342,8 @@ do -- SETTINGS self.MessageTypeTimings = self.MessageTypeTimings or {} self.MessageTypeTimings[MessageType] = MessageTime end - - + + --- Gets the SETTINGS Message Display Timing of a MessageType -- @param #SETTINGS self -- @param Core.Message#MESSAGE MessageType The type of the message. @@ -463,40 +479,77 @@ do -- SETTINGS end --- @param #SETTINGS self + -- @param Wrapper.Group#GROUP MenuGroup Group for which to add menus. + -- @param #table RootMenu Root menu table -- @return #SETTINGS function SETTINGS:SetSystemMenu( MenuGroup, RootMenu ) local MenuText = "System Settings" - + local MenuTime = timer.getTime() - + local SettingsMenu = MENU_GROUP:New( MenuGroup, MenuText, RootMenu ):SetTime( MenuTime ) - local A2GCoordinateMenu = MENU_GROUP:New( MenuGroup, "A2G Coordinate System", SettingsMenu ):SetTime( MenuTime ) - - - if not self:IsA2G_LL_DMS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) + ------- + -- A2G Coordinate System + ------- + + local text="A2G Coordinate System" + if _SETTINGS.MenuShort then + text="A2G Coordinates" end - + local A2GCoordinateMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) + + -- Set LL DMS + if not self:IsA2G_LL_DMS() then + local text="Lat/Lon Degree Min Sec (LL DMS)" + if _SETTINGS.MenuShort then + text="LL DMS" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) + end + + -- Set LL DDM if not self:IsA2G_LL_DDM() then + local text="Lat/Lon Degree Dec Min (LL DDM)" + if _SETTINGS.MenuShort then + text="LL DDM" + end MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime ) end - + + -- Set LL DMS accuracy. if self:IsA2G_LL_DDM() then + local text1="LL DDM Accuracy 1" + local text2="LL DDM Accuracy 2" + local text3="LL DDM Accuracy 3" + if _SETTINGS.MenuShort then + text1="LL DDM" + end MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) end - + + -- Set BR. if not self:IsA2G_BR() then - MENU_GROUP_COMMAND:New( MenuGroup, "Bearing, Range (BR)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "BR" ):SetTime( MenuTime ) + local text="Bearing, Range (BR)" + if _SETTINGS.MenuShort then + text="BR" + end + MENU_GROUP_COMMAND:New( MenuGroup, text , A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "BR" ):SetTime( MenuTime ) end - + + -- Set MGRS. if not self:IsA2G_MGRS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Military Grid (MGRS)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) + local text="Military Grid (MGRS)" + if _SETTINGS.MenuShort then + text="MGRS" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) end - + + -- Set MGRS accuracy. if self:IsA2G_MGRS() then MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 1", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 2", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) @@ -505,32 +558,61 @@ do -- SETTINGS MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 5", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 5 ):SetTime( MenuTime ) end - local A2ACoordinateMenu = MENU_GROUP:New( MenuGroup, "A2A Coordinate System", SettingsMenu ):SetTime( MenuTime ) + ------- + -- A2A Coordinate System + ------- + + local text="A2A Coordinate System" + if _SETTINGS.MenuShort then + text="A2A Coordinates" + end + local A2ACoordinateMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) if not self:IsA2A_LL_DMS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) + local text="Lat/Lon Degree Min Sec (LL DMS)" + if _SETTINGS.MenuShort then + text="LL DMS" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) end if not self:IsA2A_LL_DDM() then - MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime ) + local text="Lat/Lon Degree Dec Min (LL DDM)" + if _SETTINGS.MenuShort then + text="LL DDM" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime ) end - if self:IsA2A_LL_DDM() then - MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 1", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 2", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 3", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) - end + if self:IsA2A_LL_DDM() or self:IsA2A_LL_DMS() then + MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 0", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 0 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 1", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 2", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 3", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) + end if not self:IsA2A_BULLS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Bullseye (BULLS)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BULLS" ):SetTime( MenuTime ) + local text="Bullseye (BULLS)" + if _SETTINGS.MenuShort then + text="Bulls" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BULLS" ):SetTime( MenuTime ) end - + if not self:IsA2A_BRAA() then - MENU_GROUP_COMMAND:New( MenuGroup, "Bearing Range Altitude Aspect (BRAA)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BRAA" ):SetTime( MenuTime ) + local text="Bearing Range Altitude Aspect (BRAA)" + if _SETTINGS.MenuShort then + text="BRAA" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BRAA" ):SetTime( MenuTime ) end - + if not self:IsA2A_MGRS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Military Grid (MGRS)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) + local text="Military Grid (MGRS)" + if _SETTINGS.MenuShort then + text="MGRS" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) end if self:IsA2A_MGRS() then @@ -539,19 +621,35 @@ do -- SETTINGS MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 3", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 4", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 4 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 5", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 5 ):SetTime( MenuTime ) - end - - local MetricsMenu = MENU_GROUP:New( MenuGroup, "Measures and Weights System", SettingsMenu ):SetTime( MenuTime ) - - if self:IsMetric() then - MENU_GROUP_COMMAND:New( MenuGroup, "Imperial (Miles,Feet)", MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, false ):SetTime( MenuTime ) end - + + local text="Measures and Weights System" + if _SETTINGS.MenuShort then + text="Unit System" + end + local MetricsMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) + + if self:IsMetric() then + local text="Imperial (Miles,Feet)" + if _SETTINGS.MenuShort then + text="Imperial" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, false ):SetTime( MenuTime ) + end + if self:IsImperial() then - MENU_GROUP_COMMAND:New( MenuGroup, "Metric (Kilometers,Meters)", MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, true ):SetTime( MenuTime ) - end - - local MessagesMenu = MENU_GROUP:New( MenuGroup, "Messages and Reports", SettingsMenu ):SetTime( MenuTime ) + local text="Metric (Kilometers,Meters)" + if _SETTINGS.MenuShort then + text="Metric" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, true ):SetTime( MenuTime ) + end + + local text="Messages and Reports" + if _SETTINGS.MenuShort then + text="Messages & Reports" + end + local MessagesMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) local UpdateMessagesMenu = MENU_GROUP:New( MenuGroup, "Update Messages", MessagesMenu ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "Off", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 0 ):SetTime( MenuTime ) @@ -589,10 +687,10 @@ do -- SETTINGS MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 60 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 120 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "3 minutes", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 180 ):SetTime( MenuTime ) - + SettingsMenu:Remove( MenuTime ) - + return self end @@ -618,8 +716,6 @@ do -- SETTINGS self.ShowPlayerMenu = false end - - --- Updates the menu of the player seated in the PlayerUnit. -- @param #SETTINGS self -- @param Wrapper.Client#CLIENT PlayerUnit @@ -627,136 +723,200 @@ do -- SETTINGS function SETTINGS:SetPlayerMenu( PlayerUnit ) if _SETTINGS.ShowPlayerMenu == true then - + local PlayerGroup = PlayerUnit:GetGroup() local PlayerName = PlayerUnit:GetPlayerName() local PlayerNames = PlayerGroup:GetPlayerNames() - + local PlayerMenu = MENU_GROUP:New( PlayerGroup, 'Settings "' .. PlayerName .. '"' ) - + self.PlayerMenu = PlayerMenu - - local A2GCoordinateMenu = MENU_GROUP:New( PlayerGroup, "A2G Coordinate System", PlayerMenu ) - - if not self:IsA2G_LL_DMS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) + + self:I(string.format("Setting menu for player %s", tostring(PlayerName))) + + local submenu = MENU_GROUP:New( PlayerGroup, "LL Accuracy", PlayerMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 0 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 0 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 1 Decimal", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 2 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 3 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 4 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) + + local submenu = MENU_GROUP:New( PlayerGroup, "MGRS Accuracy", PlayerMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 0", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 0 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 1", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 2", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 3", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 4", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 5", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 5 ) + + ------ + -- A2G Coordinate System + ------ + + local text="A2G Coordinate System" + if _SETTINGS.MenuShort then + text="A2G Coordinates" end - - if not self:IsA2G_LL_DDM() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) + local A2GCoordinateMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) + + if not self:IsA2G_LL_DMS() or _SETTINGS.MenuStatic then + local text="Lat/Lon Degree Min Sec (LL DMS)" + if _SETTINGS.MenuShort then + text="A2G LL DMS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) end - - if self:IsA2G_LL_DDM() then - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) + + if not self:IsA2G_LL_DDM() or _SETTINGS.MenuStatic then + local text="Lat/Lon Degree Dec Min (LL DDM)" + if _SETTINGS.MenuShort then + text="A2G LL DDM" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) end - - if not self:IsA2G_BR() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Bearing, Range (BR)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "BR" ) + + if not self:IsA2G_BR() or _SETTINGS.MenuStatic then + local text="Bearing, Range (BR)" + if _SETTINGS.MenuShort then + text="A2G BR" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "BR" ) end - - if not self:IsA2G_MGRS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) - end - - if self:IsA2G_MGRS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 1", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 2", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 3", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 4", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 5", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 5 ) + + if not self:IsA2G_MGRS() or _SETTINGS.MenuStatic then + local text="Military Grid (MGRS)" + if _SETTINGS.MenuShort then + text="A2G MGRS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) end - - local A2ACoordinateMenu = MENU_GROUP:New( PlayerGroup, "A2A Coordinate System", PlayerMenu ) - - - if not self:IsA2A_LL_DMS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) + + ------ + -- A2A Coordinates Menu + ------ + + local text="A2A Coordinate System" + if _SETTINGS.MenuShort then + text="A2A Coordinates" end - - if not self:IsA2A_LL_DDM() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) + local A2ACoordinateMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) + + + if not self:IsA2A_LL_DMS() or _SETTINGS.MenuStatic then + local text="Lat/Lon Degree Min Sec (LL DMS)" + if _SETTINGS.MenuShort then + text="A2A LL DMS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) end - - if self:IsA2A_LL_DDM() then - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) + + if not self:IsA2A_LL_DDM() or _SETTINGS.MenuStatic then + local text="Lat/Lon Degree Dec Min (LL DDM)" + if _SETTINGS.MenuShort then + text="A2A LL DDM" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) end - - if not self:IsA2A_BULLS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Bullseye (BULLS)", A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BULLS" ) + + if not self:IsA2A_BULLS() or _SETTINGS.MenuStatic then + local text="Bullseye (BULLS)" + if _SETTINGS.MenuShort then + text="A2A BULLS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BULLS" ) end - - if not self:IsA2A_BRAA() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Bearing Range Altitude Aspect (BRAA)", A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BRAA" ) + + if not self:IsA2A_BRAA() or _SETTINGS.MenuStatic then + local text="Bearing Range Altitude Aspect (BRAA)" + if _SETTINGS.MenuShort then + text="A2A BRAA" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BRAA" ) end - - if not self:IsA2A_MGRS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS)", A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) + + if not self:IsA2A_MGRS() or _SETTINGS.MenuStatic then + local text="Military Grid (MGRS)" + if _SETTINGS.MenuShort then + text="A2A MGRS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) end - - if self:IsA2A_MGRS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 1", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 2", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 3", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 4", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 5", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 5 ) - end - - local MetricsMenu = MENU_GROUP:New( PlayerGroup, "Measures and Weights System", PlayerMenu ) - - if self:IsMetric() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Imperial (Miles,Feet)", MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, false ) + + --- + -- Unit system + --- + + local text="Measures and Weights System" + if _SETTINGS.MenuShort then + text="Unit System" end - - if self:IsImperial() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Metric (Kilometers,Meters)", MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, true ) - end - - - local MessagesMenu = MENU_GROUP:New( PlayerGroup, "Messages and Reports", PlayerMenu ) - + local MetricsMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) + + if self:IsMetric() or _SETTINGS.MenuStatic then + local text="Imperial (Miles,Feet)" + if _SETTINGS.MenuShort then + text="Imperial" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, false ) + end + + if self:IsImperial() or _SETTINGS.MenuStatic then + local text="Metric (Kilometers,Meters)" + if _SETTINGS.MenuShort then + text="Metric" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, true ) + end + + --- + -- Messages and Reports + --- + + local text="Messages and Reports" + if _SETTINGS.MenuShort then + text="Messages & Reports" + end + local MessagesMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) + local UpdateMessagesMenu = MENU_GROUP:New( PlayerGroup, "Update Messages", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Off", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 0 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "5 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 5 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "10 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 10 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 60 ) - - local InformationMessagesMenu = MENU_GROUP:New( PlayerGroup, "Information Messages", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "5 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 5 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "10 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 10 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 60 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 120 ) - + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates Off", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 0 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 5 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 5 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 10 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 10 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 15 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 30 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 1 min", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 60 ) + + local InformationMessagesMenu = MENU_GROUP:New( PlayerGroup, "Info Messages", MessagesMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 5 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 5 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 10 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 10 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 15 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 30 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 1 min", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 60 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 2 min", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 120 ) + local BriefingReportsMenu = MENU_GROUP:New( PlayerGroup, "Briefing Reports", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 60 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 120 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "3 minutes", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 180 ) - + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 15 sec", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 30 sec", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 1 min", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 60 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 2 min", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 120 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 3 min", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 180 ) + local OverviewReportsMenu = MENU_GROUP:New( PlayerGroup, "Overview Reports", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 60 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 120 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "3 minutes", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 180 ) - + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 15 sec", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 30 sec", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 1 min", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 60 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 2 min", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 120 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 3 min", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 180 ) + local DetailedReportsMenu = MENU_GROUP:New( PlayerGroup, "Detailed Reports", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 60 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 120 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "3 minutes", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 180 ) - - end - + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 15 sec", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 30 sec", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 1 min", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 60 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 2 min", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 120 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 3 min", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 180 ) + + end + return self end @@ -770,7 +930,7 @@ do -- SETTINGS self.PlayerMenu:Remove() self.PlayerMenu = nil end - + return self end @@ -822,40 +982,50 @@ do -- SETTINGS BASE:E( {self, PlayerUnit:GetName(), A2GSystem} ) self.A2GSystem = A2GSystem MESSAGE:New( string.format( "Settings: A2G format set to %s for player %s.", A2GSystem, PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end end - + --- @param #SETTINGS self function SETTINGS:MenuGroupA2ASystem( PlayerUnit, PlayerGroup, PlayerName, A2ASystem ) self.A2ASystem = A2ASystem MESSAGE:New( string.format( "Settings: A2A format set to %s for player %s.", A2ASystem, PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end end - + --- @param #SETTINGS self function SETTINGS:MenuGroupLL_DDM_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, LL_Accuracy ) self.LL_Accuracy = LL_Accuracy - MESSAGE:New( string.format( "Settings: A2G LL format accuracy set to %d for player %s.", LL_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + MESSAGE:New( string.format( "Settings: LL format accuracy set to %d decimal places for player %s.", LL_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end end - + --- @param #SETTINGS self function SETTINGS:MenuGroupMGRS_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, MGRS_Accuracy ) self.MGRS_Accuracy = MGRS_Accuracy - MESSAGE:New( string.format( "Settings: A2G MGRS format accuracy set to %d for player %s.", MGRS_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + MESSAGE:New( string.format( "Settings: MGRS format accuracy set to %d for player %s.", MGRS_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end end --- @param #SETTINGS self function SETTINGS:MenuGroupMWSystem( PlayerUnit, PlayerGroup, PlayerName, MW ) self.Metric = MW MESSAGE:New( string.format( "Settings: Measurement format set to %s for player %s.", MW and "Metric" or "Imperial", PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end end --- @param #SETTINGS self @@ -863,50 +1033,48 @@ do -- SETTINGS self:SetMessageTime( MessageType, MessageTime ) MESSAGE:New( string.format( "Settings: Default message time set for %s to %d.", MessageType, MessageTime ), 5 ):ToGroup( PlayerGroup ) end - + end - + --- Configures the era of the mission to be WWII. -- @param #SETTINGS self -- @return #SETTINGS self function SETTINGS:SetEraWWII() - + self.Era = SETTINGS.__Enum.Era.WWII - + end - + --- Configures the era of the mission to be Korea. -- @param #SETTINGS self -- @return #SETTINGS self function SETTINGS:SetEraKorea() - + self.Era = SETTINGS.__Enum.Era.Korea - + end - - + + --- Configures the era of the mission to be Cold war. -- @param #SETTINGS self -- @return #SETTINGS self function SETTINGS:SetEraCold() - + self.Era = SETTINGS.__Enum.Era.Cold - + end - + --- Configures the era of the mission to be Modern war. -- @param #SETTINGS self -- @return #SETTINGS self function SETTINGS:SetEraModern() - + self.Era = SETTINGS.__Enum.Era.Modern - + end - - - + + + end - - diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index df37c1c50..0c8e87a23 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -326,9 +326,27 @@ end -- coalition do -- Types --- @type Desc - -- @field #TypeName typeName type name - -- @field #string displayName localized display name - -- @field #table attributes object type attributes + -- @field #number speedMax0 Max speed in meters/second at zero altitude. + -- @field #number massEmpty Empty mass in kg. + -- @field #number tankerType Type of refueling system: 0=boom, 1=probe. + -- @field #number range Range in km(?). + -- @field #table box Bounding box. + -- @field #number Hmax Max height in meters. + -- @field #number Kmax ? + -- @field #number speedMax10K Max speed in meters/second at 10k altitude. + -- @field #number NyMin ? + -- @field #number NyMax ? + -- @field #number fuelMassMax Max fuel mass in kg. + -- @field #number speedMax10K Max speed in meters/second. + -- @field #number massMax Max mass of unit. + -- @field #number RCS ? + -- @field #number life Life points. + -- @field #number VyMax Max vertical velocity in m/s. + -- @field #number Kab ? + -- @field #table attributes Table of attributes. + -- @field #TypeName typeName Type Name. + -- @field #string displayName Localized display name. + -- @field #number category Unit category. --- A distance type -- @type Distance diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index 91c814937..f47e66b94 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -1,49 +1,163 @@ --- **Utilities** Enumerators. -- --- See the [Simulator Scripting Engine Documentation](https://wiki.hoggitworld.com/view/Simulator_Scripting_Engine_Documentation) on Hoggit for further explanation and examples. +-- An enumerator is a variable that holds a constant value. Enumerators are very useful because they make the code easier to read and to change in general. -- --- @module DCS +-- For example, instead of using the same value at multiple different places in your code, you should use a variable set to that value. +-- If, for whatever reason, the value needs to be changed, you only have to change the variable once and do not have to search through you code and reset +-- every value by hand. +-- +-- Another big advantage is that the LDT intellisense "knows" the enumerators. So you can use the autocompletion feature and do not have to keep all the +-- values in your head or look them up in the docs. +-- +-- DCS itself provides a lot of enumerators for various things. See [Enumerators](https://wiki.hoggitworld.com/view/Category:Enumerators) on Hoggit. +-- +-- Other Moose classe also have enumerators. For example, the AIRBASE class has enumerators for airbase names. +-- +-- @module ENUMS -- @image MOOSE.JPG +--- [DCS Enum world](https://wiki.hoggitworld.com/view/DCS_enum_world) +-- @type ENUMS +--- Because ENUMS are just better practice. +-- +-- The ENUMS class adds some handy variables, which help you to make your code better and more general. +-- +-- @field #ENUMS ENUMS = {} +--- Rules of Engagement. +-- @type ENUMS.ROE +-- @field #number WeaponFree AI will engage any enemy group it detects. Target prioritization is based based on the threat of the target. +-- @field #number OpenFireWeaponFree AI will engage any enemy group it detects, but will prioritize targets specified in the groups tasking. +-- @field #number OpenFire AI will engage only targets specified in its taskings. +-- @field #number ReturnFire AI will only engage threats that shoot first. +-- @field #number WeaponHold AI will hold fire under all circumstances. ENUMS.ROE = { - HoldFire = 1, - ReturnFire = 2, - OpenFire = 3, - WeaponFree = 4 + WeaponFree=0, + OpenFireWeaponFree=1, + OpenFire=2, + ReturnFire=3, + WeaponHold=4, } - + +--- Reaction On Threat. +-- @type ENUMS.ROT +-- @field #number NoReaction No defensive actions will take place to counter threats. +-- @field #number PassiveDefense AI will use jammers and other countermeasures in an attempt to defeat the threat. AI will not attempt a maneuver to defeat a threat. +-- @field #number EvadeFire AI will react by performing defensive maneuvers against incoming threats. AI will also use passive defense. +-- @field #number BypassAndEscape AI will attempt to avoid enemy threat zones all together. This includes attempting to fly above or around threats. +-- @field #number AllowAbortMission If a threat is deemed severe enough the AI will abort its mission and return to base. ENUMS.ROT = { - NoReaction = 1, - PassiveDefense = 2, - EvadeFire = 3, - Vertical = 4 + NoReaction=0, + PassiveDefense=1, + EvadeFire=2, + BypassAndEscape=3, + AllowAbortMission=4, } +--- Weapon types. See the [Weapon Flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) enumerotor on hoggit wiki. +-- @type ENUMS.WeaponFlag ENUMS.WeaponFlag={ - -- Auto - Auto=1073741822, -- Bombs - LGB=2, - TvGB=4, - SNSGB=8, - HEBomb=16, - Penetrator=32, - NapalmBomb=64, - FAEBomb=128, - ClusterBomb=256, - Dispencer=512, - CandleBomb=1024, - ParachuteBomb=2147483648, - GuidedBomb=14, -- (LGB + TvGB + SNSGB) - AnyUnguidedBomb=2147485680, -- (HeBomb + Penetrator + NapalmBomb + FAEBomb + ClusterBomb + Dispencer + CandleBomb + ParachuteBomb) - AnyBomb=2147485694, -- (GuidedBomb + AnyUnguidedBomb) + LGB = 2, + TvGB = 4, + SNSGB = 8, + HEBomb = 16, + Penetrator = 32, + NapalmBomb = 64, + FAEBomb = 128, + ClusterBomb = 256, + Dispencer = 512, + CandleBomb = 1024, + ParachuteBomb = 2147483648, -- Rockets - LightRocket=2048, - MarkerRocket=4096, - CandleRocket=8192, - HeavyRocket=16384, - AnyRocket=30720 -- (LightRocket + MarkerRocket + CandleRocket + HeavyRocket) + LightRocket = 2048, + MarkerRocket = 4096, + CandleRocket = 8192, + HeavyRocket = 16384, + -- Air-To-Surface Missiles + AntiRadarMissile = 32768, + AntiShipMissile = 65536, + AntiTankMissile = 131072, + FireAndForgetASM = 262144, + LaserASM = 524288, + TeleASM = 1048576, + CruiseMissile = 2097152, + AntiRadarMissile2 = 1073741824, + -- Air-To-Air Missiles + SRAM = 4194304, + MRAAM = 8388608, + LRAAM = 16777216, + IR_AAM = 33554432, + SAR_AAM = 67108864, + AR_AAM = 134217728, + --- Guns + GunPod = 268435456, + BuiltInCannon = 536870912, + --- + -- Combinations + -- + -- Bombs + GuidedBomb = 14, -- (LGB + TvGB + SNSGB) + AnyUnguidedBomb = 2147485680, -- (HeBomb + Penetrator + NapalmBomb + FAEBomb + ClusterBomb + Dispencer + CandleBomb + ParachuteBomb) + AnyBomb = 2147485694, -- (GuidedBomb + AnyUnguidedBomb) + --- Rockets + AnyRocket = 30720, -- LightRocket + MarkerRocket + CandleRocket + HeavyRocket + --- Air-To-Surface Missiles + GuidedASM = 1572864, -- (LaserASM + TeleASM) + TacticalASM = 1835008, -- (GuidedASM + FireAndForgetASM) + AnyASM = 4161536, -- (AntiRadarMissile + AntiShipMissile + AntiTankMissile + FireAndForgetASM + GuidedASM + CruiseMissile) + AnyASM2 = 1077903360, -- 4161536+1073741824, + --- Air-To-Air Missiles + AnyAAM = 264241152, -- IR_AAM + SAR_AAM + AR_AAM + SRAAM + MRAAM + LRAAM + AnyAutonomousMissile = 36012032, -- IR_AAM + AntiRadarMissile + AntiShipMissile + FireAndForgetASM + CruiseMissile + AnyMissile = 268402688, -- AnyASM + AnyAAM + --- Guns + Cannons = 805306368, -- GUN_POD + BuiltInCannon + --- + -- Even More Genral + Auto = 3221225470, -- Any Weapon (AnyBomb + AnyRocket + AnyMissile + Cannons) + AutoDCS = 1073741822, -- Something if often see + AnyAG = 2956984318, -- Any Air-To-Ground Weapon + AnyAA = 264241152, -- Any Air-To-Air Weapon + AnyUnguided = 2952822768, -- Any Unguided Weapon + AnyGuided = 268402702, -- Any Guided Weapon +} + +--- Mission tasks. +-- @type ENUMS.MissionTask +-- @field #string NOTHING No special task. Group can perform the minimal tasks: Orbit, Refuelling, Follow and Aerobatics. +-- @field #string AFAC Forward Air Controller Air. Can perform the tasks: Attack Group, Attack Unit, FAC assign group, Bombing, Attack Map Object. +-- @field #string ANTISHIPSTRIKE Naval ops. Can perform the tasks: Attack Group, Attack Unit. +-- @field #string AWACS AWACS. +-- @field #string CAP Combat Air Patrol. +-- @field #string CAS Close Air Support. +-- @field #string ESCORT Escort another group. +-- @field #string FIGHTERSWEEP Fighter sweep. +-- @field #string GROUNDATTACK Ground attack. +-- @field #string INTERCEPT Intercept. +-- @field #string PINPOINTSTRIKE Pinpoint strike. +-- @field #string RECONNAISSANCE Reconnaissance mission. +-- @field #string REFUELING Refueling mission. +-- @field #string RUNWAYATTACK Attack the runway of an airdrome. +-- @field #string SEAD Suppression of Enemy Air Defenses. +-- @field #string TRANSPORT Troop transport. +ENUMS.MissionTask={ + NOTHING="Nothing", + AFAC="AFAC", + ANTISHIPSTRIKE="Antiship Strike", + AWACS="AWACS", + CAP="CAP", + CAS="CAS", + ESCORT="Escort", + FIGHTERSWEEP="Fighter Sweep", + GROUNDATTACK="Ground Attack", + INTERCEPT="Intercept", + PINPOINTSTRIKE="Pinpoint Strike", + RECONNAISSANCE="Reconnaissance", + REFUELING="Refueling", + RUNWAYATTACK="Runway Attack", + SEAD="SEAD", + TRANSPORT="Transport", } \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 97f17d59a..9f34f8cf6 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -312,7 +312,6 @@ function CONTROLLABLE:ClearTasks() if DCSControllable then local Controller = self:_GetController() - env.info("FF clearing tasks!") Controller:resetTask() return self end @@ -450,7 +449,6 @@ end -- @param #number lastWayPoint Last waypoint. -- return DCS#Task function CONTROLLABLE:TaskCondition( time, userFlag, userFlagValue, condition, duration, lastWayPoint ) - self:F2( { time, userFlag, userFlagValue, condition, duration, lastWayPoint } ) --[[ StopCondition = { @@ -471,7 +469,6 @@ function CONTROLLABLE:TaskCondition( time, userFlag, userFlagValue, condition, d DCSStopCondition.duration = duration DCSStopCondition.lastWayPoint = lastWayPoint - self:T3( { DCSStopCondition } ) return DCSStopCondition end @@ -481,11 +478,8 @@ end -- @param DCS#DCSStopCondition DCSStopCondition -- @return DCS#Task function CONTROLLABLE:TaskControlled( DCSTask, DCSStopCondition ) - self:F2( { DCSTask, DCSStopCondition } ) - local DCSTaskControlled - - DCSTaskControlled = { + local DCSTaskControlled = { id = 'ControlledTask', params = { task = DCSTask, @@ -493,7 +487,6 @@ function CONTROLLABLE:TaskControlled( DCSTask, DCSStopCondition ) } } - self:T3( { DCSTaskControlled } ) return DCSTaskControlled end @@ -502,22 +495,14 @@ end -- @param DCS#TaskArray DCSTasks Array of @{DCSTasking.Task#Task} -- @return DCS#Task function CONTROLLABLE:TaskCombo( DCSTasks ) - self:F2( { DCSTasks } ) - local DCSTaskCombo - - DCSTaskCombo = { + local DCSTaskCombo = { id = 'ComboTask', params = { tasks = DCSTasks } } - - for TaskID, Task in ipairs( DCSTasks ) do - self:T( Task ) - end - - self:T3( { DCSTaskCombo } ) + return DCSTaskCombo end @@ -526,11 +511,8 @@ end -- @param DCS#Command DCSCommand -- @return DCS#Task function CONTROLLABLE:TaskWrappedAction( DCSCommand, Index ) - self:F2( { DCSCommand } ) - local DCSTaskWrappedAction - - DCSTaskWrappedAction = { + local DCSTaskWrappedAction = { id = "WrappedAction", enabled = true, number = Index or 1, @@ -540,7 +522,6 @@ function CONTROLLABLE:TaskWrappedAction( DCSCommand, Index ) }, } - self:T3( { DCSTaskWrappedAction } ) return DCSTaskWrappedAction end @@ -703,7 +684,6 @@ end -- @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= { @@ -731,7 +711,6 @@ end -- @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={}} @@ -750,7 +729,6 @@ end -- @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={}} @@ -771,7 +749,6 @@ end -- @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}} @@ -791,28 +768,56 @@ end -- @param #number Delay (Optional) Delay in seconds before the callsign is set. Default is immediately. -- @return #CONTROLLABLE self function CONTROLLABLE:CommandEPLRS(SwitchOnOff, Delay) - self:F() if SwitchOnOff==nil then SwitchOnOff=true end - -- ID - local _id=self:GetID() - -- Command to set the callsign. - local CommandEPLRS={id='EPLRS', params={value=SwitchOnOff, groupId=_id}} + local CommandEPLRS={ + id='EPLRS', + params={ + value=SwitchOnOff, + groupId=self:GetID() + } + } if Delay and Delay>0 then SCHEDULER:New(nil, self.CommandEPLRS, {self, SwitchOnOff}, Delay) else - self:T(string.format("EPLRS=%s for controllable %s (id=%s)", tostring(SwitchOnOff), tostring(self:GetName()), tostring(_id))) + self:T(string.format("EPLRS=%s for controllable %s (id=%s)", tostring(SwitchOnOff), tostring(self:GetName()), tostring(self:GetID()))) self:SetCommand(CommandEPLRS) end return self end +--- Set radio frequency. See [DCS command EPLRS](https://wiki.hoggitworld.com/view/DCS_command_setFrequency) +-- @param #CONTROLLABLE self +-- @param #number Frequency Radio frequency in MHz. +-- @param #number Modulation Radio modulation. Default `radio.modulation.AM`. +-- @param #number Delay (Optional) Delay in seconds before the frequncy is set. Default is immediately. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandSetFrequency(Frequency, Modulation, Delay) + + local CommandSetFrequency = { + id = 'SetFrequency', + params = { + frequency = Frequency, + modulation = Modulation or radio.modulation.AM, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandSetFrequency, {self, Frequency, Modulation}, Delay) + else + self:SetCommand(CommandSetFrequency) + end + + return self +end + + --- Set EPLRS data link on/off. -- @param #CONTROLLABLE self -- @param #boolean SwitchOnOff If true (or nil) switch EPLRS on. If false switch off. @@ -820,14 +825,20 @@ end -- @return #table Task wrapped action. function CONTROLLABLE:TaskEPLRS(SwitchOnOff, idx) - -- ID - local _id=self:GetID() + if SwitchOnOff==nil then + SwitchOnOff=true + end -- Command to set the callsign. - local CommandEPLRS={id='EPLRS', params={value=SwitchOnOff, groupId=_id}} + local CommandEPLRS={ + id='EPLRS', + params={ + value=SwitchOnOff, + groupId=self:GetID() + } + } return self:TaskWrappedAction(CommandEPLRS, idx or 1) - end @@ -835,16 +846,17 @@ end --- (AIR) Attack a Controllable. -- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. +-- @param Wrapper.Group#GROUP AttackGroup The Group to be attacked. -- @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 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 DCS#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. -- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. +-- @param #boolean GroupAttack (Optional) If true, attack as group. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) +function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit, GroupAttack ) + --self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) -- AttackGroup = { -- id = 'AttackGroup', @@ -868,15 +880,15 @@ function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, At weaponType = WeaponType or 1073741822, expend = WeaponExpend or "Auto", attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, + attackQty = AttackQty or 1, directionEnabled = Direction and true or false, - direction = Direction and math.rad(Direction) or nil, + direction = Direction and math.rad(Direction) or 0, altitudeEnabled = Altitude and true or false, altitude = Altitude, + groupAttack = GroupAttack and true or false, }, - }, + } - self:T3( { DCSTask } ) return DCSTask end @@ -891,17 +903,15 @@ end -- @param #number WeaponType (optional) The WeaponType. See [DCS Enumerator Weapon Type](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) on Hoggit. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType) - self:F2({self.ControllableName, AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType}) - local DCSTask - DCSTask = { + local DCSTask = { id = 'AttackUnit', params = { unitId = AttackUnit:GetID(), groupAttack = GroupAttack and GroupAttack or false, expend = WeaponExpend or "Auto", directionEnabled = Direction and true or false, - direction = Direction and math.rad(Direction) or nil, + direction = Direction and math.rad(Direction) or 0, altitudeEnabled = Altitude and true or false, altitude = Altitude, attackQtyLimit = AttackQty and true or false, @@ -909,9 +919,7 @@ function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, Atta weaponType = WeaponType or 1073741822, } } - - self:T3( DCSTask ) - + return DCSTask end @@ -928,16 +936,8 @@ end -- @param #boolean Divebomb (optional) Perform dive bombing. Default false. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb ) - self:F( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb } ) - local _attacktype=nil - if Divebomb then - _attacktype="Dive" - end - - - local DCSTask - DCSTask = { + local DCSTask = { id = 'Bombing', params = { point = Vec2, @@ -946,17 +946,16 @@ function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, D groupAttack = GroupAttack and GroupAttack or false, expend = WeaponExpend or "Auto", attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, + attackQty = AttackQty or 1, directionEnabled = Direction and true or false, - direction = Direction and math.rad(Direction) or nil, + direction = Direction and math.rad(Direction) or 0, altitudeEnabled = Altitude and true or false, - altitude = Altitude, + altitude = Altitude or 2000, weaponType = WeaponType or 1073741822, - attackType = _attacktype, + attackType = Divebomb and "Dive" or nil, }, } - self:F( { TaskBombing=DCSTask } ) return DCSTask end @@ -971,10 +970,8 @@ end -- @param #number WeaponType (Optional) The WeaponType. Default Auto=1073741822. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType ) - self:F2( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType } ) - local DCSTask - DCSTask = { + local DCSTask = { id = 'AttackMapObject', params = { point = Vec2, @@ -985,14 +982,13 @@ function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, Atta attackQtyLimit = AttackQty and true or false, attackQty = AttackQty, directionEnabled = Direction and true or false, - direction = Direction, + direction = Direction and math.rad(Direction) or 0, altitudeEnabled = Altitude and true or false, altitude = Altitude, weaponType = WeaponType or 1073741822, }, - }, + } - self:T3( { DCSTask } ) return DCSTask end @@ -1009,7 +1005,6 @@ end -- @param #number CarpetLength (optional) default to 500 m. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskCarpetBombing(Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, CarpetLength) - self:F2( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, CarpetLength } ) -- Build Task Structure local DCSTask = { @@ -1026,7 +1021,7 @@ function CONTROLLABLE:TaskCarpetBombing(Vec2, GroupAttack, WeaponExpend, AttackQ attackQtyLimit = AttackQty and true or false, attackQty = AttackQty, directionEnabled = Direction and true or false, - direction = Direction and math.rad(Direction) or nil, + direction = Direction and math.rad(Direction) or 0, altitudeEnabled = Altitude and true or false, altitude = Altitude, } @@ -1064,9 +1059,9 @@ end --- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 The point where to wait. Needs to have x and y components. --- @param Core.Set#SET_GROUP GroupSetForEmparking Set of groups to embark. +-- @param Core.Set#SET_GROUP GroupSetForEmbarking Set of groups to embark. -- @param #number Duration (Optional) The maximum duration in seconds to wait until all groups have embarked. --- @param Core.Set#SET_GROUP (Optional) DistributionGroupSet Set of groups identifying the groups needing to board specific helicopters. +-- @param Core.Set#SET_GROUP DistributionGroupSet (Optional) Set of groups identifying the groups needing to board specific helicopters. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, DistributionGroupSet) @@ -1107,7 +1102,6 @@ function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, Distri } } - self:T3( { DCSTask } ) return DCSTask end @@ -1218,7 +1212,6 @@ end -- @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, GroupAttack) - self:F2( { self.ControllableName, Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack } ) local DCSTask = { id = 'BombingRunway', @@ -1227,8 +1220,8 @@ function CONTROLLABLE:TaskBombingRunway(Airbase, WeaponType, WeaponExpend, Attac weaponType = WeaponType or ENUMS.WeaponFlag.AnyBomb, expend = WeaponExpend or AI.Task.WeaponExpend.ALL, attackQty = AttackQty or 1, - direction = Direction and math.rad(Direction) or nil, - groupAttack = GroupAttack and GroupAttack or false, + direction = Direction and math.rad(Direction) or 0, + groupAttack = GroupAttack and true or false, }, } @@ -1241,7 +1234,10 @@ end -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskRefueling() - local DCSTask={id='Refueling', params={}} + local DCSTask={ + id='Refueling', + params={} + } return DCSTask end @@ -1249,7 +1245,7 @@ end --- (AIR HELICOPTER) Landing at the ground. For helicopters only. -- @param #CONTROLLABLE self --- @param DCS#Vec2 Point The point where to land. +-- @param DCS#Vec2 Vec2 The point where to land. -- @param #number Duration The duration in seconds to stay on the ground. -- @return #CONTROLLABLE self function CONTROLLABLE:TaskLandAtVec2(Vec2, Duration) @@ -1262,6 +1258,7 @@ function CONTROLLABLE:TaskLandAtVec2(Vec2, Duration) duration = Duration, }, } + return DCSTask end @@ -1271,18 +1268,12 @@ end -- @param #number Duration The duration in seconds to stay on the ground. -- @return #CONTROLLABLE self function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) - self:F2( { self.ControllableName, Zone, Duration, RandomPoint } ) - local Point - if RandomPoint then - Point = Zone:GetRandomVec2() - else - Point = Zone:GetVec2() - end + -- Get landing point + local Point=RandomPoint and Zone:GetRandomVec2() or Zone:GetVec2() - local DCSTask = self:TaskLandAtVec2( Point, Duration ) + local DCSTask = CONTROLLABLE.TaskLandAtVec2( self, Point, Duration ) - self:T3( DCSTask ) return DCSTask end @@ -1343,7 +1334,6 @@ end -- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. Default {"Air"}. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) - self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes } ) -- Escort = { -- id = 'Escort', @@ -1368,9 +1358,8 @@ function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, E engagementDistMax = EngagementDistance, targetTypes = TargetTypes or {"Air"}, }, - }, + } - self:T3( { DCSTask } ) return DCSTask end @@ -1385,7 +1374,6 @@ end -- @param #number WeaponType (optional) Enum for weapon type ID. This value is only required if you want the group firing to use a specific weapon, for instance using the task on a ship to force it to fire guided missiles at targets within cannon range. See http://wiki.hoggit.us/view/DCS_enum_weapon_flag -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType ) - self:F2( { self.ControllableName, Vec2, Radius, AmmoCount, WeaponType } ) -- FireAtPoint = { -- id = 'FireAtPoint', @@ -1397,13 +1385,12 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType ) -- } -- } - local DCSTask - DCSTask = { + local DCSTask = { id = 'FireAtPoint', params = { - point = Vec2, - zoneRadius = Radius, - expendQty = 100, -- dummy value + point = Vec2, + zoneRadius = Radius, + expendQty = 100, -- dummy value expendQtyEnabled = false, } } @@ -1436,13 +1423,16 @@ end -- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. -- If the task is assigned to the controllable lead unit will be a FAC. -- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param DCS#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @param Wrapper.Group#GROUP AttackGroup Target GROUP object. +-- @param #number WeaponType Bitmask of weapon types, which are allowed to use. +-- @param DCS#AI.Task.Designation Designation (Optional) Designation type. +-- @param #boolean Datalink (Optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @param #number Frequency Frequency used to communicate with the FAC. +-- @param #number Modulation Modulation of radio for communication. +-- @param #number CallsignName Callsign enumerator name of the FAC. +-- @param #number CallsignNumber Callsign number, e.g. Axeman-**1**. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, Designation, Datalink } ) +function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink, Frequency, Modulation, CallsignName, CallsignNumber ) local DCSTask = { id = 'FAC_AttackGroup', @@ -1451,10 +1441,13 @@ function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, weaponType = WeaponType, designation = Designation, datalink = Datalink, + frequency = Frequency, + modulation = Modulation, + callname = CallsignName, + number = CallsignNumber, } } - self:T3( { DCSTask } ) return DCSTask end @@ -1467,7 +1460,6 @@ end -- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority ) - self:F2( { self.ControllableName, Distance, TargetTypes, Priority } ) local DCSTask = { id = 'EngageTargets', @@ -1479,7 +1471,6 @@ function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority } } - self:T3( { DCSTask } ) return DCSTask end @@ -1489,11 +1480,10 @@ end -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 2D-coordinates of the zone. -- @param DCS#Distance Radius Radius of the zone. --- @param DCS#AttributeNameArray (Optional) TargetTypes Array of target categories allowed to engage. Default {"Air"}. +-- @param DCS#AttributeNameArray TargetTypes (Optional) Array of target categories allowed to engage. Default {"Air"}. -- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, Priority ) - self:F2( { self.ControllableName, Vec2, Radius, TargetTypes, Priority } ) local DCSTask = { id = 'EngageTargetsInZone', @@ -1505,7 +1495,6 @@ function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, } } - self:T3( { DCSTask } ) return DCSTask end @@ -1522,7 +1511,6 @@ end -- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) -- EngageControllable = { -- id = 'EngageControllable ', @@ -1554,9 +1542,8 @@ function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, attackQty = AttackQty, priority = Priority or 1, }, - }, + } - self:T3( { DCSTask } ) return DCSTask end @@ -1574,7 +1561,6 @@ end -- @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. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack ) - self:F2( { self.ControllableName, EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack } ) local DCSTask = { id = 'EngageUnit', @@ -1592,9 +1578,8 @@ function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, attackQty = AttackQty, controllableAttack = ControllableAttack, }, - }, + } - self:T3( { DCSTask } ) return DCSTask end @@ -1604,11 +1589,12 @@ end -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskAWACS( ) - self:F2( { self.ControllableName } ) - local DCSTask = {id = 'AWACS', params = {}} + local DCSTask = { + id = 'AWACS', + params = {}, + } - self:T3( { DCSTask } ) return DCSTask end @@ -1617,11 +1603,12 @@ end -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskTanker( ) - self:F2( { self.ControllableName } ) - local DCSTask = {id = 'Tanker', params = {}} + local DCSTask = { + id = 'Tanker', + params = {}, + } - self:T3( { DCSTask } ) return DCSTask end @@ -1632,11 +1619,12 @@ end -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEWR( ) - self:F2( { self.ControllableName } ) - local DCSTask = {id = 'EWR', params = {}} + local DCSTask = { + id = 'EWR', + params = {}, + } - self:T3( { DCSTask } ) return DCSTask end @@ -1654,7 +1642,6 @@ end -- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, Priority, Designation, Datalink } ) local DCSTask = { id = 'FAC_EngageControllable', @@ -1667,7 +1654,6 @@ function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponT } } - self:T3( { DCSTask } ) return DCSTask end @@ -1680,7 +1666,6 @@ end -- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) - self:F2( { self.ControllableName, Radius, Priority } ) -- FAC = { -- id = 'FAC', @@ -1690,15 +1675,14 @@ function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) -- } -- } - local DCSTask - DCSTask = { id = 'FAC', + local DCSTask = { + id = 'FAC', params = { radius = Radius, priority = Priority } } - self:T3( { DCSTask } ) return DCSTask end @@ -1799,30 +1783,6 @@ function CONTROLLABLE:TaskDisembarkFromTransport(Coordinate, Radius) end ]] ---- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. --- @param #CONTROLLABLE self --- @param DCS#Vec2 Point The point where to wait. --- @param #number Duration The duration in seconds to wait. --- @param #CONTROLLABLE EmbarkingControllable The controllable to be embarked. --- @return DCS#Task The DCS task structure -function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) - self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } ) - - local DCSTask - DCSTask = { id = 'Embarking', - params = { x = Point.x, - y = Point.y, - duration = Duration, - controllablesForEmbarking = { EmbarkingControllable.ControllableID }, - durationFlag = true, - distributionFlag = false, - distribution = {}, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end --- (GROUND) Embark to a Transport landed at a location. -- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. @@ -1831,10 +1791,8 @@ end -- @param #number Radius The radius of the embarking zone around the Point. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) - self:F2( { self.ControllableName, Point, Radius } ) - local DCSTask --DCS#Task - DCSTask = { + local DCSTask = { id = 'EmbarkToTransport', params = { point = Point, @@ -1844,7 +1802,6 @@ function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) } } - self:T3( { DCSTask } ) return DCSTask end @@ -1899,12 +1856,9 @@ end -- function CONTROLLABLE:TaskFunction( FunctionString, ... ) - local DCSTask - + -- Script 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: (.*)") self:SetState( self, ArgumentKey, arg ) @@ -1914,12 +1868,10 @@ function CONTROLLABLE:TaskFunction( FunctionString, ... ) DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" end - DCSTask = self:TaskWrappedAction(self:CommandDoScript(table.concat( DCSScript ))) - - self:T( DCSTask ) - + -- DCS task. + local DCSTask = self:TaskWrappedAction(self:CommandDoScript(table.concat( DCSScript ))) + return DCSTask - end @@ -1929,12 +1881,12 @@ end -- @param #table TaskMission A table containing the mission task. -- @return DCS#Task function CONTROLLABLE:TaskMission( TaskMission ) - self:F2( Points ) - local DCSTask - DCSTask = { id = 'Mission', params = { TaskMission, }, } + local DCSTask = { + id = 'Mission', + params = { TaskMission, }, + } - self:T3( { DCSTask } ) return DCSTask end @@ -2995,6 +2947,51 @@ end -- Options +--- Set option. +-- @param #CONTROLLABLE self +-- @param #number OptionID ID/Type of the option. +-- @param #number OptionValue Value of the option +-- @return #CONTROLLABLE self +function CONTROLLABLE:SetOption(OptionID, OptionValue) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + Controller:setOption( OptionID, OptionValue ) + + return self + end + + return nil +end + +--- Set option for Rules of Engagement (ROE). +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #number ROEvalue ROE value. See ENUMS.ROE. +-- @return Wrapper.Controllable#CONTROLLABLE self +function CONTROLLABLE:OptionROE(ROEvalue) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption(AI.Option.Air.id.ROE, ROEvalue ) + elseif self:IsGround() then + Controller:setOption(AI.Option.Ground.id.ROE, ROEvalue ) + elseif self:IsShip() then + Controller:setOption(AI.Option.Naval.id.ROE, ROEvalue ) + end + + return self + end + + return nil +end + --- Can the CONTROLLABLE hold their weapons? -- @param #CONTROLLABLE self -- @return #boolean @@ -3237,6 +3234,27 @@ function CONTROLLABLE:OptionROTNoReaction() return nil end +--- Set Reation On Threat behaviour. +-- @param #CONTROLLABLE self +-- @param #number ROTvalue ROT value. See ENUMS.ROT. +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROT(ROTvalue) + 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.REACTION_ON_THREAT, ROTvalue ) + end + + return self + end + + return nil +end + --- Can the CONTROLLABLE evade using passive defenses? -- @param #CONTROLLABLE self -- @return #boolean @@ -3515,8 +3533,76 @@ function CONTROLLABLE:OptionKeepWeaponsOnThreat() return nil end +--- Prohibit Afterburner. +-- @param #CONTROLLABLE self +-- @param #boolean Prohibit If true or nil, prohibit. If false, do not prohibit. +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionProhibitAfterburner(Prohibit) + self:F2( { self.ControllableName } ) + + if Prohibit==nil then + Prohibit=true + end + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.PROHIBIT_AB, Prohibit) + end + + return self +end + +--- Defines the usage of Electronic Counter Measures by airborne forces. Disables the ability for AI to use their ECM. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_Never() + self:F2( { self.ControllableName } ) + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.ECM_USING, 0) + end + + return self +end + +--- Defines the usage of Electronic Counter Measures by airborne forces. If the AI is actively being locked by an enemy radar they will enable their ECM jammer. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_OnlyLockByRadar() + self:F2( { self.ControllableName } ) + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.ECM_USING, 1) + end + + return self +end +--- Defines the usage of Electronic Counter Measures by airborne forces. If the AI is being detected by a radar they will enable their ECM. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_DetectedLockByRadar() + self:F2( { self.ControllableName } ) + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.ECM_USING, 2) + end + + return self +end + +--- Defines the usage of Electronic Counter Measures by airborne forces. AI will leave their ECM on all the time. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_AlwaysOn() + self:F2( { self.ControllableName } ) + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.ECM_USING, 3) + end + + return self +end --- Retrieve the controllable mission and allow to place function hooks within the mission waypoint plan. -- Use the method @{Wrapper.Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints. diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 484dc160b..9ac9211c2 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -440,9 +440,16 @@ function GROUP:Destroy( GenerateEvent, delay ) end ---- Returns category of the DCS Group. +--- Returns category of the DCS Group. Returns one of +-- +-- * Group.Category.AIRPLANE +-- * Group.Category.HELICOPTER +-- * Group.Category.GROUND +-- * Group.Category.SHIP +-- * Group.Category.TRAIN +-- -- @param #GROUP self --- @return DCS#Group.Category The category ID +-- @return DCS#Group.Category The category ID. function GROUP:GetCategory() self:F2( self.GroupName ) @@ -458,7 +465,7 @@ end --- Returns the category name of the #GROUP. -- @param #GROUP self --- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship +-- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship, Train. function GROUP:GetCategoryName() self:F2( self.GroupName ) @@ -469,6 +476,7 @@ function GROUP:GetCategoryName() [Group.Category.HELICOPTER] = "Helicopter", [Group.Category.GROUND] = "Ground Unit", [Group.Category.SHIP] = "Ship", + [Group.Category.TRAIN] = "Train", } local GroupCategory = DCSGroup:getCategory() self:T3( GroupCategory ) @@ -867,10 +875,15 @@ end --- Activates a late activated GROUP. -- @param #GROUP self +-- @param #number delay Delay in seconds, before the group is activated. -- @return #GROUP self -function GROUP:Activate() +function GROUP:Activate(delay) self:F2( { self.GroupName } ) - trigger.action.activateGroup( self:GetDCSObject() ) + if delay and delay>0 then + self:ScheduleOnce(delay, GROUP.Activate, self) + else + trigger.action.activateGroup( self:GetDCSObject() ) + end return self end diff --git a/Moose Development/Moose/Wrapper/Identifiable.lua b/Moose Development/Moose/Wrapper/Identifiable.lua index 7943fe049..5b340aec8 100644 --- a/Moose Development/Moose/Wrapper/Identifiable.lua +++ b/Moose Development/Moose/Wrapper/Identifiable.lua @@ -90,7 +90,6 @@ end --- Returns the type name of the DCS Identifiable. -- @param #IDENTIFIABLE self -- @return #string The type name of the DCS Identifiable. --- @return #nil The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetTypeName() self:F2( self.IdentifiableName ) @@ -107,9 +106,17 @@ function IDENTIFIABLE:GetTypeName() end ---- Returns category of the DCS Identifiable. +--- Returns object category of the DCS Identifiable. One of +-- +-- * Object.Category.UNIT = 1 +-- * Object.Category.WEAPON = 2 +-- * Object.Category.STATIC = 3 +-- * Object.Category.BASE = 4 +-- * Object.Category.SCENERY = 5 +-- * Object.Category.Cargo = 6 +-- -- @param #IDENTIFIABLE self --- @return DCS#Object.Category The category ID +-- @return DCS#Object.Category The category ID, i.e. a number. function IDENTIFIABLE:GetCategory() self:F2( self.ObjectName ) diff --git a/Moose Development/Moose/Wrapper/Static.lua b/Moose Development/Moose/Wrapper/Static.lua index d26aca44c..64a12a98a 100644 --- a/Moose Development/Moose/Wrapper/Static.lua +++ b/Moose Development/Moose/Wrapper/Static.lua @@ -4,7 +4,7 @@ -- -- ### Author: **FlightControl** -- --- ### Contributions: +-- ### Contributions: **funkyfranky** -- -- === -- @@ -48,6 +48,10 @@ STATIC = { } +--- Register a static object. +-- @param #STATIC self +-- @param #string StaticName Name of the static object. +-- @return #STATIC self function STATIC:Register( StaticName ) local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) self.StaticName = StaticName @@ -71,21 +75,19 @@ end -- @param #STATIC self -- @param #string StaticName Name of the DCS **Static** as defined within the Mission Editor. -- @param #boolean RaiseError Raise an error if not found. --- @return #STATIC -function STATIC:FindByName( StaticName, RaiseError ) +-- @return #STATIC self or *nil* +function STATIC:FindByName( StaticName ) + + -- Find static in DB. local StaticFound = _DATABASE:FindStatic( StaticName ) + -- Set static name. self.StaticName = StaticName if StaticFound then - StaticFound:F3( { StaticName } ) return StaticFound end - if RaiseError == nil or RaiseError == true then - error( "STATIC not found for: " .. StaticName ) - end - return nil end diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 9be25e3f8..a7b2316b9 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -424,9 +424,9 @@ function UNIT:GetRange() local Desc = self:GetDesc() if Desc then - local Range = Desc.range --This is in nautical miles for some reason. But should check again! + local Range = Desc.range --This is in kilometers (not meters) for some reason. But should check again! if Range then - Range=UTILS.NMToMeters(Range) + Range=Range*1000 -- convert to meters. else Range=10000000 --10.000 km if no range end @@ -436,6 +436,64 @@ function UNIT:GetRange() return nil end +--- Check if the unit is refuelable. Also retrieves the refuelling system (boom or probe) if applicable. +-- @param #UNIT self +-- @return #boolean If true, unit is refuelable (checks for the attribute "Refuelable"). +-- @return #number Refueling system (if any): 0=boom, 1=probe. +function UNIT:IsRefuelable() + self:F2( self.UnitName ) + + local refuelable=self:HasAttribute("Refuelable") + + local system=nil + + local Desc=self:GetDesc() + if Desc and Desc.tankerType then + system=Desc.tankerType + end + + return refuelable, system +end + +--- Check if the unit is a tanker. Also retrieves the refuelling system (boom or probe) if applicable. +-- @param #UNIT self +-- @return #boolean If true, unit is refuelable (checks for the attribute "Refuelable"). +-- @return #number Refueling system (if any): 0=boom, 1=probe. +function UNIT:IsTanker() + self:F2( self.UnitName ) + + local tanker=self:HasAttribute("Tankers") + + local system=nil + + if tanker then + + local Desc=self:GetDesc() + if Desc and Desc.tankerType then + system=Desc.tankerType + end + + local typename=self:GetTypeName() + + -- Some hard coded data as this is not in the descriptors... + if typename=="IL-78M" then + system=1 --probe + elseif typename=="KC130" then + system=1 --probe + elseif typename=="KC135BDA" then + system=1 --probe + elseif typename=="KC135MPRS" then + system=1 --probe + elseif typename=="S-3B Tanker" then + system=1 --probe + end + + end + + return tanker, system +end + + --- Returns the unit's group if it exist and nil otherwise. -- @param Wrapper.Unit#UNIT self -- @return Wrapper.Group#GROUP The Group of the Unit. @@ -756,6 +814,27 @@ function UNIT:GetDamageRelative() return 1 end +--- Returns the category of the #UNIT from descriptor. Returns one of +-- +-- * Unit.Category.AIRPLANE +-- * Unit.Category.HELICOPTER +-- * Unit.Category.GROUND_UNIT +-- * Unit.Category.SHIP +-- * Unit.Category.STRUCTURE +-- +-- @param #UNIT self +-- @return #number Unit category from `getDesc().category`. +function UNIT:GetUnitCategory() + self:F3( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + if DCSUnit then + return DCSUnit:getDesc().category + end + + return nil +end + --- Returns the category name of the #UNIT. -- @param #UNIT self -- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship @@ -833,7 +912,9 @@ end -- * Threat level 10: Unit is AAA. -- -- --- @param #UNIT self +-- @param #UNIT self +-- @return #number Number between 0 (low threat level) and 10 (high threat level). +-- @return #string Some text. function UNIT:GetThreatLevel() From 457b30ed07577667ed20a15774a39713efa66e25 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 24 Apr 2020 16:46:46 +0200 Subject: [PATCH 467/485] ATIS v0.7.1 Fixed bug that windfrom < 0. --- Moose Development/Moose/Ops/ATIS.lua | 15 ++++++++++++--- Moose Development/Moose/Ops/Airboss.lua | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 0c34562c6..db2fa22a9 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -531,7 +531,7 @@ _ATIS={} --- ATIS class version. -- @field #string version -ATIS.version="0.7.0" +ATIS.version="0.7.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1230,13 +1230,22 @@ function ATIS:onafterBroadcast(From, Event, To) if self.windtrue then magvar=0 end + windFrom=windFrom-magvar + + -- Correct negative values. + if windFrom<0 then + windFrom=windFrom+360 + end - local WINDFROM=string.format("%03d", windFrom-magvar) + local WINDFROM=string.format("%03d", windFrom) local WINDSPEED=string.format("%d", UTILS.MpsToKnots(windSpeed)) - + + -- Report North as 0. if WINDFROM=="000" then WINDFROM="360" end + + env.info(string.format("FF WINDFROM = %s", tostring(WINDFROM))) if self.metric then WINDSPEED=string.format("%d", windSpeed) diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 6230b97cc..d604d7441 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -12002,7 +12002,7 @@ function AIRBOSS:_LSOgrade(playerData) ----------------- grade="OWO" points=2.0 - if G=="Unicorn" then + if N==0 then G="n/a" end elseif playerData.waveoff then From 863e239204706dbbdc2a934e7eba50329eea84b3 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 24 Apr 2020 16:53:15 +0200 Subject: [PATCH 468/485] Update Utils.lua --- Moose Development/Moose/Utilities/Utils.lua | 37 +++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 15ae1ac1b..2980550b9 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -65,7 +65,7 @@ CALLSIGN={ Enfield=1, Springfield=2, Uzi=3, - Cold=4, + Colt=4, Dodge=5, Ford=6, Chevy=7, @@ -437,8 +437,8 @@ UTILS.tostringLL = function( lat, lon, acc, DMS) end -- 024� 23' 12"N or 024� 23' 12.03"N - return string.format('%03d°', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%03d°', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi + return string.format('%03d°', latDeg)..string.format('%02d', latMin)..'\''..string.format(secFrmtStr, latSec)..'"'..latHemi..' ' + .. string.format('%03d°', lonDeg)..string.format('%02d', lonMin)..'\''..string.format(secFrmtStr, lonSec)..'"'..lonHemi else -- degrees, decimal minutes. latMin = UTILS.Round(latMin, acc) @@ -1098,5 +1098,36 @@ function UTILS.GetModulationName(Modulation) end +--- Get the callsign name from its enumerator value +-- @param #number Callsign The enumerator callsign. +-- @return #string The callsign name. +function UTILS.GetCallsignName(Callsign) + + for name, value in pairs(CALLSIGN.Aircraft) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.AWACS) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.JTAC) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.Tanker) do + if value==Callsign then + return name + end + end + + return "Unknown Calsing" +end -- Just a test to see commits in new environment for Wingthor \ No newline at end of file From 0a570da986625431c2fff5253d0e15b1d5c1bdaa Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 24 Apr 2020 21:16:42 +0200 Subject: [PATCH 469/485] Up --- Moose Development/Moose/DCS.lua | 10 +- .../Moose/Wrapper/Controllable.lua | 237 +++++++++--------- 2 files changed, 125 insertions(+), 122 deletions(-) diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index 0c8e87a23..206165bd3 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -47,9 +47,13 @@ do -- world -- @field S_EVENT_PLAYER_COMMENT -- @field S_EVENT_SHOOTING_START [https://wiki.hoggitworld.com/view/DCS_event_shooting_start](https://wiki.hoggitworld.com/view/DCS_event_shooting_start) -- @field S_EVENT_SHOOTING_END [https://wiki.hoggitworld.com/view/DCS_event_shooting_end](https://wiki.hoggitworld.com/view/DCS_event_shooting_end) - -- @field S_EVENT_MARK ADDED [https://wiki.hoggitworld.com/view/DCS_event_mark_added](https://wiki.hoggitworld.com/view/DCS_event_mark_added) - -- @field S_EVENT_MARK CHANGE [https://wiki.hoggitworld.com/view/DCS_event_mark_change](https://wiki.hoggitworld.com/view/DCS_event_mark_change) - -- @field S_EVENT_MARK REMOVE [https://wiki.hoggitworld.com/view/DCS_event_mark_remove](https://wiki.hoggitworld.com/view/DCS_event_mark_remove) + -- @field S_EVENT_MARK ADDED [https://wiki.hoggitworld.com/view/DCS_event_mark_added](https://wiki.hoggitworld.com/view/DCS_event_mark_added) DCS>=2.5.1 + -- @field S_EVENT_MARK CHANGE [https://wiki.hoggitworld.com/view/DCS_event_mark_change](https://wiki.hoggitworld.com/view/DCS_event_mark_change) DCS>=2.5.1 + -- @field S_EVENT_MARK REMOVE [https://wiki.hoggitworld.com/view/DCS_event_mark_remove](https://wiki.hoggitworld.com/view/DCS_event_mark_remove) DCS>=2.5.1 + -- @field S_EVENT_KILL [https://wiki.hoggitworld.com/view/DCS_event_kill](https://wiki.hoggitworld.com/view/DCS_event_kill) DCS>=2.5.6 + -- @field S_EVENT_SCORE [https://wiki.hoggitworld.com/view/DCS_event_score](https://wiki.hoggitworld.com/view/DCS_event_score) DCS>=2.5.6 + -- @field S_EVENT_UNIT_LOST [https://wiki.hoggitworld.com/view/DCS_event_unit_lost](https://wiki.hoggitworld.com/view/DCS_event_unit_lost) DCS>=2.5.6 + -- @field S_EVENT_LANDING_AFTER_EJECTION [https://wiki.hoggitworld.com/view/DCS_event_landing_after_ejection](https://wiki.hoggitworld.com/view/DCS_event_landing_after_ejection) DCS>=2.5.6 -- @field S_EVENT_MAX --- The birthplace enumerator is used to define where an aircraft or helicopter has spawned in association with birth events. diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 9f34f8cf6..3847cdf47 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1091,6 +1091,7 @@ function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, Distri local DCSTask = { id = 'Embarking', params = { + --selectedTransport = self:GetID(), Vec2 = Vec2, x = Vec2.x, y = Vec2.y, @@ -1105,6 +1106,123 @@ function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, Distri return DCSTask end +--- Used in conjunction with the EmbarkToTransport task for a ground infantry group, the controlled helicopter flight will land at the specified coordinates, +-- pick up boarding troops and transport them to that groups DisembarkFromTransport task. +-- The CONTROLLABLE has to be a helicopter group! +-- @param #CONTROLLABLE self +-- @param Core.Set#SET_GROUP GroupSet Set of groups to be embarked by the controllable. +-- @param Core.Point#COORDINATE Coordinate Coordinate of embarking. +-- @param #number Duration Duration of embarking in seconds. +-- @return #table Embarking task. +function CONTROLLABLE:TaskEmbarking(GroupSet, Coordinate, Duration) + + -- Create table of group IDs. + local gids={} + for _,_group in pairs(GroupSet:GetAliveSet()) do + local group=_group --Wrapper.Group#GROUP + table.insert(gids, group:GetID()) + end + + -- Group ID of controllable. + local id=self:GetID() + + -- Distribution + local distribution={} + distribution[id]=gids + + local durationFlag=false + if Duration then + durationFlag=true + else + Duration=300 + end + + local DCStask={ + id="Embarking", + params={ + + distributionFlag=true, + distribution=distribution, + groupsForEmbarking=gids, + durationFlag=durationFlag, + distribution=distribution, + duration=Duration, + x=Coordinate.x, + y=Coordinate.z, + } + } + + self:E(DCStask) + + return DCStask +end + +--- Used in conjunction with the embarking task for a transport helicopter group. The Ground units will move to the specified location and wait to be picked up by a helicopter. +-- The helicopter will then fly them to their dropoff point defined by another task for the ground forces; DisembarkFromTransport task. +-- The controllable has to be an infantry group! +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. +-- @param #number Radius Radius in meters. +-- @return #table Embark to transport task. +function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius) + + local EmbarkToTransport = { + id="EmbarkToTransport", + params={ + x=Coordinate.x, + y=Coordinate.z, + zoneRadius=Radius, + --selectedType="UH-1H", + } + } + + self:E(EmbarkToTransport) + return EmbarkToTransport +end + + + +--- Specifies the location an infantry group that is being transported by helicopters will be unloaded at. Used in conjunction with the EmbarkToTransport task. +-- The CONTROLLABLE has to be an infantry group! +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. +-- @param #number Radius Radius in meters. +-- @return #table Embark to transport task. +function CONTROLLABLE:TaskDisembarkFromTransport(Coordinate, Radius) + + local DisembarkFromTransport={ + id="DisembarkFromTransport", + params = { + x=Coordinate.x, + y=Coordinate.y, + zoneRadius=Radius, + }} + + return DisembarkFromTransport +end + + +--- (GROUND) Embark to a Transport landed at a location. +-- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. +-- @param #CONTROLLABLE self +-- @param DCS#Vec2 Point The point where to wait. +-- @param #number Radius The radius of the embarking zone around the Point. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) + + local DCSTask = { + id = 'EmbarkToTransport', + params = { + point = Point, + x = Point.x, + y = Point.y, + zoneRadius = Radius, + } + } + + return DCSTask +end + --- (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. -- @param #CONTROLLABLE self @@ -1687,125 +1805,6 @@ function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) end - ---[[ ---- Used in conjunction with the embarking task for a transport helicopter group. The Ground units will move to the specified location and wait to be picked up by a helicopter. --- The helicopter will then fly them to their dropoff point defined by another task for the ground forces; DisembarkFromTransport task. --- The controllable has to be an infantry group! --- @param #CONTROLLABLE self --- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. --- @param #number Radius Radius in meters. --- @return #table Embark to transport task. -function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius) - - local EmbarkToTransport = { - id="EmbarkToTransport", - params={ - x=Coordinate.x, - y=Coordinate.z, - zoneRadius=Radius, - --selectedType="UH-1H", - } - } - - self:E(EmbarkToTransport) - return EmbarkToTransport -end - ---- Used in conjunction with the EmbarkToTransport task for a ground infantry group, the controlled helicopter flight will land at the specified coordinates, --- pick up boarding troops and transport them to that groups DisembarkFromTransport task. --- The CONTROLLABLE has to be a helicopter group! --- @param #CONTROLLABLE self --- @param Core.Set#SET_GROUP GroupSet Set of groups to be embarked by the controllable. --- @param Core.Point#COORDINATE Coordinate Coordinate of embarking. --- @param #number Duration Duration of embarking in seconds. --- @return #table Embarking task. -function CONTROLLABLE:TaskEmbarking(GroupSet, Coordinate, Duration) - - -- Create table of group IDs. - local gids={} - for _,_group in pairs(GroupSet:GetAliveSet()) do - local group=_group --Wrapper.Group#GROUP - table.insert(gids, group:GetID()) - end - - -- Group ID of controllable. - local id=self:GetID() - - -- Distribution - local distribution={} - distribution[id]=gids - - local durationFlag=false - if Duration then - durationFlag=true - else - Duration=300 - end - - local DCStask={ - id="Embarking", - params={ - selectedTransport=self:GetID(), - distributionFlag=true, - distribution=distribution, - groupsForEmbarking=gids, - durationFlag=durationFlag, - distribution=distribution, - duration=Duration, - x=Coordinate.x, - y=Coordinate.z, - } - } - - self:E(DCStask) - - return DCStask -end - ---- Specifies the location an infantry group that is being transported by helicopters will be unloaded at. Used in conjunction with the EmbarkToTransport task. --- The CONTROLLABLE has to be an infantry group! --- @param #CONTROLLABLE self --- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. --- @param #number Radius Radius in meters. --- @return #table Embark to transport task. -function CONTROLLABLE:TaskDisembarkFromTransport(Coordinate, Radius) - - local DisembarkFromTransport={ - id="DisembarkFromTransport", - params = { - x=Coordinate.x, - y=Coordinate.y, - zoneRadius=Radius, - }} - - return DisembarkFromTransport -end -]] - - ---- (GROUND) Embark to a Transport landed at a location. --- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. --- @param #CONTROLLABLE self --- @param DCS#Vec2 Point The point where to wait. --- @param #number Radius The radius of the embarking zone around the Point. --- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) - - local DCSTask = { - id = 'EmbarkToTransport', - params = { - point = Point, - x = Point.x, - y = Point.y, - zoneRadius = Radius, - } - } - - return DCSTask -end - - --- This creates a Task element, with an action to call a function as part of a Wrapped Task. -- This Task can then be embedded at a Waypoint by calling the method @{#CONTROLLABLE.SetTaskWaypoint}. -- @param #CONTROLLABLE self From c3cc76c15b5a2fbd3f62014075a4b3205c2af97c Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 24 Apr 2020 21:57:10 +0200 Subject: [PATCH 470/485] Update Controllable.lua --- Moose Development/Moose/Wrapper/Controllable.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 3847cdf47..193df6889 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1176,7 +1176,6 @@ function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius) } } - self:E(EmbarkToTransport) return EmbarkToTransport end @@ -1196,7 +1195,8 @@ function CONTROLLABLE:TaskDisembarkFromTransport(Coordinate, Radius) x=Coordinate.x, y=Coordinate.y, zoneRadius=Radius, - }} + } + } return DisembarkFromTransport end From 68ece29ab52517d57843fde6277ae23201bd6fed Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 24 Apr 2020 22:45:45 +0200 Subject: [PATCH 471/485] Update Controllable.lua --- .../Moose/Wrapper/Controllable.lua | 142 +++++------------- 1 file changed, 40 insertions(+), 102 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 193df6889..dca45845d 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1056,14 +1056,14 @@ function CONTROLLABLE:TaskFollowBigFormation(FollowControllable, Vec3, LastWaypo end ---- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. +--- (AIR HELICOPTER) Move the controllable to a Vec2 Point, wait for a defined duration and embark infantry groups. -- @param #CONTROLLABLE self --- @param DCS#Vec2 Vec2 The point where to wait. Needs to have x and y components. +-- @param Core.Point#COORDINATE Coordinate The point where to pickup the troops. -- @param Core.Set#SET_GROUP GroupSetForEmbarking Set of groups to embark. -- @param #number Duration (Optional) The maximum duration in seconds to wait until all groups have embarked. --- @param Core.Set#SET_GROUP DistributionGroupSet (Optional) Set of groups identifying the groups needing to board specific helicopters. +-- @param #table Distribution (Optional) Distribution used to put the infantry groups into specific carrier units. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, DistributionGroupSet) +function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, Distribution) -- Table of group IDs for embarking. local g4e={} @@ -1079,26 +1079,25 @@ function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, Distri end -- Table of group IDs for embarking. - local Distribution={} + --local Distribution={} - if DistributionGroupSet then - for _,_group in pairs(DistributionGroupSet:GetSet()) do - local group=_group --Wrapper.Group#GROUP - table.insert(Distribution, group:GetID()) - end - end + -- Distribution + --local distribution={} + --distribution[id]=gids + + local groupID=self and self:GetID() local DCSTask = { id = 'Embarking', params = { - --selectedTransport = self:GetID(), + selectedTransport = groupID, Vec2 = Vec2, x = Vec2.x, y = Vec2.y, groupsForEmbarking = g4e, durationFlag = Duration and true or false, duration = Duration, - distributionFlag = DistributionGroupSet and true or false, + distributionFlag = Distribution and true or false, distribution = Distribution, } } @@ -1106,73 +1105,24 @@ function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, Distri return DCSTask end ---- Used in conjunction with the EmbarkToTransport task for a ground infantry group, the controlled helicopter flight will land at the specified coordinates, --- pick up boarding troops and transport them to that groups DisembarkFromTransport task. --- The CONTROLLABLE has to be a helicopter group! --- @param #CONTROLLABLE self --- @param Core.Set#SET_GROUP GroupSet Set of groups to be embarked by the controllable. --- @param Core.Point#COORDINATE Coordinate Coordinate of embarking. --- @param #number Duration Duration of embarking in seconds. --- @return #table Embarking task. -function CONTROLLABLE:TaskEmbarking(GroupSet, Coordinate, Duration) - - -- Create table of group IDs. - local gids={} - for _,_group in pairs(GroupSet:GetAliveSet()) do - local group=_group --Wrapper.Group#GROUP - table.insert(gids, group:GetID()) - end - - -- Group ID of controllable. - local id=self:GetID() - - -- Distribution - local distribution={} - distribution[id]=gids - - local durationFlag=false - if Duration then - durationFlag=true - else - Duration=300 - end - - local DCStask={ - id="Embarking", - params={ - - distributionFlag=true, - distribution=distribution, - groupsForEmbarking=gids, - durationFlag=durationFlag, - distribution=distribution, - duration=Duration, - x=Coordinate.x, - y=Coordinate.z, - } - } - - self:E(DCStask) - - return DCStask -end --- Used in conjunction with the embarking task for a transport helicopter group. The Ground units will move to the specified location and wait to be picked up by a helicopter. -- The helicopter will then fly them to their dropoff point defined by another task for the ground forces; DisembarkFromTransport task. -- The controllable has to be an infantry group! -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. --- @param #number Radius Radius in meters. --- @return #table Embark to transport task. -function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius) +-- @param #number Radius Radius in meters. Default 200 m. +-- @param #string UnitType The unit type name of the carrier, e.g. "UH-1H". Must not be specified. +-- @return DCS#Task Embark to transport task. +function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius, UnitType) local EmbarkToTransport = { - id="EmbarkToTransport", + id = "EmbarkToTransport", params={ - x=Coordinate.x, - y=Coordinate.z, - zoneRadius=Radius, - --selectedType="UH-1H", + x = Coordinate.x, + y = Coordinate.z, + zoneRadius = Radius or 200, + selectedType = UnitType, } } @@ -1180,21 +1130,31 @@ function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius) end - ---- Specifies the location an infantry group that is being transported by helicopters will be unloaded at. Used in conjunction with the EmbarkToTransport task. --- The CONTROLLABLE has to be an infantry group! +--- Specifies the location infantry groups that is being transported by helicopters will be unloaded at. Used in conjunction with the EmbarkToTransport task. -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. --- @param #number Radius Radius in meters. --- @return #table Embark to transport task. -function CONTROLLABLE:TaskDisembarkFromTransport(Coordinate, Radius) +-- @return DCS#Task Embark to transport task. +function CONTROLLABLE:TaskDisembarking(Coordinate, GroupSetToDisembark) + + -- Table of group IDs for disembarking. + local g4e={} + + if GroupSetToDisembark then + for _,_group in pairs(GroupSetToDisembark:GetSet()) do + local group=_group --Wrapper.Group#GROUP + table.insert(g4e, group:GetID()) + end + else + self:E("ERROR: No groups for disembarking specified!") + return nil + end local DisembarkFromTransport={ id="DisembarkFromTransport", params = { - x=Coordinate.x, - y=Coordinate.y, - zoneRadius=Radius, + x = Coordinate.x, + y = Coordinate.z, + groupsForEmbarking = g4e, -- This is no bug, the entry is really "groupsForEmbarking" even if we disembark the troops. } } @@ -1202,28 +1162,6 @@ function CONTROLLABLE:TaskDisembarkFromTransport(Coordinate, Radius) end ---- (GROUND) Embark to a Transport landed at a location. --- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. --- @param #CONTROLLABLE self --- @param DCS#Vec2 Point The point where to wait. --- @param #number Radius The radius of the embarking zone around the Point. --- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) - - local DCSTask = { - id = 'EmbarkToTransport', - params = { - point = Point, - x = Point.x, - y = Point.y, - zoneRadius = Radius, - } - } - - return DCSTask -end - - --- (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Point The point to hold the position. From 4e49184da26c1e7a6ec6f962fec75cadfd4f9c1f Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 24 Apr 2020 22:53:42 +0200 Subject: [PATCH 472/485] Update Static.lua --- Moose Development/Moose/Wrapper/Static.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Wrapper/Static.lua b/Moose Development/Moose/Wrapper/Static.lua index 64a12a98a..16b07c03f 100644 --- a/Moose Development/Moose/Wrapper/Static.lua +++ b/Moose Development/Moose/Wrapper/Static.lua @@ -76,7 +76,7 @@ end -- @param #string StaticName Name of the DCS **Static** as defined within the Mission Editor. -- @param #boolean RaiseError Raise an error if not found. -- @return #STATIC self or *nil* -function STATIC:FindByName( StaticName ) +function STATIC:FindByName( StaticName, RaiseError ) -- Find static in DB. local StaticFound = _DATABASE:FindStatic( StaticName ) @@ -87,6 +87,10 @@ function STATIC:FindByName( StaticName ) if StaticFound then return StaticFound end + + if RaiseError == nil or RaiseError == true then + error( "STATIC not found for: " .. StaticName ) + end return nil end From 517d5860fb96b30a771565ac54548d36f620c12c Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 25 Apr 2020 00:57:59 +0200 Subject: [PATCH 473/485] Update Controllable.lua --- Moose Development/Moose/Wrapper/Controllable.lua | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index dca45845d..828188dc9 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1063,7 +1063,7 @@ end -- @param #number Duration (Optional) The maximum duration in seconds to wait until all groups have embarked. -- @param #table Distribution (Optional) Distribution used to put the infantry groups into specific carrier units. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, Distribution) +function CONTROLLABLE:TaskEmbarking(Coordinate, GroupSetForEmbarking, Duration, Distribution) -- Table of group IDs for embarking. local g4e={} @@ -1091,9 +1091,8 @@ function CONTROLLABLE:TaskEmbarking(Vec2, GroupSetForEmbarking, Duration, Distri id = 'Embarking', params = { selectedTransport = groupID, - Vec2 = Vec2, - x = Vec2.x, - y = Vec2.y, + x = Coordinate.x, + y = Coordinate.z, groupsForEmbarking = g4e, durationFlag = Duration and true or false, duration = Duration, @@ -1149,8 +1148,8 @@ function CONTROLLABLE:TaskDisembarking(Coordinate, GroupSetToDisembark) return nil end - local DisembarkFromTransport={ - id="DisembarkFromTransport", + local Disembarking={ + id = "Disembarking", params = { x = Coordinate.x, y = Coordinate.z, @@ -1158,7 +1157,7 @@ function CONTROLLABLE:TaskDisembarking(Coordinate, GroupSetToDisembark) } } - return DisembarkFromTransport + return Disembarking end From 056cc1630dddc9878c2c9b4c8c5ec9ffe83ca502 Mon Sep 17 00:00:00 2001 From: Stephen Locke Date: Sat, 25 Apr 2020 15:07:04 +0100 Subject: [PATCH 474/485] Added call to self:UpdateTaskInfo( self.DetectedItem ) to RerportOrder for both Task_A2A and Task_A2G. This fixes an issue with multi task missions where coordinates are not available when Moose attempts to sort the tasks for the comms menu. --- Moose Development/Moose/Tasking/Task_A2A.lua | 4 +++- Moose Development/Moose/Tasking/Task_A2G.lua | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Tasking/Task_A2A.lua b/Moose Development/Moose/Tasking/Task_A2A.lua index 0f11ba4c9..2da9be005 100644 --- a/Moose Development/Moose/Tasking/Task_A2A.lua +++ b/Moose Development/Moose/Tasking/Task_A2A.lua @@ -292,7 +292,9 @@ do -- TASK_A2A --- Return the relative distance to the target vicinity from the player, in order to sort the targets in the reports per distance from the threats. -- @param #TASK_A2A self - function TASK_A2A:ReportOrder( ReportGroup ) + function TASK_A2A:ReportOrder( ReportGroup ) + self:UpdateTaskInfo( self.DetectedItem ) + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) diff --git a/Moose Development/Moose/Tasking/Task_A2G.lua b/Moose Development/Moose/Tasking/Task_A2G.lua index bb2e2f816..f4ccf40d6 100644 --- a/Moose Development/Moose/Tasking/Task_A2G.lua +++ b/Moose Development/Moose/Tasking/Task_A2G.lua @@ -295,6 +295,8 @@ do -- TASK_A2G --- Return the relative distance to the target vicinity from the player, in order to sort the targets in the reports per distance from the threats. -- @param #TASK_A2G self function TASK_A2G:ReportOrder( ReportGroup ) + self:UpdateTaskInfo( self.DetectedItem ) + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) From c78bd2652aae649f7a42a7d7138cf08d47bcd0f4 Mon Sep 17 00:00:00 2001 From: zlinman <63908222+zlinman@users.noreply.github.com> Date: Thu, 30 Apr 2020 18:33:15 +0200 Subject: [PATCH 475/485] Update CleanUp.lua --- .../Moose/Functional/CleanUp.lua | 109 +++++++++--------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/Moose Development/Moose/Functional/CleanUp.lua b/Moose Development/Moose/Functional/CleanUp.lua index daed34b02..e8fd44972 100644 --- a/Moose Development/Moose/Functional/CleanUp.lua +++ b/Moose Development/Moose/Functional/CleanUp.lua @@ -1,54 +1,54 @@ --- **Functional** -- Keep airbases clean of crashing or colliding airplanes, and kill missiles when being fired at airbases. --- +-- -- === --- +-- -- ## Features: --- --- +-- +-- -- * Try to keep the airbase clean and operational. -- * Prevent airplanes from crashing. -- * Clean up obstructing airplanes from the runway that are standing still for a period of time. -- * Prevent airplanes firing missiles within the airbase zone. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [CLA - CleanUp Airbase](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CLA%20-%20CleanUp%20Airbase) --- +-- -- === --- +-- -- Specific airbases need to be provided that need to be guarded. Each airbase registered, will be guarded within a zone of 8 km around the airbase. -- Any unit that fires a missile, or shoots within the zone of an airbase, will be monitored by CLEANUP_AIRBASE. --- Within the 8km zone, units cannot fire any missile, which prevents the airbase runway to receive missile or bomb hits. +-- Within the 8km zone, units cannot fire any missile, which prevents the airbase runway to receive missile or bomb hits. -- Any airborne or ground unit that is on the runway below 30 meters (default value) will be automatically removed if it is damaged. --- +-- -- This is not a full 100% secure implementation. It is still possible that CLEANUP_AIRBASE cannot prevent (in-time) to keep the airbase clean. -- The following situations may happen that will still stop the runway of an airbase: --- +-- -- * A damaged unit is not removed on time when above the runway, and crashes on the runway. -- * A bomb or missile is still able to dropped on the runway. -- * Units collide on the airbase, and could not be removed on time. --- +-- -- When a unit is within the airbase zone and needs to be monitored, -- its status will be checked every 0.25 seconds! This is required to ensure that the airbase is kept clean. -- But as a result, there is more CPU overload. --- +-- -- So as an advise, I suggest you use the CLEANUP_AIRBASE class with care: --- +-- -- * Only monitor airbases that really need to be monitored! -- * Try not to monitor airbases that are likely to be invaded by enemy troops. -- For these airbases, there is little use to keep them clean, as they will be invaded anyway... --- +-- -- By following the above guidelines, you can add airbase cleanup with acceptable CPU overhead. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- ### Contributions: --- +-- ### Contributions: +-- -- === --- +-- -- @module Functional.CleanUp -- @image CleanUp_Airbases.JPG @@ -60,29 +60,29 @@ -- @extends #CLEANUP_AIRBASE.__ --- Keeps airbases clean, and tries to guarantee continuous airbase operations, even under combat. --- +-- -- # 1. CLEANUP_AIRBASE Constructor --- +-- -- Creates the main object which is preventing the airbase to get polluted with debris on the runway, which halts the airbase. --- +-- -- -- Clean these Zones. --- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi ) --- +-- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi } ) +-- -- -- or -- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) -- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) --- +-- -- # 2. Add or Remove airbases --- +-- -- The method @{#CLEANUP_AIRBASE.AddAirbase}() to add an airbase to the cleanup validation process. -- The method @{#CLEANUP_AIRBASE.RemoveAirbase}() removes an airbase from the cleanup validation process. --- +-- -- # 3. Clean missiles and bombs within the airbase zone. --- +-- -- When missiles or bombs hit the runway, the airbase operations stop. -- Use the method @{#CLEANUP_AIRBASE.SetCleanMissiles}() to control the cleaning of missiles, which will prevent airbases to stop. -- Note that this method will not allow anymore airbases to be attacked, so there is a trade-off here to do. --- +-- -- @field #CLEANUP_AIRBASE CLEANUP_AIRBASE = { ClassName = "CLEANUP_AIRBASE", @@ -106,11 +106,11 @@ CLEANUP_AIRBASE.__.Airbases = {} -- or -- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) -- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) -function CLEANUP_AIRBASE:New( AirbaseNames ) +function CLEANUP_AIRBASE:New( AirbaseNames ) local self = BASE:Inherit( self, BASE:New() ) -- #CLEANUP_AIRBASE self:F( { AirbaseNames } ) - + if type( AirbaseNames ) == 'table' then for AirbaseID, AirbaseName in pairs( AirbaseNames ) do self:AddAirbase( AirbaseName ) @@ -119,9 +119,9 @@ function CLEANUP_AIRBASE:New( AirbaseNames ) local AirbaseName = AirbaseNames self:AddAirbase( AirbaseName ) end - + self:HandleEvent( EVENTS.Birth, self.__.OnEventBirth ) - + self.__.CleanUpScheduler = SCHEDULER:New( self, self.__.CleanUpSchedule, {}, 1, self.TimeInterval ) self:HandleEvent( EVENTS.EngineShutdown , self.__.EventAddForCleanUp ) @@ -130,7 +130,7 @@ function CLEANUP_AIRBASE:New( AirbaseNames ) self:HandleEvent( EVENTS.PilotDead, self.__.OnEventCrash ) self:HandleEvent( EVENTS.Dead, self.__.OnEventCrash ) self:HandleEvent( EVENTS.Crash, self.__.OnEventCrash ) - + for UnitName, Unit in pairs( _DATABASE.UNITS ) do local Unit = Unit -- Wrapper.Unit#UNIT if Unit:IsAlive() ~= nil then @@ -144,7 +144,7 @@ function CLEANUP_AIRBASE:New( AirbaseNames ) end end end - + return self end @@ -155,7 +155,7 @@ end function CLEANUP_AIRBASE:AddAirbase( AirbaseName ) self.__.Airbases[AirbaseName] = AIRBASE:FindByName( AirbaseName ) self:F({"Airbase:", AirbaseName, self.__.Airbases[AirbaseName]:GetDesc()}) - + return self end @@ -197,7 +197,7 @@ function CLEANUP_AIRBASE.__:IsInAirbase( Vec2 ) break; end end - + return InAirbase end @@ -233,7 +233,7 @@ end -- @param DCS#Weapon MissileObject function CLEANUP_AIRBASE.__:DestroyMissile( MissileObject ) self:F( { MissileObject } ) - + if MissileObject and MissileObject:isExist() then MissileObject:destroy() self:T( "MissileObject Destroyed") @@ -244,7 +244,7 @@ end -- @param Core.Event#EVENTDATA EventData function CLEANUP_AIRBASE.__:OnEventBirth( EventData ) self:F( { EventData } ) - + if EventData.IniUnit:IsAlive() ~= nil then if self:IsInAirbase( EventData.IniUnit:GetVec2() ) then self.CleanUpList[EventData.IniDCSUnitName] = {} @@ -267,7 +267,7 @@ function CLEANUP_AIRBASE.__:OnEventCrash( Event ) --TODO: DCS BUG - This stuff is not working due to a DCS bug. Burning units cannot be destroyed. -- self:T("before getGroup") - -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired + -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired -- self:T("after getGroup") -- _grp:destroy() -- self:T("after deactivateGroup") @@ -280,7 +280,7 @@ function CLEANUP_AIRBASE.__:OnEventCrash( Event ) self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName end - + end --- Detects if a unit shoots a missile. @@ -334,16 +334,16 @@ function CLEANUP_AIRBASE.__:AddForCleanUp( CleanUpUnit, CleanUpUnitName ) self.CleanUpList[CleanUpUnitName] = {} self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName - + local CleanUpGroup = CleanUpUnit:GetGroup() - + self.CleanUpList[CleanUpUnitName].CleanUpGroup = CleanUpGroup self.CleanUpList[CleanUpUnitName].CleanUpGroupName = CleanUpGroup:GetName() self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() self.CleanUpList[CleanUpUnitName].CleanUpMoved = false self:T( { "CleanUp: Add to CleanUpList: ", CleanUpGroup:GetName(), CleanUpUnitName } ) - + end --- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given AirbaseNames. If this is the case, add the Group to the CLEANUP_AIRBASE List. @@ -369,7 +369,7 @@ function CLEANUP_AIRBASE.__:EventAddForCleanUp( Event ) end end end - + end @@ -380,26 +380,26 @@ function CLEANUP_AIRBASE.__:CleanUpSchedule() local CleanUpCount = 0 for CleanUpUnitName, CleanUpListData in pairs( self.CleanUpList ) do CleanUpCount = CleanUpCount + 1 - + local CleanUpUnit = CleanUpListData.CleanUpUnit -- Wrapper.Unit#UNIT local CleanUpGroupName = CleanUpListData.CleanUpGroupName if CleanUpUnit:IsAlive() ~= nil then - + if self:IsInAirbase( CleanUpUnit:GetVec2() ) then if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then - + local CleanUpCoordinate = CleanUpUnit:GetCoordinate() - + self:T( { "CleanUp Scheduler", CleanUpUnitName } ) if CleanUpUnit:GetLife() <= CleanUpUnit:GetLife0() * 0.95 then if CleanUpUnit:IsAboveRunway() then if CleanUpUnit:InAir() then - + local CleanUpLandHeight = CleanUpCoordinate:GetLandHeight() local CleanUpUnitHeight = CleanUpCoordinate.y - CleanUpLandHeight - + if CleanUpUnitHeight < 100 then self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } ) self:DestroyUnit( CleanUpUnit ) @@ -439,7 +439,6 @@ function CLEANUP_AIRBASE.__:CleanUpSchedule() end end self:T(CleanUpCount) - + return true end - From b86eac33b6a9b4c2886ff6983ce91dba6fdd25f2 Mon Sep 17 00:00:00 2001 From: zlinman <63908222+zlinman@users.noreply.github.com> Date: Thu, 30 Apr 2020 18:38:04 +0200 Subject: [PATCH 476/485] Revert "Update CleanUp.lua" This reverts commit c78bd2652aae649f7a42a7d7138cf08d47bcd0f4. --- .../Moose/Functional/CleanUp.lua | 109 +++++++++--------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/Moose Development/Moose/Functional/CleanUp.lua b/Moose Development/Moose/Functional/CleanUp.lua index e8fd44972..daed34b02 100644 --- a/Moose Development/Moose/Functional/CleanUp.lua +++ b/Moose Development/Moose/Functional/CleanUp.lua @@ -1,54 +1,54 @@ --- **Functional** -- Keep airbases clean of crashing or colliding airplanes, and kill missiles when being fired at airbases. --- +-- -- === --- +-- -- ## Features: --- --- +-- +-- -- * Try to keep the airbase clean and operational. -- * Prevent airplanes from crashing. -- * Clean up obstructing airplanes from the runway that are standing still for a period of time. -- * Prevent airplanes firing missiles within the airbase zone. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [CLA - CleanUp Airbase](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CLA%20-%20CleanUp%20Airbase) --- +-- -- === --- +-- -- Specific airbases need to be provided that need to be guarded. Each airbase registered, will be guarded within a zone of 8 km around the airbase. -- Any unit that fires a missile, or shoots within the zone of an airbase, will be monitored by CLEANUP_AIRBASE. --- Within the 8km zone, units cannot fire any missile, which prevents the airbase runway to receive missile or bomb hits. +-- Within the 8km zone, units cannot fire any missile, which prevents the airbase runway to receive missile or bomb hits. -- Any airborne or ground unit that is on the runway below 30 meters (default value) will be automatically removed if it is damaged. --- +-- -- This is not a full 100% secure implementation. It is still possible that CLEANUP_AIRBASE cannot prevent (in-time) to keep the airbase clean. -- The following situations may happen that will still stop the runway of an airbase: --- +-- -- * A damaged unit is not removed on time when above the runway, and crashes on the runway. -- * A bomb or missile is still able to dropped on the runway. -- * Units collide on the airbase, and could not be removed on time. --- +-- -- When a unit is within the airbase zone and needs to be monitored, -- its status will be checked every 0.25 seconds! This is required to ensure that the airbase is kept clean. -- But as a result, there is more CPU overload. --- +-- -- So as an advise, I suggest you use the CLEANUP_AIRBASE class with care: --- +-- -- * Only monitor airbases that really need to be monitored! -- * Try not to monitor airbases that are likely to be invaded by enemy troops. -- For these airbases, there is little use to keep them clean, as they will be invaded anyway... --- +-- -- By following the above guidelines, you can add airbase cleanup with acceptable CPU overhead. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- ### Contributions: --- +-- ### Contributions: +-- -- === --- +-- -- @module Functional.CleanUp -- @image CleanUp_Airbases.JPG @@ -60,29 +60,29 @@ -- @extends #CLEANUP_AIRBASE.__ --- Keeps airbases clean, and tries to guarantee continuous airbase operations, even under combat. --- +-- -- # 1. CLEANUP_AIRBASE Constructor --- +-- -- Creates the main object which is preventing the airbase to get polluted with debris on the runway, which halts the airbase. --- +-- -- -- Clean these Zones. --- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi } ) --- +-- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi ) +-- -- -- or -- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) -- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) --- +-- -- # 2. Add or Remove airbases --- +-- -- The method @{#CLEANUP_AIRBASE.AddAirbase}() to add an airbase to the cleanup validation process. -- The method @{#CLEANUP_AIRBASE.RemoveAirbase}() removes an airbase from the cleanup validation process. --- +-- -- # 3. Clean missiles and bombs within the airbase zone. --- +-- -- When missiles or bombs hit the runway, the airbase operations stop. -- Use the method @{#CLEANUP_AIRBASE.SetCleanMissiles}() to control the cleaning of missiles, which will prevent airbases to stop. -- Note that this method will not allow anymore airbases to be attacked, so there is a trade-off here to do. --- +-- -- @field #CLEANUP_AIRBASE CLEANUP_AIRBASE = { ClassName = "CLEANUP_AIRBASE", @@ -106,11 +106,11 @@ CLEANUP_AIRBASE.__.Airbases = {} -- or -- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) -- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) -function CLEANUP_AIRBASE:New( AirbaseNames ) +function CLEANUP_AIRBASE:New( AirbaseNames ) local self = BASE:Inherit( self, BASE:New() ) -- #CLEANUP_AIRBASE self:F( { AirbaseNames } ) - + if type( AirbaseNames ) == 'table' then for AirbaseID, AirbaseName in pairs( AirbaseNames ) do self:AddAirbase( AirbaseName ) @@ -119,9 +119,9 @@ function CLEANUP_AIRBASE:New( AirbaseNames ) local AirbaseName = AirbaseNames self:AddAirbase( AirbaseName ) end - + self:HandleEvent( EVENTS.Birth, self.__.OnEventBirth ) - + self.__.CleanUpScheduler = SCHEDULER:New( self, self.__.CleanUpSchedule, {}, 1, self.TimeInterval ) self:HandleEvent( EVENTS.EngineShutdown , self.__.EventAddForCleanUp ) @@ -130,7 +130,7 @@ function CLEANUP_AIRBASE:New( AirbaseNames ) self:HandleEvent( EVENTS.PilotDead, self.__.OnEventCrash ) self:HandleEvent( EVENTS.Dead, self.__.OnEventCrash ) self:HandleEvent( EVENTS.Crash, self.__.OnEventCrash ) - + for UnitName, Unit in pairs( _DATABASE.UNITS ) do local Unit = Unit -- Wrapper.Unit#UNIT if Unit:IsAlive() ~= nil then @@ -144,7 +144,7 @@ function CLEANUP_AIRBASE:New( AirbaseNames ) end end end - + return self end @@ -155,7 +155,7 @@ end function CLEANUP_AIRBASE:AddAirbase( AirbaseName ) self.__.Airbases[AirbaseName] = AIRBASE:FindByName( AirbaseName ) self:F({"Airbase:", AirbaseName, self.__.Airbases[AirbaseName]:GetDesc()}) - + return self end @@ -197,7 +197,7 @@ function CLEANUP_AIRBASE.__:IsInAirbase( Vec2 ) break; end end - + return InAirbase end @@ -233,7 +233,7 @@ end -- @param DCS#Weapon MissileObject function CLEANUP_AIRBASE.__:DestroyMissile( MissileObject ) self:F( { MissileObject } ) - + if MissileObject and MissileObject:isExist() then MissileObject:destroy() self:T( "MissileObject Destroyed") @@ -244,7 +244,7 @@ end -- @param Core.Event#EVENTDATA EventData function CLEANUP_AIRBASE.__:OnEventBirth( EventData ) self:F( { EventData } ) - + if EventData.IniUnit:IsAlive() ~= nil then if self:IsInAirbase( EventData.IniUnit:GetVec2() ) then self.CleanUpList[EventData.IniDCSUnitName] = {} @@ -267,7 +267,7 @@ function CLEANUP_AIRBASE.__:OnEventCrash( Event ) --TODO: DCS BUG - This stuff is not working due to a DCS bug. Burning units cannot be destroyed. -- self:T("before getGroup") - -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired + -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired -- self:T("after getGroup") -- _grp:destroy() -- self:T("after deactivateGroup") @@ -280,7 +280,7 @@ function CLEANUP_AIRBASE.__:OnEventCrash( Event ) self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName end - + end --- Detects if a unit shoots a missile. @@ -334,16 +334,16 @@ function CLEANUP_AIRBASE.__:AddForCleanUp( CleanUpUnit, CleanUpUnitName ) self.CleanUpList[CleanUpUnitName] = {} self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName - + local CleanUpGroup = CleanUpUnit:GetGroup() - + self.CleanUpList[CleanUpUnitName].CleanUpGroup = CleanUpGroup self.CleanUpList[CleanUpUnitName].CleanUpGroupName = CleanUpGroup:GetName() self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() self.CleanUpList[CleanUpUnitName].CleanUpMoved = false self:T( { "CleanUp: Add to CleanUpList: ", CleanUpGroup:GetName(), CleanUpUnitName } ) - + end --- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given AirbaseNames. If this is the case, add the Group to the CLEANUP_AIRBASE List. @@ -369,7 +369,7 @@ function CLEANUP_AIRBASE.__:EventAddForCleanUp( Event ) end end end - + end @@ -380,26 +380,26 @@ function CLEANUP_AIRBASE.__:CleanUpSchedule() local CleanUpCount = 0 for CleanUpUnitName, CleanUpListData in pairs( self.CleanUpList ) do CleanUpCount = CleanUpCount + 1 - + local CleanUpUnit = CleanUpListData.CleanUpUnit -- Wrapper.Unit#UNIT local CleanUpGroupName = CleanUpListData.CleanUpGroupName if CleanUpUnit:IsAlive() ~= nil then - + if self:IsInAirbase( CleanUpUnit:GetVec2() ) then if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then - + local CleanUpCoordinate = CleanUpUnit:GetCoordinate() - + self:T( { "CleanUp Scheduler", CleanUpUnitName } ) if CleanUpUnit:GetLife() <= CleanUpUnit:GetLife0() * 0.95 then if CleanUpUnit:IsAboveRunway() then if CleanUpUnit:InAir() then - + local CleanUpLandHeight = CleanUpCoordinate:GetLandHeight() local CleanUpUnitHeight = CleanUpCoordinate.y - CleanUpLandHeight - + if CleanUpUnitHeight < 100 then self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } ) self:DestroyUnit( CleanUpUnit ) @@ -439,6 +439,7 @@ function CLEANUP_AIRBASE.__:CleanUpSchedule() end end self:T(CleanUpCount) - + return true end + From f866af0d4a46b130f1370f4a76f9f6d8f2716220 Mon Sep 17 00:00:00 2001 From: zlinman <63908222+zlinman@users.noreply.github.com> Date: Thu, 30 Apr 2020 18:45:06 +0200 Subject: [PATCH 477/485] Update CleanUp.lua --- .../Moose/Functional/CleanUp.lua | 109 +++++++++--------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/Moose Development/Moose/Functional/CleanUp.lua b/Moose Development/Moose/Functional/CleanUp.lua index daed34b02..e8fd44972 100644 --- a/Moose Development/Moose/Functional/CleanUp.lua +++ b/Moose Development/Moose/Functional/CleanUp.lua @@ -1,54 +1,54 @@ --- **Functional** -- Keep airbases clean of crashing or colliding airplanes, and kill missiles when being fired at airbases. --- +-- -- === --- +-- -- ## Features: --- --- +-- +-- -- * Try to keep the airbase clean and operational. -- * Prevent airplanes from crashing. -- * Clean up obstructing airplanes from the runway that are standing still for a period of time. -- * Prevent airplanes firing missiles within the airbase zone. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [CLA - CleanUp Airbase](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CLA%20-%20CleanUp%20Airbase) --- +-- -- === --- +-- -- Specific airbases need to be provided that need to be guarded. Each airbase registered, will be guarded within a zone of 8 km around the airbase. -- Any unit that fires a missile, or shoots within the zone of an airbase, will be monitored by CLEANUP_AIRBASE. --- Within the 8km zone, units cannot fire any missile, which prevents the airbase runway to receive missile or bomb hits. +-- Within the 8km zone, units cannot fire any missile, which prevents the airbase runway to receive missile or bomb hits. -- Any airborne or ground unit that is on the runway below 30 meters (default value) will be automatically removed if it is damaged. --- +-- -- This is not a full 100% secure implementation. It is still possible that CLEANUP_AIRBASE cannot prevent (in-time) to keep the airbase clean. -- The following situations may happen that will still stop the runway of an airbase: --- +-- -- * A damaged unit is not removed on time when above the runway, and crashes on the runway. -- * A bomb or missile is still able to dropped on the runway. -- * Units collide on the airbase, and could not be removed on time. --- +-- -- When a unit is within the airbase zone and needs to be monitored, -- its status will be checked every 0.25 seconds! This is required to ensure that the airbase is kept clean. -- But as a result, there is more CPU overload. --- +-- -- So as an advise, I suggest you use the CLEANUP_AIRBASE class with care: --- +-- -- * Only monitor airbases that really need to be monitored! -- * Try not to monitor airbases that are likely to be invaded by enemy troops. -- For these airbases, there is little use to keep them clean, as they will be invaded anyway... --- +-- -- By following the above guidelines, you can add airbase cleanup with acceptable CPU overhead. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- ### Contributions: --- +-- ### Contributions: +-- -- === --- +-- -- @module Functional.CleanUp -- @image CleanUp_Airbases.JPG @@ -60,29 +60,29 @@ -- @extends #CLEANUP_AIRBASE.__ --- Keeps airbases clean, and tries to guarantee continuous airbase operations, even under combat. --- +-- -- # 1. CLEANUP_AIRBASE Constructor --- +-- -- Creates the main object which is preventing the airbase to get polluted with debris on the runway, which halts the airbase. --- +-- -- -- Clean these Zones. --- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi ) --- +-- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi } ) +-- -- -- or -- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) -- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) --- +-- -- # 2. Add or Remove airbases --- +-- -- The method @{#CLEANUP_AIRBASE.AddAirbase}() to add an airbase to the cleanup validation process. -- The method @{#CLEANUP_AIRBASE.RemoveAirbase}() removes an airbase from the cleanup validation process. --- +-- -- # 3. Clean missiles and bombs within the airbase zone. --- +-- -- When missiles or bombs hit the runway, the airbase operations stop. -- Use the method @{#CLEANUP_AIRBASE.SetCleanMissiles}() to control the cleaning of missiles, which will prevent airbases to stop. -- Note that this method will not allow anymore airbases to be attacked, so there is a trade-off here to do. --- +-- -- @field #CLEANUP_AIRBASE CLEANUP_AIRBASE = { ClassName = "CLEANUP_AIRBASE", @@ -106,11 +106,11 @@ CLEANUP_AIRBASE.__.Airbases = {} -- or -- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) -- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) -function CLEANUP_AIRBASE:New( AirbaseNames ) +function CLEANUP_AIRBASE:New( AirbaseNames ) local self = BASE:Inherit( self, BASE:New() ) -- #CLEANUP_AIRBASE self:F( { AirbaseNames } ) - + if type( AirbaseNames ) == 'table' then for AirbaseID, AirbaseName in pairs( AirbaseNames ) do self:AddAirbase( AirbaseName ) @@ -119,9 +119,9 @@ function CLEANUP_AIRBASE:New( AirbaseNames ) local AirbaseName = AirbaseNames self:AddAirbase( AirbaseName ) end - + self:HandleEvent( EVENTS.Birth, self.__.OnEventBirth ) - + self.__.CleanUpScheduler = SCHEDULER:New( self, self.__.CleanUpSchedule, {}, 1, self.TimeInterval ) self:HandleEvent( EVENTS.EngineShutdown , self.__.EventAddForCleanUp ) @@ -130,7 +130,7 @@ function CLEANUP_AIRBASE:New( AirbaseNames ) self:HandleEvent( EVENTS.PilotDead, self.__.OnEventCrash ) self:HandleEvent( EVENTS.Dead, self.__.OnEventCrash ) self:HandleEvent( EVENTS.Crash, self.__.OnEventCrash ) - + for UnitName, Unit in pairs( _DATABASE.UNITS ) do local Unit = Unit -- Wrapper.Unit#UNIT if Unit:IsAlive() ~= nil then @@ -144,7 +144,7 @@ function CLEANUP_AIRBASE:New( AirbaseNames ) end end end - + return self end @@ -155,7 +155,7 @@ end function CLEANUP_AIRBASE:AddAirbase( AirbaseName ) self.__.Airbases[AirbaseName] = AIRBASE:FindByName( AirbaseName ) self:F({"Airbase:", AirbaseName, self.__.Airbases[AirbaseName]:GetDesc()}) - + return self end @@ -197,7 +197,7 @@ function CLEANUP_AIRBASE.__:IsInAirbase( Vec2 ) break; end end - + return InAirbase end @@ -233,7 +233,7 @@ end -- @param DCS#Weapon MissileObject function CLEANUP_AIRBASE.__:DestroyMissile( MissileObject ) self:F( { MissileObject } ) - + if MissileObject and MissileObject:isExist() then MissileObject:destroy() self:T( "MissileObject Destroyed") @@ -244,7 +244,7 @@ end -- @param Core.Event#EVENTDATA EventData function CLEANUP_AIRBASE.__:OnEventBirth( EventData ) self:F( { EventData } ) - + if EventData.IniUnit:IsAlive() ~= nil then if self:IsInAirbase( EventData.IniUnit:GetVec2() ) then self.CleanUpList[EventData.IniDCSUnitName] = {} @@ -267,7 +267,7 @@ function CLEANUP_AIRBASE.__:OnEventCrash( Event ) --TODO: DCS BUG - This stuff is not working due to a DCS bug. Burning units cannot be destroyed. -- self:T("before getGroup") - -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired + -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired -- self:T("after getGroup") -- _grp:destroy() -- self:T("after deactivateGroup") @@ -280,7 +280,7 @@ function CLEANUP_AIRBASE.__:OnEventCrash( Event ) self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName end - + end --- Detects if a unit shoots a missile. @@ -334,16 +334,16 @@ function CLEANUP_AIRBASE.__:AddForCleanUp( CleanUpUnit, CleanUpUnitName ) self.CleanUpList[CleanUpUnitName] = {} self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName - + local CleanUpGroup = CleanUpUnit:GetGroup() - + self.CleanUpList[CleanUpUnitName].CleanUpGroup = CleanUpGroup self.CleanUpList[CleanUpUnitName].CleanUpGroupName = CleanUpGroup:GetName() self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() self.CleanUpList[CleanUpUnitName].CleanUpMoved = false self:T( { "CleanUp: Add to CleanUpList: ", CleanUpGroup:GetName(), CleanUpUnitName } ) - + end --- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given AirbaseNames. If this is the case, add the Group to the CLEANUP_AIRBASE List. @@ -369,7 +369,7 @@ function CLEANUP_AIRBASE.__:EventAddForCleanUp( Event ) end end end - + end @@ -380,26 +380,26 @@ function CLEANUP_AIRBASE.__:CleanUpSchedule() local CleanUpCount = 0 for CleanUpUnitName, CleanUpListData in pairs( self.CleanUpList ) do CleanUpCount = CleanUpCount + 1 - + local CleanUpUnit = CleanUpListData.CleanUpUnit -- Wrapper.Unit#UNIT local CleanUpGroupName = CleanUpListData.CleanUpGroupName if CleanUpUnit:IsAlive() ~= nil then - + if self:IsInAirbase( CleanUpUnit:GetVec2() ) then if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then - + local CleanUpCoordinate = CleanUpUnit:GetCoordinate() - + self:T( { "CleanUp Scheduler", CleanUpUnitName } ) if CleanUpUnit:GetLife() <= CleanUpUnit:GetLife0() * 0.95 then if CleanUpUnit:IsAboveRunway() then if CleanUpUnit:InAir() then - + local CleanUpLandHeight = CleanUpCoordinate:GetLandHeight() local CleanUpUnitHeight = CleanUpCoordinate.y - CleanUpLandHeight - + if CleanUpUnitHeight < 100 then self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } ) self:DestroyUnit( CleanUpUnit ) @@ -439,7 +439,6 @@ function CLEANUP_AIRBASE.__:CleanUpSchedule() end end self:T(CleanUpCount) - + return true end - From 2620370890b155f78b045121aa54709388c55710 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 1 May 2020 23:01:43 +0200 Subject: [PATCH 478/485] Fixes UTILS - Corrected Big Smoke and Fire presets. Issue #1313 CONTROLLABLE - Fixed callsign number in :CommandSetCallsign function. #1314 ENUMS - Added formations (old and new) --- Moose Development/Moose/Utilities/Enums.lua | 85 ++++++++++++++++++- Moose Development/Moose/Utilities/Utils.lua | 47 ++++++---- .../Moose/Wrapper/Controllable.lua | 5 +- 3 files changed, 116 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index f47e66b94..e59b958f5 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -160,4 +160,87 @@ ENUMS.MissionTask={ RUNWAYATTACK="Runway Attack", SEAD="SEAD", TRANSPORT="Transport", -} \ No newline at end of file +} + +--- Formations (new). See the [Formations](https://wiki.hoggitworld.com/view/DCS_enum_formation) on hoggit wiki. +-- @type ENUMS.Formation +ENUMS.Formation={} +ENUMS.Formation.FixedWing={} +ENUMS.Formation.FixedWing.LineAbreast={} +ENUMS.Formation.FixedWing.LineAbreast.Close = 65537 +ENUMS.Formation.FixedWing.LineAbreast.Open = 65538 +ENUMS.Formation.FixedWing.LineAbreast.Group = 65539 +ENUMS.Formation.FixedWing.Trail={} +ENUMS.Formation.FixedWing.Trail.Close = 131073 +ENUMS.Formation.FixedWing.Trail.Open = 131074 +ENUMS.Formation.FixedWing.Trail.Group = 131075 +ENUMS.Formation.FixedWing.Wedge={} +ENUMS.Formation.FixedWing.Wedge.Close = 196609 +ENUMS.Formation.FixedWing.Wedge.Open = 196610 +ENUMS.Formation.FixedWing.Wedge.Group = 196611 +ENUMS.Formation.FixedWing.EchelonRight={} +ENUMS.Formation.FixedWing.EchelonRight.Close = 262145 +ENUMS.Formation.FixedWing.EchelonRight.Open = 262146 +ENUMS.Formation.FixedWing.EchelonRight.Group = 262147 +ENUMS.Formation.FixedWing.EchelonLeft={} +ENUMS.Formation.FixedWing.EchelonLeft.Close = 327681 +ENUMS.Formation.FixedWing.EchelonLeft.Open = 327682 +ENUMS.Formation.FixedWing.EchelonLeft.Group = 327683 +ENUMS.Formation.FixedWing.FingerFour={} +ENUMS.Formation.FixedWing.FingerFour.Close = 393217 +ENUMS.Formation.FixedWing.FingerFour.Open = 393218 +ENUMS.Formation.FixedWing.FingerFour.Group = 393219 +ENUMS.Formation.FixedWing.Spread={} +ENUMS.Formation.FixedWing.Spread.Close = 458753 +ENUMS.Formation.FixedWing.Spread.Open = 458754 +ENUMS.Formation.FixedWing.Spread.Group = 458755 +ENUMS.Formation.FixedWing.BomberElement={} +ENUMS.Formation.FixedWing.BomberElement.Close = 786433 +ENUMS.Formation.FixedWing.BomberElement.Open = 786434 +ENUMS.Formation.FixedWing.BomberElement.Group = 786435 +ENUMS.Formation.FixedWing.BomberElementHeight={} +ENUMS.Formation.FixedWing.BomberElementHeight.Close = 851968 +ENUMS.Formation.FixedWing.FighterVic={} +ENUMS.Formation.FixedWing.FighterVic.Close = 917505 +ENUMS.Formation.FixedWing.FighterVic.Open = 917506 +ENUMS.Formation.RotaryWing={} +ENUMS.Formation.RotaryWing.Column={} +ENUMS.Formation.RotaryWing.Column.D70=720896 +ENUMS.Formation.RotaryWing.Wedge={} +ENUMS.Formation.RotaryWing.Wedge.D70=8 +ENUMS.Formation.RotaryWing.FrontRight={} +ENUMS.Formation.RotaryWing.FrontRight.D300=655361 +ENUMS.Formation.RotaryWing.FrontRight.D600=655362 +ENUMS.Formation.RotaryWing.FrontLeft={} +ENUMS.Formation.RotaryWing.FrontLeft.D300=655617 +ENUMS.Formation.RotaryWing.FrontLeft.D600=655618 +ENUMS.Formation.RotaryWing.EchelonRight={} +ENUMS.Formation.RotaryWing.EchelonRight.D70 =589825 +ENUMS.Formation.RotaryWing.EchelonRight.D300=589826 +ENUMS.Formation.RotaryWing.EchelonRight.D600=589827 +ENUMS.Formation.RotaryWing.EchelonLeft={} +ENUMS.Formation.RotaryWing.EchelonLeft.D70 =590081 +ENUMS.Formation.RotaryWing.EchelonLeft.D300=590082 +ENUMS.Formation.RotaryWing.EchelonLeft.D600=590083 + +--- Formations (old). The old format is a simplified version of the new formation enums, which allow more sophisticated settings. +-- See the [Formations](https://wiki.hoggitworld.com/view/DCS_enum_formation) on hoggit wiki. +-- @type ENUMS.FormationOld +ENUMS.FormationOld={} +ENUMS.FormationOld.FixedWing={} +ENUMS.FormationOld.FixedWing.LineAbreast=1 +ENUMS.FormationOld.FixedWing.Trail=2 +ENUMS.FormationOld.FixedWing.Wedge=3 +ENUMS.FormationOld.FixedWing.EchelonRight=4 +ENUMS.FormationOld.FixedWing.EchelonLeft=5 +ENUMS.FormationOld.FixedWing.FingerFour=6 +ENUMS.FormationOld.FixedWing.SpreadFour=7 +ENUMS.FormationOld.FixedWing.BomberElement=12 +ENUMS.FormationOld.FixedWing.BomberElementHeight=13 +ENUMS.FormationOld.FixedWing.FighterVic=14 +ENUMS.FormationOld={} +ENUMS.FormationOld.RotaryWing={} +ENUMS.FormationOld.RotaryWing.Wedge=8 +ENUMS.FormationOld.RotaryWing.Echelon=9 +ENUMS.FormationOld.RotaryWing.Front=10 +ENUMS.FormationOld.RotaryWing.Column=11 \ No newline at end of file diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 2980550b9..5bba94cc1 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1,5 +1,4 @@ ---- This module contains derived utilities taken from the MIST framework, --- which are excellent tools to be reused in an OO environment!. +--- This module contains derived utilities taken from the MIST framework, which are excellent tools to be reused in an OO environment. -- -- ### Authors: -- @@ -33,14 +32,14 @@ FLARECOLOR = trigger.flareColor -- #FLARECOLOR --- Big smoke preset enum. -- @type BIGSMOKEPRESET BIGSMOKEPRESET = { - SmallSmokeAndFire=0, - MediumSmokeAndFire=1, - LargeSmokeAndFire=2, - HugeSmokeAndFire=3, - SmallSmoke=4, - MediumSmoke=5, - LargeSmoke=6, - HugeSmoke=7, + SmallSmokeAndFire=1, + MediumSmokeAndFire=2, + LargeSmokeAndFire=3, + HugeSmokeAndFire=4, + SmallSmoke=5, + MediumSmoke=6, + LargeSmoke=7, + HugeSmoke=8, } --- DCS map as returned by env.mission.theatre. @@ -116,6 +115,7 @@ CALLSIGN={ --- Utilities static class. -- @type UTILS +-- @field #number _MarkID Marker index counter. Running number when marker is added. UTILS = { _MarkID = 1 } @@ -183,7 +183,9 @@ UTILS.IsInstanceOf = function( object, className ) end ---from http://lua-users.org/wiki/CopyTable +--- Deep copy a table. See http://lua-users.org/wiki/CopyTable +-- @param #table object The input table. +-- @return #table Copy of the input table. UTILS.DeepCopy = function(object) local lookup_table = {} local function _copy(object) @@ -204,7 +206,8 @@ UTILS.DeepCopy = function(object) end --- porting in Slmod's serialize_slmod2 +--- Porting in Slmod's serialize_slmod2. +-- @param #table tbl Input table. UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function lookup_table = {} @@ -340,18 +343,30 @@ UTILS.MiphToMps = function( miph ) return miph * 0.44704 end +--- Convert meters per second to miles per hour. +-- @param #number mps Speed in m/s. +-- @return #number Speed in miles per hour. UTILS.MpsToMiph = function( mps ) return mps / 0.44704 end +--- Convert meters per second to knots. +-- @param #number knots Speed in m/s. +-- @return #number Speed in knots. UTILS.MpsToKnots = function( mps ) return mps * 1.94384 --3600 / 1852 end +--- Convert knots to meters per second. +-- @param #number knots Speed in knots. +-- @return #number Speed in m/s. UTILS.KnotsToMps = function( knots ) return knots / 1.94384 --* 1852 / 3600 end +--- Convert temperature from Celsius to Farenheit. +-- @param #number Celcius Temperature in degrees Celsius. +-- @return #number Temperature in degrees Farenheit. UTILS.CelciusToFarenheit = function( Celcius ) return Celcius * 9/5 + 32 end @@ -982,7 +997,7 @@ end --- Returns the DCS map/theatre as optained by env.mission.theatre --- @return #string DCS map name . +-- @return #string DCS map name. function UTILS.GetDCSMap() return env.mission.theatre end @@ -1100,7 +1115,7 @@ end --- Get the callsign name from its enumerator value -- @param #number Callsign The enumerator callsign. --- @return #string The callsign name. +-- @return #string The callsign name or "Ghostrider". function UTILS.GetCallsignName(Callsign) for name, value in pairs(CALLSIGN.Aircraft) do @@ -1127,7 +1142,5 @@ function UTILS.GetCallsignName(Callsign) end end - return "Unknown Calsing" + return "Ghostrider" end - --- Just a test to see commits in new environment for Wingthor \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 828188dc9..e1be30eb4 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -751,7 +751,7 @@ end function CONTROLLABLE:CommandSetCallsign(CallName, CallNumber, Delay) -- Command to set the callsign. - local CommandSetCallsign={id='SetCallsign', params={callname=CallName, callnumber=CallNumber or 1}} + local CommandSetCallsign={id='SetCallsign', params={callname=CallName, number=CallNumber or 1}} if Delay and Delay>0 then SCHEDULER:New(nil, self.CommandSetCallsign, {self, CallName, CallNumber}, Delay) @@ -1011,7 +1011,6 @@ function CONTROLLABLE:TaskCarpetBombing(Vec2, GroupAttack, WeaponExpend, AttackQ id = 'CarpetBombing', params = { attackType = "Carpet", - point = Vec2, x = Vec2.x, y = Vec2.y, groupAttack = GroupAttack and GroupAttack or false, @@ -1019,7 +1018,7 @@ function CONTROLLABLE:TaskCarpetBombing(Vec2, GroupAttack, WeaponExpend, AttackQ weaponType = WeaponType or ENUMS.WeaponFlag.AnyBomb, expend = WeaponExpend or "All", attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, + attackQty = AttackQty or 1, directionEnabled = Direction and true or false, direction = Direction and math.rad(Direction) or 0, altitudeEnabled = Altitude and true or false, From 77de1dda2b8d77fe1271832249682533f85f7e98 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 3 May 2020 01:14:37 +0200 Subject: [PATCH 479/485] Stuff --- .../Moose/Functional/Warehouse.lua | 57 ++++++++++++------- Moose Development/Moose/Ops/Airboss.lua | 12 ++++ 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 6fe3be455..05e033a2c 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -4944,6 +4944,21 @@ function WAREHOUSE:onafterDefeated(From, Event, To) end end +--- Respawn warehouse. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function WAREHOUSE:onafterRespawn(From, Event, To) + + -- Info message. + local text=string.format("Respawning warehouse %s.", self.alias) + self:_InfoMessage(text) + + -- Respawn warehouse. + self.warehouse:ReSpawn() + +end --- On before "ChangeCountry" event. Checks whether a change of country is necessary by comparing the actual country to the the requested one. -- @param #WAREHOUSE self @@ -4967,22 +4982,6 @@ function WAREHOUSE:onbeforeChangeCountry(From, Event, To, Country) return false end ---- Respawn warehouse. --- @param #WAREHOUSE self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function WAREHOUSE:onafterRespawn(From, Event, To) - - -- Info message. - local text=string.format("Respawning warehouse %s.", self.alias) - self:_InfoMessage(text) - - -- Respawn warehouse. - self.warehouse:ReSpawn() - -end - --- On after "ChangeCountry" event. Warehouse is respawned with the specified country. All queued requests are deleted and the owned airbase is reset if the coalition is changed by changing the -- country. -- @param #WAREHOUSE self @@ -4995,19 +4994,21 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) local CoalitionOld=self:GetCoalition() -- Respawn warehouse with new coalition/country. - self.warehouse:ReSpawn(Country) + self:Respawn(Country) local CoalitionNew=self:GetCoalition() -- Delete all waiting requests because they are not valid any more. self.queue=nil self.queue={} + + -- Get airbase of this warehouse. + local airbase=AIRBASE:FindByName(self.airbasename) -- Airbase could have been captured before and already belongs to the new coalition. -- Check if Warehouse has a arbiase atthached local airbasecoaltion - if self.airbase ~= nil then - local airbase=AIRBASE:FindByName(self.airbasename) + if self.airbase~=nil then airbasecoaltion=airbase:GetCoalition() else -- Warehouse has no airbase attached so just keep whatever, self.airbase will still be nil since CoalitionNew will not be nil if Warehouse have a airbse attacjed. airbasecoaltion = nil @@ -5032,6 +5033,20 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) end +--- On before "Captured" event. Warehouse has been captured by another coalition. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#coalition.side Coalition which captured the warehouse. +-- @param DCS#country.id Country which has captured the warehouse. +function WAREHOUSE:onbeforeCaptured(From, Event, To, Coalition, Country) + + -- Warehouse respawned. + self:ChangeCountry(Country) + +end + --- On after "Captured" event. Warehouse has been captured by another coalition. -- @param #WAREHOUSE self -- @param #string From From state. @@ -5044,9 +5059,7 @@ function WAREHOUSE:onafterCaptured(From, Event, To, Coalition, Country) -- Message. local text=string.format("Warehouse %s: We were captured by enemy coalition (side=%d)!", self.alias, Coalition) self:_InfoMessage(text) - - -- Warehouse respawned. - self:ChangeCountry(Country) + end diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index d604d7441..1ae6b24fa 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -1273,11 +1273,17 @@ AIRBOSS.AircraftCarrier={ --- Carrier types. -- @type AIRBOSS.CarrierType +-- @field #string ROOSEVELT USS Theodore Roosevelt (CVN-71) +-- @field #string LINCOLN USS Abraham Lincoln (CVN-72) +-- @field #string WASHINGTON USS George Washington (CVN-73) -- @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) AIRBOSS.CarrierType={ + ROOSEVELT="CVN_71", + LINCOLN="CVN_72", + WASHINGTON="CVN_73", STENNIS="Stennis", VINSON="VINSON", TARAWA="LHA_Tarawa", @@ -1929,6 +1935,12 @@ function AIRBOSS:New(carriername, alias) -- Init carrier parameters. if self.carriertype==AIRBOSS.CarrierType.STENNIS then self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.ROOSEVELT then + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.LINCOLN then + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.WASHINGTON then + self:_InitStennis() elseif self.carriertype==AIRBOSS.CarrierType.VINSON then -- TODO: Carl Vinson parameters. self:_InitStennis() From bee44b97fd59110a290c7396a977cf2d49a93da6 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 3 May 2020 23:56:32 +0200 Subject: [PATCH 480/485] WAREHOUSE v1.0.1 --- Moose Development/Moose/Core/SpawnStatic.lua | 28 ++++--- .../Moose/Functional/Warehouse.lua | 73 ++++++++++++------- Moose Development/Moose/Wrapper/Static.lua | 2 +- 3 files changed, 66 insertions(+), 37 deletions(-) diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index 66c3581d0..8f96a1c60 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -240,23 +240,31 @@ end --- Respawns the original @{Static}. -- @param #SPAWNSTATIC self +-- @param #number delay Delay before respawn in seconds. -- @return #SPAWNSTATIC -function SPAWNSTATIC:ReSpawn() +function SPAWNSTATIC:ReSpawn(delay) - local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix ) + if delay and delay>0 then + self:ScheduleOnce(delay, SPAWNSTATIC.ReSpawn, self) + else - if StaticTemplate then - - local StaticUnitTemplate = StaticTemplate.units[1] - StaticTemplate.route = nil - StaticTemplate.groupId = nil + local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix ) - local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] ) + if StaticTemplate then + + local StaticUnitTemplate = StaticTemplate.units[1] + StaticTemplate.route = nil + StaticTemplate.groupId = nil + + local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] ) + + return _DATABASE:FindStatic(Static:getName()) + end - return _DATABASE:FindStatic(Static:getName()) + return nil end - return nil + return self end diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 05e033a2c..b2a3a9d52 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1764,7 +1764,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="1.0.0" +WAREHOUSE.version="1.0.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1870,6 +1870,12 @@ function WAREHOUSE:New(warehouse, alias) -- Set unique ID for this warehouse. self.uid=_WAREHOUSEDB.WarehouseID + + -- Coalition of the warehouse. + self.coalition=self.warehouse:GetCoalition() + + -- Country of the warehouse. + self.countryid=self.warehouse:GetCountry() -- Closest of the same coalition but within 5 km range. local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition()) @@ -3395,8 +3401,13 @@ end -- @param #string To To state. function WAREHOUSE:onafterStatus(From, Event, To) + local FSMstate=self:GetState() + + local coalition=self:GetCoalitionName() + local country=self:GetCountryName() + -- Info. - self:I(self.lid..string.format("State=%s, Assets=%d, Requests: waiting=%d, pending=%d", self:GetState(), #self.stock, #self.queue, #self.pending)) + self:I(self.lid..string.format("State=%s %s [%s]: Assets=%d, Requests: waiting=%d, pending=%d", FSMstate, country, coalition, #self.stock, #self.queue, #self.pending)) -- Check if any pending jobs are done and can be deleted from the queue. self:_JobDone() @@ -4949,11 +4960,16 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function WAREHOUSE:onafterRespawn(From, Event, To) +-- @param #number CountryID The country ID (determines also the coaliton) of the respawned warehouse. +function WAREHOUSE:onafterRespawn(From, Event, To, CountryID) -- Info message. - local text=string.format("Respawning warehouse %s.", self.alias) + local text=string.format("Respawning warehouse %s", self.alias) self:_InfoMessage(text) + + if self.warehouse and self.warehouse:IsAlive() then + self.warehouse:Destroy() + end -- Respawn warehouse. self.warehouse:ReSpawn() @@ -4982,19 +4998,26 @@ function WAREHOUSE:onbeforeChangeCountry(From, Event, To, Country) return false end +--- On after "ChangeCountry" event. Warehouse is respawned with the specified country. All queued requests are deleted and the owned airbase is reset if the coalition is changed by changing the +-- country. +-- @param #WAREHOUSE self +-- @param DCS#country.id Country Country which has captured the warehouse. +function WAREHOUSE:_ChangeCountry(From, Event, To, Country) + +end + --- On after "ChangeCountry" event. Warehouse is respawned with the specified country. All queued requests are deleted and the owned airbase is reset if the coalition is changed by changing the -- country. -- @param #WAREHOUSE self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param DCS#country.id Country which has captured the warehouse. +-- @param DCS#country.id Country Country which has captured the warehouse. function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) local CoalitionOld=self:GetCoalition() - -- Respawn warehouse with new coalition/country. - self:Respawn(Country) + self.warehouse:ReSpawn(Country) local CoalitionNew=self:GetCoalition() @@ -5002,24 +5025,22 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) self.queue=nil self.queue={} - -- Get airbase of this warehouse. - local airbase=AIRBASE:FindByName(self.airbasename) - - -- Airbase could have been captured before and already belongs to the new coalition. - -- Check if Warehouse has a arbiase atthached - local airbasecoaltion - if self.airbase~=nil then - airbasecoaltion=airbase:GetCoalition() - else -- Warehouse has no airbase attached so just keep whatever, self.airbase will still be nil since CoalitionNew will not be nil if Warehouse have a airbse attacjed. - airbasecoaltion = nil - end - - if CoalitionNew==airbasecoaltion then - -- Airbase already owned by the coalition that captured the warehouse. Airbase can be used by this warehouse. - self.airbase=airbase - else - -- Airbase is owned by other coalition. So this warehouse does not have an airbase unil it is captured. - self.airbase=nil + if self.airbasename then + + -- Get airbase of this warehouse. + local airbase=AIRBASE:FindByName(self.airbasename) + + -- Get coalition of the airbase. + local airbaseCoalition=airbase:GetCoalition() + + if CoalitionNew==airbaseCoalition then + -- Airbase already owned by the coalition that captured the warehouse. Airbase can be used by this warehouse. + self.airbase=airbase + else + -- Airbase is owned by other coalition. So this warehouse does not have an airbase until it is captured. + self.airbase=nil + end + end -- Debug smoke. @@ -8487,7 +8508,7 @@ function WAREHOUSE:_DebugMessage(text, duration) if duration>0 then MESSAGE:New(text, duration):ToAllIf(self.Debug) end - self:I(self.lid..text) + self:T(self.lid..text) end --- Error message. Message send to all (if duration > 0). Text self:E(text) added to DCS.log file. diff --git a/Moose Development/Moose/Wrapper/Static.lua b/Moose Development/Moose/Wrapper/Static.lua index 16b07c03f..9df2fd594 100644 --- a/Moose Development/Moose/Wrapper/Static.lua +++ b/Moose Development/Moose/Wrapper/Static.lua @@ -224,7 +224,7 @@ function STATIC:ReSpawn(countryid, Delay) else local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName, countryid ) - + SpawnStatic:ReSpawn() end From 6b318d03098d5eb768bda184add01476aa8b30da Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 4 May 2020 00:31:43 +0200 Subject: [PATCH 481/485] Update Enums.lua --- Moose Development/Moose/Utilities/Enums.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index e59b958f5..da73fd4b6 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -238,7 +238,6 @@ ENUMS.FormationOld.FixedWing.SpreadFour=7 ENUMS.FormationOld.FixedWing.BomberElement=12 ENUMS.FormationOld.FixedWing.BomberElementHeight=13 ENUMS.FormationOld.FixedWing.FighterVic=14 -ENUMS.FormationOld={} ENUMS.FormationOld.RotaryWing={} ENUMS.FormationOld.RotaryWing.Wedge=8 ENUMS.FormationOld.RotaryWing.Echelon=9 From dcd106049659cbd03eac73f7b1fd68139ec80455 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 4 May 2020 00:33:33 +0200 Subject: [PATCH 482/485] Update Warehouse.lua --- Moose Development/Moose/Functional/Warehouse.lua | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index b2a3a9d52..ab286591c 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -4998,14 +4998,6 @@ function WAREHOUSE:onbeforeChangeCountry(From, Event, To, Country) return false end ---- On after "ChangeCountry" event. Warehouse is respawned with the specified country. All queued requests are deleted and the owned airbase is reset if the coalition is changed by changing the --- country. --- @param #WAREHOUSE self --- @param DCS#country.id Country Country which has captured the warehouse. -function WAREHOUSE:_ChangeCountry(From, Event, To, Country) - -end - --- On after "ChangeCountry" event. Warehouse is respawned with the specified country. All queued requests are deleted and the owned airbase is reset if the coalition is changed by changing the -- country. -- @param #WAREHOUSE self From 7d8e27c83e01d14f329f5a84b76904def498e53f Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 4 May 2020 00:37:28 +0200 Subject: [PATCH 483/485] Update Warehouse.lua --- Moose Development/Moose/Functional/Warehouse.lua | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index ab286591c..433e3f178 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -4960,16 +4960,11 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number CountryID The country ID (determines also the coaliton) of the respawned warehouse. -function WAREHOUSE:onafterRespawn(From, Event, To, CountryID) +function WAREHOUSE:onafterRespawn(From, Event, To) -- Info message. local text=string.format("Respawning warehouse %s", self.alias) self:_InfoMessage(text) - - if self.warehouse and self.warehouse:IsAlive() then - self.warehouse:Destroy() - end -- Respawn warehouse. self.warehouse:ReSpawn() From 0b7ac754c521e720396ebe49909ba6f5ec8c8130 Mon Sep 17 00:00:00 2001 From: zlinman <63908222+zlinman@users.noreply.github.com> Date: Mon, 4 May 2020 12:53:09 +0200 Subject: [PATCH 484/485] only type error corrected, spaces deleted --- Moose Development/Moose/AI/AI_A2A_Cap.lua | 81 +- .../Moose/AI/AI_A2A_Dispatcher.lua | 2479 ++++++++--------- Moose Development/Moose/AI/AI_A2A_Gci.lua | 76 +- .../Moose/Actions/Act_Account.lua | 132 +- .../Moose/Actions/Act_Assign.lua | 142 +- .../Moose/Actions/Act_Assist.lua | 110 +- Moose Development/Moose/Actions/Act_Route.lua | 196 +- 7 files changed, 1605 insertions(+), 1611 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Cap.lua b/Moose Development/Moose/AI/AI_A2A_Cap.lua index a5bee30e4..08062cc11 100644 --- a/Moose Development/Moose/AI/AI_A2A_Cap.lua +++ b/Moose Development/Moose/AI/AI_A2A_Cap.lua @@ -1,10 +1,10 @@ --- **AI** -- (R2.2) - Models the process of Combat Air Patrol (CAP) for airplanes. -- -- === --- +-- -- ### Author: **FlightControl** --- --- === +-- +-- === -- -- @module AI.AI_A2A_Cap -- @image AI_Combat_Air_Patrol.JPG @@ -14,54 +14,54 @@ -- @extends AI.AI_Air_Engage#AI_AIR_ENGAGE ---- The AI_A2A_CAP class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} +--- The AI_A2A_CAP 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_A2A_CAP is assigned a @{Wrapper.Group} and this must be done before the AI_A2A_CAP 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_A2A_CAP constructor --- +-- -- * @{#AI_A2A_CAP.New}(): Creates a new AI_A2A_CAP object. --- +-- -- ## 2. AI_A2A_CAP is a FSM --- +-- -- ![Process](..\Presentations\AI_CAP\Dia2.JPG) --- +-- -- ### 2.1 AI_A2A_CAP 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_A2A_CAP 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_A2A_CAP.Engage}**: Let the AI engage the bogeys. @@ -74,25 +74,25 @@ -- * **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, +-- +-- 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_A2A_CAP.SetEngageRange}() to define that range. -- -- ## 4. Set the Zone of Engagement --- +-- -- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) --- --- An optional @{Zone} can be set, +-- +-- 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_A2A_CAP.SetEngageZone}() to define that Zone. --- +-- -- === --- +-- -- @field #AI_A2A_CAP AI_A2A_CAP = { ClassName = "AI_A2A_CAP", @@ -124,7 +124,7 @@ function AI_A2A_CAP:New2( AICap, EngageMinSpeed, EngageMaxSpeed, EngageFloorAlti self:SetFuelThreshold( .2, 60 ) self:SetDamageThreshold( 0.4 ) self:SetDisengageRadius( 70000 ) - + return self end @@ -160,35 +160,35 @@ function AI_A2A_CAP:onafterStart( AICap, From, Event, To ) end ---- Set the Engage Zone which defines where the AI will engage bogies. +--- Set the Engage Zone which defines where the AI will engage bogies. -- @param #AI_A2A_CAP self -- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. -- @return #AI_A2A_CAP self function AI_A2A_CAP:SetEngageZone( EngageZone ) self:F2() - if EngageZone then + if EngageZone then self.EngageZone = EngageZone else self.EngageZone = nil end end ---- Set the Engage Range when the AI will engage with airborne enemies. +--- Set the Engage Range when the AI will engage with airborne enemies. -- @param #AI_A2A_CAP self -- @param #number EngageRange The Engage Range. -- @return #AI_A2A_CAP self function AI_A2A_CAP:SetEngageRange( EngageRange ) self:F2() - if EngageRange then + if EngageRange then self.EngageRange = EngageRange else self.EngageRange = nil end end ---- Evaluate the attack and create an AttackUnitTask list. +--- Evaluate the attack and create an AttackUnitTask list. -- @param #AI_A2A_CAP self -- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. -- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. @@ -202,12 +202,11 @@ function AI_A2A_CAP:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageA local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT if AttackUnit and AttackUnit:IsAlive() and AttackUnit:IsAir() then -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition() - -- Maybe the detected set also contains + -- Maybe the detected set also contains self:T( { "Attacking Task:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) end end - + return AttackUnitTasks end - diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 0e13f7c2c..cdf8d52ad 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -1,9 +1,9 @@ --- **AI** - (R2.2) - Manages the process of an automatic A2A defense system based on an EWR network targets and coordinating CAP and GCI. --- +-- -- === --- +-- -- Features: --- +-- -- * Setup quickly an A2A defense system for a coalition. -- * Setup (CAP) Control Air Patrols at defined zones to enhance your A2A defenses. -- * Setup (GCI) Ground Control Intercept at defined airbases to enhance your A2A defenses. @@ -18,170 +18,170 @@ -- * Setup specific settings for specific squadrons. -- * Quickly setup an A2A defense system using @{#AI_A2A_GCICAP}. -- * Setup a more advanced defense system using @{#AI_A2A_DISPATCHER}. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [AID-A2A - AI A2A Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching) --- +-- -- === --- +-- -- ## YouTube Channel: --- +-- -- [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) --- +-- -- === --- +-- -- # QUICK START GUIDE --- +-- -- There are basically two classes available to model an A2A defense system. --- +-- -- AI\_A2A\_DISPATCHER is the main A2A defense class that models the A2A defense system. -- AI\_A2A\_GCICAP derives or inherits from AI\_A2A\_DISPATCHER and is a more **noob** user friendly class, but is less flexible. --- +-- -- Before you start using the AI\_A2A\_DISPATCHER or AI\_A2A\_GCICAP ask youself the following questions. --- +-- -- ## 0. Do I need AI\_A2A\_DISPATCHER or do I need AI\_A2A\_GCICAP? --- +-- -- AI\_A2A\_GCICAP, automates a lot of the below questions using the mission editor and requires minimal lua scripting. -- But the AI\_A2A\_GCICAP provides less flexibility and a lot of options are defaulted. -- With AI\_A2A\_DISPATCHER you can setup a much more **fine grained** A2A defense mechanism, but some more (easy) lua scripting is required. --- +-- -- ## 1. Which Coalition am I modeling an A2A defense system for? blue or red? --- +-- -- One AI\_A2A\_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\_A2A\_DISPATCHER **objects**, -- each governing their defense system. --- --- +-- +-- -- ## 2. Which type of EWR 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: --- +-- -- * Perform detections from multiple FACs as one co-operating entity. -- * Communicate with a Head Quarters, 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. +-- +-- 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 +-- 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. --- +-- -- ## 4. Is a border required? --- --- 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 +-- +-- Is this a cold war 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. --- +-- -- ## 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. --- +-- -- ## 6. Which Airbases, Carrier Ships, Farps will take part in the defense system for the Coalition? --- +-- -- 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? --- +-- -- The defense system works with Squadrons. Each Squadron must be given a unique name, that forms the **key** to the defense system. -- Several options and activities can be set per Squadron. --- +-- -- ## 8. 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. --- +-- -- ## 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**. -- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**. -- The A2A defense system will select from the given templates a random template to spawn a new plane (group). --- +-- -- ## 10. 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. +-- +-- 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 A2A 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. --- +-- -- ## 12. For each Squadron doing CAP, in which ZONE(s) will the CAP be performed? --- +-- -- 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? --- +-- -- Per CAP 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. --- +-- -- ## 14. For each Squadron doing CAP, what are the time intervals and CAP amounts to be performed? --- +-- -- For each CAP: --- +-- -- * **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? --- +-- -- ## 15. For each Squadron, which will perform GCI? --- +-- -- For each Squadron, evaluate which Squadrons will perform GCI? -- Not all Squadrons need to perform GCI. --- +-- -- ## 16. 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.** --- +-- -- ## 17. 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.** --- +-- -- ## 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? -- 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. --- +-- -- **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? -- Per one, two, three, four? --- +-- -- **The default grouping is 1. That means, that each spawned defender will act individually.** --- +-- -- === --- +-- -- ### Authors: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). -- ### Authors: **Stonehouse**, **SNAFU** in terms of the advice, documentation, and the original GCICAP script. --- +-- -- @module AI.AI_A2A_Dispatcher -- @image AI_Air_To_Air_Dispatching.JPG @@ -193,657 +193,657 @@ do -- AI_A2A_DISPATCHER -- @type AI_A2A_DISPATCHER -- @extends Tasking.DetectionManager#DETECTION_MANAGER - --- Create an automatic air defence system for a coalition. - -- + --- Create an automatic air defence system for a coalition. + -- -- === - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia3.JPG) - -- - -- It 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 a ground based radar network. + -- + -- It 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 a ground based radar network. -- 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 an interception or return to base a new CAP flight will spawn to take their place. -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control. - -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system. + -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system. -- In short it is a plug in very flexible and configurable air defence module for DCS World. - -- + -- -- Note that in order to create a two way A2A defense system, two AI\_A2A\_DISPATCHER defense system may need to be created, for each coalition one. -- This is a good implementation, because maybe in the future, more coalitions may become available in DCS world. - -- + -- -- === -- -- # USAGE GUIDE - -- + -- -- ## 1. AI\_A2A\_DISPATCHER constructor: - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_1.JPG) - -- - -- + -- + -- -- The @{#AI_A2A_DISPATCHER.New}() method creates a new AI\_A2A\_DISPATCHER instance. - -- + -- -- ### 1.1. Define the **EWR network**: - -- + -- -- As part of the AI\_A2A\_DISPATCHER :New() constructor, an EWR network must be given as the first parameter. -- 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. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia5.JPG) - -- - -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. + -- + -- 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 + -- 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. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia7.JPG) - -- - -- 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. - -- + -- + -- 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 constructed**, that is, they form part of the @{Functional.Detection#DETECTION_BASE} object that is given as the input parameter of the AI\_A2A\_DISPATCHER class. - -- 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, + -- 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. - -- + -- -- See the following example to setup an EWR network containing EWR stations and AWACS. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_2.JPG) -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_3.JPG) - -- + -- -- -- Define a SET_GROUP object that builds a collection of groups that define the EWR network. -- -- Here we build the network with all the groups that have a name starting with DF CCCP AWACS and DF CCCP EWR. -- DetectionSetGroup = SET_GROUP:New() -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) -- DetectionSetGroup:FilterStart() - -- + -- -- -- Setup the detection and group targets to a 30km range! -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) -- -- -- Setup the A2A dispatcher, and initialize it. -- A2ADispatcher = AI_A2A_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 **DF CCCP AWACS** or **DF CCCP EWR** to be included in the Set. -- **DetectionSetGroup** is then being ordered to start the dynamic filtering. Note that any destroy or new spawn of a group with the above names will be removed or added to the Set. - -- + -- -- Then a new Detection object is created from the class DETECTION_AREAS. A grouping radius of 30000 is choosen, which is 30km. -- The **Detection** object is then passed to the @{#AI_A2A_DISPATCHER.New}() method to indicate the EWR network configuration and setup the A2A defense detection mechanism. - -- + -- -- You could build a **mutual defense system** like this: - -- + -- -- A2ADispatcher_Red = AI_A2A_DISPATCHER:New( EWR_Red ) -- A2ADispatcher_Blue = AI_A2A_DISPATCHER:New( EWR_Blue ) - -- + -- -- ### 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. - -- Fast planes like in the 80s, need a larger radius than WWII planes. + -- 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. - -- + -- -- Note that 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. This may result in additional GCI being started by the dispatcher! So don't make this value too small! - -- + -- -- ## 3. Set the **Engage Radius**: - -- - -- Define the **Engage Radius** to **engage any target by airborne friendlies**, + -- + -- Define the **Engage Radius** to **engage any target by airborne friendlies**, -- which are executing **cap** or **returning** from an intercept mission. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia10.JPG) - -- - -- If there is a target area detected and reported, - -- then any friendlies that are airborne near this target area, + -- + -- 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 **50000** or **50km** is given as a value, then any friendly that is airborne within **50km** from the detected target, + -- + -- For example, if **50000** or **50km** is given as a value, then any friendly that is airborne within **50km** 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 intercept 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. - -- - -- The **default** Engage Radius is defined as **100000** or **100km**. + -- + -- The **default** Engage Radius is defined as **100000** or **100km**. -- Use the method @{#AI_A2A_DISPATCHER.SetEngageRadius}() to set a specific Engage Radius. -- **The Engage Radius is defined for ALL squadrons which are operational.** - -- + -- -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2A%20-%20Engage%20Range%20Test) - -- + -- -- In this example an Engage Radius is set to various values. - -- + -- -- -- Set 50km as the radius to engage any target by airborne friendlies. -- A2ADispatcher:SetEngageRadius( 50000 ) - -- + -- -- -- Set 100km as the radius to engage any target by airborne friendlies. -- A2ADispatcher:SetEngageRadius() -- 100000 is the default value. - -- - -- + -- + -- -- ## 4. Set the **Ground Controlled Intercept Radius** or **Gci radius**: - -- + -- -- When targets are detected that are still really far off, you don't want the AI_A2A_DISPATCHER to launch intercepts just yet. - -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** + -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** -- being **smaller** than the **Ground Controlled Intercept radius** or **Gci radius**. - -- - -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or, + -- + -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or, -- when you don't want to let the AI_A2A_DISPATCHER react immediately when a certain border or area is not being crossed. - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetGciRadius}() to set a specific controlled ground intercept radius. -- **The Ground Controlled Intercept radius is defined for ALL squadrons which are operational.** - -- + -- -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-013%20-%20AI_A2A%20-%20Intercept%20Test) - -- + -- -- In these examples, the Gci Radius is set to various values: - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Set 100km as the radius to ground control intercept detected targets from the nearest airbase. -- A2ADispatcher:SetGciRadius( 100000 ) - -- + -- -- -- Set 200km as the radius to ground control intercept. -- A2ADispatcher:SetGciRadius() -- 200000 is the default value. - -- + -- -- ## 5. Set the **borders**: - -- - -- According to the tactical and strategic design of the mission broadly decide the shape and extent of red and blue territories. + -- + -- According to the tactical and strategic design of the mission broadly decide the shape and extent of red and blue territories. -- They should be laid out such that a border area is created between the two coalitions. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia4.JPG) - -- + -- -- **Define a border area to simulate a cold war scenario.** -- Use the method @{#AI_A2A_DISPATCHER.SetBorderZone}() to create a border zone for the dispatcher. - -- + -- -- 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. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia9.JPG) - -- + -- -- 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}. - -- 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. + -- 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. - -- + -- -- Demonstration Mission: [AID-009 - AI_A2A - Border Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-009 - AI_A2A - Border Test) - -- + -- -- In this example a border is set for the CCCP A2A dispatcher: - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_4.JPG) - -- + -- -- -- Setup the A2A dispatcher, and initialize it. -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- -- -- Setup the border. - -- -- Initialize the dispatcher, setting up a border zone. This is a polygon, + -- -- Initialize the dispatcher, setting up a border zone. This is a polygon, -- -- which takes the waypoints of a late activated group with the name CCCP Border as the boundaries of the border area. -- -- Any enemy crossing this border will be engaged. - -- + -- -- CCCPBorderZone = ZONE_POLYGON:New( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- A2ADispatcher:SetBorderZone( CCCPBorderZone ) - -- - -- ## 6. Squadrons: - -- + -- + -- ## 6. Squadrons: + -- -- The AI\_A2A\_DISPATCHER works with **Squadrons**, that need to be defined using the different methods available. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, -- while defining which plane types are being used by the squadron and how many resources are available. - -- + -- -- Squadrons: - -- + -- -- * Have name (string) that is the identifier or key of the squadron. -- * Have specific plane types. -- * Are located at one airbase. -- * 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\_A2A\_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. - -- + -- -- This example defines a couple of squadrons. Note the templates defined within the Mission Editor. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_5.JPG) -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_6.JPG) - -- + -- -- -- Setup the squadrons. -- A2ADispatcher:SetSquadron( "Mineralnye", AIRBASE.Caucasus.Mineralnye_Vody, { "SQ CCCP SU-27" }, 20 ) -- A2ADispatcher:SetSquadron( "Maykop", AIRBASE.Caucasus.Maykop_Khanskaya, { "SQ CCCP MIG-31" }, 20 ) -- A2ADispatcher:SetSquadron( "Mozdok", AIRBASE.Caucasus.Mozdok, { "SQ CCCP MIG-31" }, 20 ) -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-27" }, 20 ) -- A2ADispatcher:SetSquadron( "Novo", AIRBASE.Caucasus.Novorossiysk, { "SQ CCCP SU-27" }, 20 ) - -- + -- -- ### 6.1. Set squadron take-off methods - -- + -- -- Use the various SetSquadronTakeoff... methods to control how squadrons are taking-off from the airfield: - -- + -- -- * @{#AI_A2A_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_A2A_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. -- * @{#AI_A2A_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_A2A_DISPATCHER.Takeoff.Air ) -- A2ADispatcher:SetSquadronTakeoffInAir( "Sochi" ) -- A2ADispatcher:SetSquadronTakeoffFromRunway( "Mozdok" ) -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" ) - -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) - -- - -- + -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) + -- + -- -- ### 6.1. Set Squadron takeoff altitude when spawning new aircraft in the air. - -- + -- -- In the case of the @{#AI_A2A_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_A2A_DISPATCHER.SetSquadronTakeoffInAirAltitude}() to set the altitude for a specific squadron. -- The default takeoff altitude can be modified or set using the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAirAltitude}(). -- As part of the method @{#AI_A2A_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. - -- + -- -- ### 6.2. Set squadron landing methods - -- + -- -- In analogy with takeoff, the landing methods are to control how squadrons land at the airfield: - -- + -- -- * @{#AI_A2A_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_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. -- * @{#AI_A2A_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_A2A_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_A2A_DISPATCHER.Landing.AtRunway ) - -- - -- + -- + -- -- ### 6.3. Set squadron grouping - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronGrouping}() to set the grouping of CAP or GCI flights that will take-off when spawned. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia12.JPG) - -- + -- -- In the case of GCI, the @{#AI_A2A_DISPATCHER.SetSquadronGrouping}() method has additional behaviour. When there aren't enough CAP flights airborne, a GCI will be initiated for the remaining - -- targets to be engaged. Depending on the grouping parameter, the spawned flights for GCI are grouped into this setting. - -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by CAP or any airborne flight, + -- targets to be engaged. Depending on the grouping parameter, the spawned flights for GCI are grouped into this setting. + -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by CAP or any airborne flight, -- a GCI needs to be started, the GCI flights will be grouped as follows: Group 1 of 2 flights and Group 2 of one flight! - -- + -- -- Even more ... If one target has been detected, and the overhead is 1.5, grouping is 1, then two groups of planes will be spawned, with one unit each! - -- + -- -- 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. - -- + -- -- ### 6.4. Overhead and Balance the effectiveness of the air defenses in case of GCI. - -- + -- -- 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**. - -- + -- The **default value** of the overhead parameter is 1.0, which means **equal balance**. + -- -- ![Banner Image](..\Presentations\AI_A2A_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_A2A_DISPATCHER.SetSquadronOverhead}() method can be used to tweak the defense strength, - -- taking into account the plane types of the squadron. - -- + -- taking into account the plane types of the squadron. + -- -- For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles... -- So in this case, one may want to use the @{#AI_A2A_DISPATCHER.SetOverhead}() 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: - -- + -- 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 planes detected, 6 planes will be spawned. -- * Lower than 1, for example 0.75, will decrease the defense unit amounts. For 4 planes detected, only 3 planes will be spawned. - -- - -- 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 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. + -- -- For example ... If one target has been detected, and the overhead is 1.5, grouping is 1, then two groups of planes will be spawned, with one unit each! - -- + -- -- 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. -- -- ## 6.5. Squadron fuel treshold. - -- + -- -- When an airplane gets **out of fuel** to a certain %-tage, which is by default **15% (0.15)**, there are two possible actions that can be taken: -- - The defender will go RTB, and will be replaced with a new defender if possible. -- - The defender will refuel at a tanker, if a tanker has been specified for the squadron. - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel treshold** of spawned airplanes for all squadrons. - -- + -- -- ## 7. Setup a squadron for CAP - -- + -- -- ### 7.1. Set the CAP zones - -- + -- -- CAP zones are patrol areas where Combat Air Patrol (CAP) flights loiter until they either return to base due to low fuel or are assigned an interception task by ground control. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia6.JPG) - -- - -- * As the CAP flights wander around within the zone waiting to be tasked, these zones need to be large enough that the aircraft are not constantly turning + -- + -- * As the CAP flights wander around within the zone waiting to be tasked, these zones need to be large enough that the aircraft are not constantly turning -- but do not have to be big and numerous enough to completely cover a border. - -- + -- -- * CAP zones can be of any type, and are derived from the @{Core.Zone#ZONE_BASE} class. Zones can be @{Core.Zone#ZONE}, @{Core.Zone#ZONE_POLYGON}, @{Core.Zone#ZONE_UNIT}, @{Core.Zone#ZONE_GROUP}, etc. -- This allows to setup **static, moving and/or complex zones** wherein aircraft will perform the CAP. - -- - -- * Typically 20000-50000 metres width is used and they are spaced so that aircraft in the zone waiting for tasks don't have to far to travel to protect their coalitions important targets. - -- These targets are chosen as part of the mission design and might be an important airfield or town etc. - -- Zone size is also determined somewhat by territory size, plane types + -- + -- * Typically 20000-50000 metres width is used and they are spaced so that aircraft in the zone waiting for tasks don't have to far to travel to protect their coalitions important targets. + -- These targets are chosen as part of the mission design and might be an important airfield or town etc. + -- Zone size is also determined somewhat by territory size, plane types -- (eg WW2 aircraft might mean smaller zones or more zones because they are slower and take longer to intercept enemy aircraft). - -- - -- * In a **cold war** it is important to make sure a CAP zone doesn't intrude into enemy territory as otherwise CAP flights will likely cross borders + -- + -- * In a **cold war** it is important to make sure a CAP zone doesn't intrude into enemy territory as otherwise CAP flights will likely cross borders -- and spark a full scale conflict which will escalate rapidly. - -- - -- * CAP flights do not need to be in the CAP zone before they are "on station" and ready for tasking. - -- + -- + -- * CAP flights do not need to be in the CAP zone before they are "on station" and ready for tasking. + -- -- * Typically if a CAP flight is tasked and therefore leaves their zone empty while they go off and intercept their target another CAP flight will spawn to take their place. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia7.JPG) - -- + -- -- The following example illustrates how CAP zones are coded: - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_8.JPG) - -- + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_8.JPG) + -- -- -- CAP Squadron execution. -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_7.JPG) - -- + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_7.JPG) + -- -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_9.JPG) - -- + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_9.JPG) + -- -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- -- Note the different @{Zone} MOOSE classes being used to create zones of different types. Please click the @{Zone} link for more information about the different zone types. -- Zones can be circles, can be setup in the mission editor using trigger zones, but can also be setup in the mission editor as polygons and in this case GROUP objects are being used! - -- + -- -- ## 7.2. Set the squadron to execute CAP: - -- + -- -- The method @{#AI_A2A_DISPATCHER.SetSquadronCap}() 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. - -- + -- + -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. + -- -- The @{#AI_A2A_DISPATCHER.SetSquadronCapInterval}() 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. - -- + -- + -- 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": - -- + -- -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- -- ## 7.3. Squadron tanker to refuel when executing CAP and defender is out of fuel. - -- + -- -- 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_A2A_DISPATCHER.SetDefaultTanker}() to set the default tanker for the refuelling. -- You can also specify a specific tanker for refuelling for a squadron by using the method @{#AI_A2A_DISPATCHER.SetSquadronTanker}(). - -- + -- -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. - -- + -- -- For example, the following setup will create a CAP for squadron "Gelend" with a refuel task for the squadron: - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_10.JPG) - -- + -- -- -- Define the CAP -- A2ADispatcher:SetSquadron( "Gelend", AIRBASE.Caucasus.Gelendzhik, { "SQ CCCP SU-30" }, 20 ) -- A2ADispatcher:SetSquadronCap( "Gelend", ZONE:New( "PatrolZoneGelend" ), 4000, 8000, 600, 800, 1000, 1300 ) - -- A2ADispatcher:SetSquadronCapInterval( "Gelend", 2, 30, 600, 1 ) + -- A2ADispatcher:SetSquadronCapInterval( "Gelend", 2, 30, 600, 1 ) -- A2ADispatcher:SetSquadronGci( "Gelend", 900, 1200 ) - -- + -- -- -- Setup the Refuelling for squadron "Gelend", at tanker (group) "TankerGelend" when the fuel in the tank of the CAP defenders is less than 80%. -- A2ADispatcher:SetSquadronFuelThreshold( "Gelend", 0.8 ) -- A2ADispatcher:SetSquadronTanker( "Gelend", "TankerGelend" ) - -- + -- -- ## 7.4 Set up race track pattern - -- + -- -- By default, flights patrol randomly within the CAP zone. It is also possible to let them fly a race track pattern using the - -- @{#AI_A2A_DISPATCHER.SetDefaultCapRacetrack}(*LeglengthMin*, *LeglengthMax*, *HeadingMin*, *HeadingMax*, *DurationMin*, *DurationMax*) or + -- @{#AI_A2A_DISPATCHER.SetDefaultCapRacetrack}(*LeglengthMin*, *LeglengthMax*, *HeadingMin*, *HeadingMax*, *DurationMin*, *DurationMax*) or -- @{#AI_A2A_DISPATCHER.SetSquadronCapRacetrack}(*SquadronName*, *LeglengthMin*, *LeglengthMax*, *HeadingMin*, *HeadingMax*, *DurationMin*, *DurationMax*) functions. -- The first function enables this for all squadrons, the latter only for specific squadrons. For example, - -- + -- -- -- Enable race track pattern for CAP squadron "Mineralnye". -- A2ADispatcher:SetSquadronCapRacetrack("Mineralnye", 10000, 20000, 90, 180, 10*60, 20*60) - -- + -- -- In this case the squadron "Mineralnye" will a race track pattern at a random point in the CAP zone. The leg length will be randomly selected between 10,000 and 20,000 meters. The heading -- of the race track will randomly selected between 90 (West to East) and 180 (North to South) degrees. -- After a random duration between 10 and 20 minutes, the flight will get a new random orbit location. - -- - -- Note that all parameters except the squadron name are optional. If not specified, default values are taken. Speed and altitude are taken from the - -- + -- + -- Note that all parameters except the squadron name are optional. If not specified, default values are taken. Speed and altitude are taken from the + -- -- Also note that the center of the race track pattern is chosen randomly within the patrol zone and can be close the the boarder of the zone. Hence, it cannot be guaranteed that the -- whole pattern lies within the patrol zone. - -- + -- -- ## 8. Setup a squadron for GCI: - -- + -- -- The method @{#AI_A2A_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, + -- 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": - -- + -- -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) - -- + -- -- ## 9. Other configuration options - -- + -- -- ### 9.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\_A2A\_DISPATCHER. -- Use the method @{#AI_A2A_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. Defaults settings. - -- + -- -- This provides a good overview of the different parameters that are setup or hardcoded by default. -- For some default settings, a method is available that allows you to tweak the defaults. - -- + -- -- ## 10.1. Default takeoff method. - -- + -- -- The default **takeoff method** is set to **in the air**, which means that new spawned airplanes 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_A2A_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_A2A_DISPATCHER.SetDefaultTakeoffInAir}() will spawn by default new aircraft from the squadron directly in the air. -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromParkingCold}() will spawn by default new aircraft in without running engines at a parking spot at the airfield. -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromParkingHot}() will spawn by default new aircraft in with running engines at a parking spot at the airfield. -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromRunway}() will spawn by default new aircraft at the runway at the airfield. - -- + -- -- ## 10.2. Default landing method. - -- + -- -- The default **landing method** 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_A2A_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_A2A_DISPATCHER.SetDefaultLandingNearAirbase}() will despawn by default the returning aircraft in the air when near the airfield. -- * @{#AI_A2A_DISPATCHER.SetDefaultLandingAtRunway}() will despawn by default the returning aircraft directly after landing at the runway. -- * @{#AI_A2A_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 **1**. That essentially means that there isn't any overhead set by default. - -- + -- -- The default overhead value can be set for ALL squadrons that don't have an individual overhead value configured. -- -- Use the @{#AI_A2A_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_A2A_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_A2A_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_A2A_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage treshold** of spawned airplanes for all squadrons. - -- + -- -- ## 10.7. Default settings for CAP. - -- + -- -- ### 10.7.1. Default CAP Time Interval. - -- + -- -- CAP is time driven, and will evaluate in random time intervals if a new CAP needs to be spawned. -- The **default CAP time interval** is between **180** and **600** seconds. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons. -- Note that you can still change the CAP limit and CAP time intervals for each CAP individually using the @{#AI_A2A_DISPATCHER.SetSquadronCapTimeInterval}() method. - -- + -- -- ### 10.7.2. Default CAP limit. - -- + -- -- Multiple CAP can be airborne at the same time for one squadron, which is controlled by the **CAP limit**. -- The **default CAP limit** is 1 CAP per squadron to be airborne at the same time. -- Note that the default CAP limit is used when a Squadron CAP is defined, and cannot be changed afterwards. -- So, ensure that you set the default CAP limit **before** you spawn the Squadron CAP. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons. -- Note that you can still change the CAP limit and CAP time intervals for each CAP individually using the @{#AI_A2A_DISPATCHER.SetSquadronCapTimeInterval}() 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_A2A_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher. -- Use the method @{#AI_A2A_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_A2A_DISPATCHER\AI_A2A_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: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. + -- + -- 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_A2A_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_A2A_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. - -- + -- -- ## 12. Q & A: - -- + -- -- ### 12.1. Which countries will be selected for each coalition? - -- - -- Which countries are assigned to a coalition influences which units are available to the coalition. - -- For example because the mission calls for a EWR radar on the blue side the Ukraine might be chosen as a blue country - -- so that the 55G6 EWR radar unit is available to blue. - -- Some countries assign different tasking to aircraft, for example Germany assigns the CAP task to F-4E Phantoms but the USA does not. - -- Therefore if F4s are wanted as a coalition's CAP or GCI aircraft Germany will need to be assigned to that coalition. - -- + -- + -- Which countries are assigned to a coalition influences which units are available to the coalition. + -- For example because the mission calls for a EWR radar on the blue side the Ukraine might be chosen as a blue country + -- so that the 55G6 EWR radar unit is available to blue. + -- Some countries assign different tasking to aircraft, for example Germany assigns the CAP task to F-4E Phantoms but the USA does not. + -- Therefore if F4s are wanted as a coalition's CAP or GCI aircraft Germany will need to be assigned to that coalition. + -- -- ### 12.2. Country, type, load out, skill and skins for CAP and GCI aircraft? - -- + -- -- * Note these can be from any countries within the coalition but must be an aircraft with one of the main tasks being "CAP". -- * Obviously skins which are selected must be available to all players that join the mission otherwise they will see a default skin. - -- * Load outs should be appropriate to a CAP mission eg perhaps drop tanks for CAP flights and extra missiles for GCI flights. + -- * Load outs should be appropriate to a CAP mission eg perhaps drop tanks for CAP flights and extra missiles for GCI flights. -- * These decisions will eventually lead to template aircraft units being placed as late activation units that the script will use as templates for spawning CAP and GCI flights. Up to 4 different aircraft configurations can be chosen for each coalition. The spawned aircraft will inherit the characteristics of the template aircraft. - -- * The selected aircraft type must be able to perform the CAP tasking for the chosen country. - -- - -- + -- * The selected aircraft type must be able to perform the CAP tasking for the chosen country. + -- + -- -- @field #AI_A2A_DISPATCHER AI_A2A_DISPATCHER = { ClassName = "AI_A2A_DISPATCHER", @@ -882,10 +882,10 @@ do -- AI_A2A_DISPATCHER --- Enumerator for spawns at airbases -- @type AI_A2A_DISPATCHER.Takeoff -- @extends Wrapper.Group#GROUP.Takeoff - + --- @field #AI_A2A_DISPATCHER.Takeoff Takeoff AI_A2A_DISPATCHER.Takeoff = GROUP.Takeoff - + --- Defnes Landing location. -- @field Landing AI_A2A_DISPATCHER.Landing = { @@ -893,7 +893,7 @@ do -- AI_A2A_DISPATCHER AtRunway = 2, AtEngineShutdown = 3, } - + --- AI_A2A_DISPATCHER constructor. -- This is defining the A2A 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. @@ -902,33 +902,33 @@ do -- AI_A2A_DISPATCHER -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. -- @return #AI_A2A_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 A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- + -- function AI_A2A_DISPATCHER:New( Detection ) -- Inherits from DETECTION_MANAGER local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_A2A_DISPATCHER - + self.Detection = Detection -- Functional.Detection#DETECTION_AREAS - + -- 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.AIRPLANE, Unit.Category.HELICOPTER } ) --self.Detection:InitDetectRadar( true ) @@ -938,7 +938,7 @@ do -- AI_A2A_DISPATCHER self:SetGciRadius() self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. - + self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air ) self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.NearAirbase ) @@ -948,10 +948,10 @@ do -- AI_A2A_DISPATCHER 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:AddTransition( "Started", "Assign", "Started" ) - + --- OnAfter Transition Handler for Event Assign. -- @function [parent=#AI_A2A_DISPATCHER] OnAfterAssign -- @param #AI_A2A_DISPATCHER self @@ -961,7 +961,7 @@ do -- AI_A2A_DISPATCHER -- @param Tasking.Task_A2A#AI_A2A Task -- @param Wrapper.Unit#UNIT TaskUnit -- @param #string PlayerName - + self:AddTransition( "*", "CAP", "*" ) --- CAP Handler OnBefore for AI_A2A_DISPATCHER @@ -971,23 +971,23 @@ do -- AI_A2A_DISPATCHER -- @param #string Event -- @param #string To -- @return #boolean - + --- CAP Handler OnAfter for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] OnAfterCAP -- @param #AI_A2A_DISPATCHER self -- @param #string From -- @param #string Event -- @param #string To - + --- CAP Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] CAP -- @param #AI_A2A_DISPATCHER self - + --- CAP Asynchronous Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] __CAP -- @param #AI_A2A_DISPATCHER self -- @param #number Delay - + self:AddTransition( "*", "GCI", "*" ) --- GCI Handler OnBefore for AI_A2A_DISPATCHER @@ -997,7 +997,7 @@ do -- AI_A2A_DISPATCHER -- @param #string Event -- @param #string To -- @return #boolean - + --- GCI Handler OnAfter for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] OnAfterGCI -- @param #AI_A2A_DISPATCHER self @@ -1007,14 +1007,14 @@ do -- AI_A2A_DISPATCHER -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. -- @param #number DefendersMissing Number of missing defenders. -- @param #table DefenderFriendlies Friendly defenders. - + --- GCI Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] GCI -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. -- @param #number DefendersMissing Number of missing defenders. -- @param #table DefenderFriendlies Friendly defenders. - + --- GCI Asynchronous Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] __GCI -- @param #AI_A2A_DISPATCHER self @@ -1022,9 +1022,9 @@ do -- AI_A2A_DISPATCHER -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. -- @param #number DefendersMissing Number of missing defenders. -- @param #table DefenderFriendlies Friendly defenders. - + self:AddTransition( "*", "ENGAGE", "*" ) - + --- ENGAGE Handler OnBefore for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] OnBeforeENGAGE -- @param #AI_A2A_DISPATCHER self @@ -1034,7 +1034,7 @@ do -- AI_A2A_DISPATCHER -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. -- @param #table Defenders Defenders table. -- @return #boolean - + --- ENGAGE Handler OnAfter for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] OnAfterENGAGE -- @param #AI_A2A_DISPATCHER self @@ -1049,38 +1049,38 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. -- @param #table Defenders Defenders table. - + --- ENGAGE Asynchronous Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] __ENGAGE -- @param #AI_A2A_DISPATCHER self -- @param #number Delay -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. -- @param #table Defenders Defenders table. - - + + -- 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 intercept 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.DefenderCAPIndex = 0 - + self:__Start( 5 ) - + return self end @@ -1102,45 +1102,45 @@ do -- AI_A2A_DISPATCHER end end end - + --- Park defender. -- @param #AI_A2A_DISPATCHER self -- @param #AI_A2A_DISPATCHER.Squadron DefenderSquadron The squadron. 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 - + local Grouping=DefenderSquadron.Grouping or self.DefenderDefault.Grouping - + Grouping=1 - + Spawn:InitGrouping(Grouping) - + 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 - + self.uncontrolled=self.uncontrolled or {} self.uncontrolled[DefenderSquadron.Name]=self.uncontrolled[DefenderSquadron.Name] or {} - + table.insert(self.uncontrolled[DefenderSquadron.Name], {group=SpawnGroup, name=GroupName, grouping=Grouping}) end - + end @@ -1150,9 +1150,9 @@ do -- AI_A2A_DISPATCHER function AI_A2A_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 @@ -1167,7 +1167,7 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2A_DISPATCHER:OnEventCrashOrDead( EventData ) - self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) + self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) end --- Event land. @@ -1195,9 +1195,9 @@ do -- AI_A2A_DISPATCHER DefenderUnit:Destroy() return end - end + end end - + --- Event engine shutdown. -- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData @@ -1217,40 +1217,40 @@ do -- AI_A2A_DISPATCHER DefenderUnit:Destroy() self:ParkDefender( Squadron ) end - end + end end - + --- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an intercept mission. - -- If there is a target area detected and reported, then any friendlies that are airborne near this target area, + -- 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, + -- + -- 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 intercept 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_A2A_DISPATCHER.SetEngageRadius}() to modify the default Engage Radius for ALL squadrons.** - -- + -- -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2A%20-%20Engage%20Range%20Test) - -- + -- -- @param #AI_A2A_DISPATCHER self -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target. -- @return #AI_A2A_DISPATCHER -- @usage - -- + -- -- -- Set 50km as the radius to engage any target by airborne friendlies. -- A2ADispatcher:SetEngageRadius( 50000 ) - -- + -- -- -- Set 100km as the radius to engage any target by airborne friendlies. -- A2ADispatcher:SetEngageRadius() -- 100000 is the default value. - -- + -- function AI_A2A_DISPATCHER:SetEngageRadius( EngageRadius ) self.Detection:SetFriendliesRange( EngageRadius or 100000 ) - + return self end @@ -1259,57 +1259,57 @@ do -- AI_A2A_DISPATCHER -- @param #number DisengageRadius (Optional, Default = 300000) The radius in meters to disengage a target when too far from the home base. -- @return #AI_A2A_DISPATCHER -- @usage - -- + -- -- -- Set 50km as the Disengage Radius. -- A2ADispatcher:SetDisengageRadius( 50000 ) - -- + -- -- -- Set 100km as the Disengage Radius. -- A2ADispatcher:SetDisngageRadius() -- 300000 is the default value. - -- + -- function AI_A2A_DISPATCHER:SetDisengageRadius( DisengageRadius ) self.DisengageRadius = DisengageRadius or 300000 - + return self end - - + + --- Define the radius to check if a target can be engaged by an ground controlled intercept. -- When targets are detected that are still really far off, you don't want the AI_A2A_DISPATCHER to launch intercepts just yet. - -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** + -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** -- being **smaller** than the **Ground Controlled Intercept radius** or **Gci radius**. - -- - -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or, + -- + -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or, -- when you don't want to let the AI_A2A_DISPATCHER react immediately when a certain border or area is not being crossed. - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetGciRadius}() to set a specific controlled ground intercept radius. -- **The Ground Controlled Intercept radius is defined for ALL squadrons which are operational.** - -- + -- -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-013%20-%20AI_A2A%20-%20Intercept%20Test) - -- + -- -- @param #AI_A2A_DISPATCHER self -- @param #number GciRadius (Optional, Default = 200000) The radius to ground control intercept detected targets from the nearest airbase. -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Set 100km as the radius to ground control intercept detected targets from the nearest airbase. -- A2ADispatcher:SetGciRadius( 100000 ) - -- + -- -- -- Set 200km as the radius to ground control intercept. -- A2ADispatcher:SetGciRadius() -- 200000 is the default value. - -- + -- function AI_A2A_DISPATCHER:SetGciRadius( GciRadius ) - self.GciRadius = GciRadius or 200000 - + self.GciRadius = GciRadius or 200000 + return self end - - - + + + --- 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. @@ -1319,29 +1319,29 @@ do -- AI_A2A_DISPATCHER -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Set one ZONE_POLYGON object as the border for the A2A dispatcher. -- local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit. -- A2ADispatcher:SetBorderZone( BorderZone ) - -- + -- -- or - -- + -- -- -- Set two ZONE_POLYGON objects as the border for the A2A 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. -- A2ADispatcher:SetBorderZone( { BorderZone1, BorderZone2 } ) - -- - -- + -- + -- function AI_A2A_DISPATCHER:SetBorderZone( BorderZone ) self.Detection:SetAcceptZones( BorderZone ) return self end - + --- Display a tactical report every 30 seconds about which aircraft are: -- * Patrolling -- * Engaging @@ -1353,19 +1353,19 @@ do -- AI_A2A_DISPATCHER -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview. -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the Tactical Display for debug mode. -- A2ADispatcher:SetTacticalDisplay( true ) - -- + -- function AI_A2A_DISPATCHER:SetTacticalDisplay( TacticalDisplay ) - + self.TacticalDisplay = TacticalDisplay - + return self - end + end --- Set the default damage treshold when defenders will RTB. @@ -1374,19 +1374,19 @@ do -- AI_A2A_DISPATCHER -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the %-tage of the damage treshold before going RTB. -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the default damage treshold. -- A2ADispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. - -- + -- function AI_A2A_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) - + self.DefenderDefault.DamageThreshold = DamageThreshold - + return self - end + end --- Set the default CAP time interval for squadrons, which will be used to determine a random CAP timing. @@ -1396,18 +1396,18 @@ do -- AI_A2A_DISPATCHER -- @param #number CapMaxSeconds The maximum amount of seconds for the random time interval. -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the default CAP time interval. -- A2ADispatcher:SetDefaultCapTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. - -- + -- function AI_A2A_DISPATCHER:SetDefaultCapTimeInterval( CapMinSeconds, CapMaxSeconds ) - + self.DefenderDefault.CapMinSeconds = CapMinSeconds self.DefenderDefault.CapMaxSeconds = CapMaxSeconds - + return self end @@ -1418,33 +1418,33 @@ do -- AI_A2A_DISPATCHER -- @param #number CapLimit The maximum amount of CAP that can be airborne at the same time for the squadron. -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the default CAP limit. -- A2ADispatcher:SetDefaultCapLimit( 2 ) -- Maximum 2 CAP per squadron. - -- + -- function AI_A2A_DISPATCHER:SetDefaultCapLimit( CapLimit ) - + self.DefenderDefault.CapLimit = CapLimit - + return self - end + end --- Set intercept. -- @param #AI_A2A_DISPATCHER self -- @param #number InterceptDelay Delay in seconds before intercept. -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetIntercept( InterceptDelay ) - + self.DefenderDefault.InterceptDelay = InterceptDelay - + local Detection = self.Detection -- Functional.Detection#DETECTION_AREAS Detection:SetIntercept( true, InterceptDelay ) - + return self - end + end --- Calculates which AI friendlies are nearby the area @@ -1452,9 +1452,9 @@ do -- AI_A2A_DISPATCHER -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem -- @return #table A list of the friendlies nearby. function AI_A2A_DISPATCHER:GetAIFriendliesNearBy( DetectedItem ) - + local FriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) - + return FriendliesNearBy end @@ -1464,7 +1464,7 @@ do -- AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:GetDefenderTasks() return self.DefenderTasks or {} end - + --- Get defender task. -- @param #AI_A2A_DISPATCHER self -- @param Wrapper.Group#GROUP Defender The defender group. @@ -1480,15 +1480,15 @@ do -- AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:GetDefenderTaskFsm( Defender ) return self:GetDefenderTask( Defender ).Fsm end - + --- Get target of defender. -- @param #AI_A2A_DISPATCHER self -- @param Wrapper.Group#GROUP Defender The defender group. - -- @return Target + -- @return Target function AI_A2A_DISPATCHER:GetDefenderTaskTarget( Defender ) return self:GetDefenderTask( Defender ).Target end - + --- -- @param #AI_A2A_DISPATCHER self -- @param Wrapper.Group#GROUP Defender The defender group. @@ -1503,8 +1503,8 @@ do -- AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:ClearDefenderTask( Defender ) if Defender and Defender:IsAlive() and self.DefenderTasks[Defender] then local Target = self.DefenderTasks[Defender].Target - local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " - Message = Message .. Defender:GetName() + 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 @@ -1518,13 +1518,13 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param Wrapper.Group#GROUP Defender The defender group. function AI_A2A_DISPATCHER:ClearDefenderTaskTarget( Defender ) - + local DefenderTask = self:GetDefenderTask( Defender ) - + if Defender and Defender:IsAlive() and DefenderTask then local Target = DefenderTask.Target - local Message = "Clearing (" .. DefenderTask.Type .. ") " - Message = Message .. Defender:GetName() + local Message = "Clearing (" .. DefenderTask.Type .. ") " + Message = Message .. Defender:GetName() if Target then Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" end @@ -1534,8 +1534,8 @@ do -- AI_A2A_DISPATCHER DefenderTask.Target = nil end -- if Defender and DefenderTask then --- if DefenderTask.Fsm:Is( "Fuel" ) --- or DefenderTask.Fsm:Is( "LostControl") +-- if DefenderTask.Fsm:Is( "Fuel" ) +-- or DefenderTask.Fsm:Is( "LostControl") -- or DefenderTask.Fsm:Is( "Damaged" ) then -- self:ClearDefenderTask( Defender ) -- end @@ -1543,7 +1543,7 @@ do -- AI_A2A_DISPATCHER return self end - + --- Set defender task. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName Name of the squadron. @@ -1553,9 +1553,9 @@ do -- AI_A2A_DISPATCHER -- @param Functional.Detection#DETECTION_BASE.DetectedItem Target The defender detected item. -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target ) - + self:F( { SquadronName = SquadronName, Defender = Defender:GetName(), Type=Type, Target=Target } ) - + self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {} self.DefenderTasks[Defender].Type = Type self.DefenderTasks[Defender].Fsm = Fsm @@ -1566,17 +1566,17 @@ do -- AI_A2A_DISPATCHER end return self end - - + + --- Set defender task target. -- @param #AI_A2A_DISPATCHER self -- @param Wrapper.Group#GROUP Defender The defender group. -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection The detection object. -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection ) - - local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " - Message = Message .. Defender:GetName() + + 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 @@ -1586,90 +1586,90 @@ do -- AI_A2A_DISPATCHER end - --- This is the main method to define Squadrons programmatically. + --- 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\_A2A\_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_A2A_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. + -- + -- @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"`. + -- + -- @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"`. + -- + -- @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 A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- @usage - -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... + -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... -- A2ADispatcher: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 A2A 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. -- A2ADispatcher: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... -- A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) -- A2ADispatcher: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. -- A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) -- A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) - -- - -- + -- + -- -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) - - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} local DefenderSquadron = self.DefenderSquadrons[SquadronName] --#AI_A2A_DISPATCHER.Squadron - + 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 @@ -1688,25 +1688,25 @@ do -- AI_A2A_DISPATCHER self:SetSquadronLanguage( SquadronName, "EN" ) -- Squadrons speak English by default. self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) - + return self end - + --- Get an item from the Squadron table. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName Name of the squadron. -- @return #AI_A2A_DISPATCHER.Squadron Defender squadron table. function AI_A2A_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. @@ -1714,27 +1714,27 @@ do -- AI_A2A_DISPATCHER -- @param #string SquadronName The squadron name. -- @return #AI_A2A_DISPATCHER self -- @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 {} - + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_DISPATCHER.Squadron - + DefenderSquadron.Uncontrolled = true - + -- For now, grouping is forced to 1 due to other parts of the class which would not work well with grouping>1. DefenderSquadron.Grouping=1 - + -- Get free parking for fighter aircraft. local nfreeparking=DefenderSquadron.Airbase:GetFreeParkingSpotsNumber(AIRBASE.TerminalType.FighterAircraft, true) - + -- Take number of free parking spots if no resource count was specifed. DefenderSquadron.ResourceCount=DefenderSquadron.ResourceCount or nfreeparking - + -- Check that resource count is not larger than free parking spots. DefenderSquadron.ResourceCount=math.min(DefenderSquadron.ResourceCount, nfreeparking) @@ -1751,22 +1751,22 @@ do -- AI_A2A_DISPATCHER -- @param #string SquadronName The squadron name. -- @return #boolean 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 {} - + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_DISPATCHER.Squadron if DefenderSquadron then return DefenderSquadron.Uncontrolled == true end - + return nil - + end --- Set a CAP for a Squadron. @@ -1785,31 +1785,31 @@ do -- AI_A2A_DISPATCHER -- @param #number PatrolAltType The altitude type to patrol, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. -- @return #AI_A2A_DISPATCHER -- @usage - -- + -- -- -- CAP Squadron execution. -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) -- -- Setup a CAP, engaging between 800 and 900 km/h, altitude 30 (above the sea), radio altitude measurement, -- -- patrolling speed between 500 and 600 km/h, altitude between 4000 and 10000 meters, barometric altitude measurement. -- A2ADispatcher:SetSquadronCapV2( "Mineralnye", 800, 900, 30, 30, "RADIO", CAPZoneEast, 500, 600, 4000, 10000, "BARO" ) -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- + -- -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) -- -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 4000 and 10000 meters, radio altitude measurement, -- -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, barometric altitude measurement. -- A2ADispatcher:SetSquadronCapV2( "Sochi", 800, 1200, 2000, 3000, "RADIO", CAPZoneWest, 600, 800, 4000, 8000, "BARO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") -- -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 5000 and 8000 meters, barometric altitude measurement, -- -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, radio altitude. -- A2ADispatcher:SetSquadronCapV2( "Maykop", 800, 1200, 5000, 8000, "BARO", CAPZoneMiddle, 600, 800, 4000, 8000, "RADIO" ) -- A2ADispatcher:SetSquadronCapInterval( "Maykop", 2, 30, 120, 1 ) - -- + -- function AI_A2A_DISPATCHER:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType ) - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + 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 @@ -1829,18 +1829,18 @@ do -- AI_A2A_DISPATCHER self:SetSquadronCapInterval( SquadronName, self.DefenderDefault.CapLimit, self.DefenderDefault.CapMinSeconds, self.DefenderDefault.CapMaxSeconds, 1 ) self:I( { CAP = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageAltType } } ) - + -- Add the CAP to the EWR network. - + local RecceSet = self.Detection:GetDetectionSet() RecceSet:FilterPrefixes( DefenderSquadron.TemplatePrefixes ) RecceSet:FilterStart() - + self.Detection:SetFriendlyPrefixes( DefenderSquadron.TemplatePrefixes ) - + return self end - + --- Set a CAP for a Squadron. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. @@ -1854,26 +1854,26 @@ do -- AI_A2A_DISPATCHER -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. -- @return #AI_A2A_DISPATCHER -- @usage - -- + -- -- -- CAP Squadron execution. -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- + -- -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- function AI_A2A_DISPATCHER:SetSquadronCap( SquadronName, Zone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) - return self:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType ) + return self:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType ) end - - --- Set the squadron CAP parameters. + + --- Set the squadron CAP parameters. -- @param #AI_A2A_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. @@ -1882,23 +1882,23 @@ do -- AI_A2A_DISPATCHER -- @param #number Probability Is not in use, you can skip this parameter. -- @return #AI_A2A_DISPATCHER -- @usage - -- + -- -- -- CAP Squadron execution. -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- + -- -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- function AI_A2A_DISPATCHER:SetSquadronCapInterval( SquadronName, CapLimit, LowInterval, HighInterval, Probability ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -1909,32 +1909,32 @@ do -- AI_A2A_DISPATCHER Cap.HighInterval = HighInterval or 600 Cap.Probability = Probability or 1 Cap.CapLimit = CapLimit or 1 - Cap.Scheduler = Cap.Scheduler or SCHEDULER:New( self ) + 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 Randomization = Variance / Repeat local Start = math.random( 1, Cap.HighInterval ) - + if ScheduleID then Scheduler:Stop( ScheduleID ) end - + Cap.ScheduleID = Scheduler:Schedule( self, self.SchedulerCAP, { SquadronName }, Start, Repeat, Randomization ) else error( "This squadron does not exist:" .. SquadronName ) end end - + --- -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:GetCAPDelay( SquadronName ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -1953,16 +1953,16 @@ do -- AI_A2A_DISPATCHER -- @return #AI_A2A_DISPATCHER.Squadron DefenderSquadron function AI_A2A_DISPATCHER:CanCAP( SquadronName ) self:F({SquadronName = SquadronName}) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + 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 ( 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 ) @@ -1988,10 +1988,10 @@ do -- AI_A2A_DISPATCHER -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. counter clockwise from North to South. -- @param #number DurationMin (Optional) Min duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. - -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. + -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultCapRacetrack(LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) - + self.DefenderDefault.Racetrack=true self.DefenderDefault.RacetrackLengthMin=LeglengthMin self.DefenderDefault.RacetrackLengthMax=LeglengthMax @@ -2003,7 +2003,7 @@ do -- AI_A2A_DISPATCHER return self end - + --- Set race track pattern when squadron is performing CAP. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName Name of the squadron. @@ -2013,12 +2013,12 @@ do -- AI_A2A_DISPATCHER -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from North to South. -- @param #number DurationMin (Optional) Min duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. - -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. + -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronCapRacetrack(SquadronName, LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) - + local DefenderSquadron = self:GetSquadron( SquadronName ) - + if DefenderSquadron then DefenderSquadron.Racetrack=true DefenderSquadron.RacetrackLengthMin=LeglengthMin @@ -2029,9 +2029,9 @@ do -- AI_A2A_DISPATCHER DefenderSquadron.RacetrackDurationMax=DurationMax DefenderSquadron.RacetrackCoordinates=CapCoordinates end - + return self - end + end --- Check if squadron can do GCI. @@ -2040,14 +2040,14 @@ do -- AI_A2A_DISPATCHER -- @return #table DefenderSquadron function AI_A2A_DISPATCHER:CanGCI( SquadronName ) self:F({SquadronName = SquadronName}) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci 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 ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. local Gci = DefenderSquadron.Gci if Gci then @@ -2066,19 +2066,19 @@ do -- AI_A2A_DISPATCHER -- @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 DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". - -- @usage - -- + -- @usage + -- -- -- GCI Squadron execution. -- A2ADispatcher:SetSquadronGci2( "Mozdok", 900, 1200, 5000, 5000, "BARO" ) -- A2ADispatcher:SetSquadronGci2( "Novo", 900, 2100, 30, 30, "RADIO" ) -- A2ADispatcher:SetSquadronGci2( "Maykop", 900, 1200, 100, 300, "RADIO" ) - -- + -- -- @return #AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:SetSquadronGci2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {} - + local Intercept = self.DefenderSquadrons[SquadronName].Gci Intercept.Name = SquadronName Intercept.EngageMinSpeed = EngageMinSpeed @@ -2086,70 +2086,70 @@ do -- AI_A2A_DISPATCHER Intercept.EngageFloorAltitude = EngageFloorAltitude Intercept.EngageCeilingAltitude = EngageCeilingAltitude Intercept.EngageAltType = EngageAltType - + self:I( { GCI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) end - + --- Set squadron GCI. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. -- @param #number EngageMinSpeed The minimum speed [km/h] at which the GCI can be executed. -- @param #number EngageMaxSpeed The maximum speed [km/h] at which the GCI can be executed. - -- @usage - -- + -- @usage + -- -- -- GCI Squadron execution. -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) -- A2ADispatcher:SetSquadronGci( "Novo", 900, 2100 ) -- A2ADispatcher:SetSquadronGci( "Maykop", 900, 1200 ) - -- + -- -- @return #AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:SetSquadronGci( SquadronName, EngageMinSpeed, EngageMaxSpeed ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {} - + local Intercept = self.DefenderSquadrons[SquadronName].Gci Intercept.Name = SquadronName Intercept.EngageMinSpeed = EngageMinSpeed Intercept.EngageMaxSpeed = EngageMaxSpeed - + self:F( { GCI = { SquadronName, EngageMinSpeed, EngageMaxSpeed } } ) end - + --- Defines the default amount of extra planes that will take-off as part of the defense system. -- @param #AI_A2A_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_A2A_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 A2A 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: - -- + -- 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 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 A2ADispatcher = AI_A2A_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. - -- + -- -- A2ADispatcher:SetDefaultOverhead( 1.5 ) - -- + -- -- @return #AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:SetDefaultOverhead( Overhead ) self.DefenderDefault.Overhead = Overhead - + return self end @@ -2161,35 +2161,35 @@ do -- AI_A2A_DISPATCHER -- The default overhead is 1, so equal balance. The @{#AI_A2A_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 A2A 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: - -- + -- 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 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 A2ADispatcher = AI_A2A_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. - -- + -- -- A2ADispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) - -- + -- -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.Overhead = Overhead - + return self end @@ -2197,20 +2197,20 @@ do -- AI_A2A_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_A2A_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 CAP or GCI defenders. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Set a grouping by default per 2 airplanes. -- A2ADispatcher:SetDefaultGrouping( 2 ) - -- - -- + -- + -- -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultGrouping( Grouping ) - + self.DefenderDefault.Grouping = Grouping - + return self end @@ -2219,21 +2219,21 @@ do -- AI_A2A_DISPATCHER -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. -- @param #AI_A2A_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 CAP or GCI defenders. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Set a grouping per 2 airplanes. -- A2ADispatcher:SetSquadronGrouping( "SquadronName", 2 ) - -- - -- + -- + -- -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) - + local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.Grouping = Grouping - + return self end @@ -2242,28 +2242,28 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default take-off in the air. -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Air ) - -- + -- -- -- Let new flights by default take-off from the runway. -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Runway ) - -- + -- -- -- Let new flights by default take-off from the airbase hot. -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Hot ) - -- + -- -- -- Let new flights by default take-off from the airbase cold. -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Cold ) - -- - -- + -- + -- -- @return #AI_A2A_DISPATCHER self - -- + -- function AI_A2A_DISPATCHER:SetDefaultTakeoff( Takeoff ) self.DefenderDefault.Takeoff = Takeoff - + return self end @@ -2272,112 +2272,112 @@ do -- AI_A2A_DISPATCHER -- @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 A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off in the air. -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Air ) - -- + -- -- -- Let new flights take-off from the runway. -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Runway ) - -- + -- -- -- Let new flights take-off from the airbase hot. -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Hot ) - -- + -- -- -- Let new flights take-off from the airbase cold. -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Cold ) - -- - -- + -- + -- -- @return #AI_A2A_DISPATCHER self - -- + -- function AI_A2A_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_A2A_DISPATCHER self -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default take-off in the air. -- local TakeoffMethod = A2ADispatcher:GetDefaultTakeoff() -- if TakeOffMethod == , AI_A2A_Dispatcher.Takeoff.InAir then -- ... -- end - -- + -- function AI_A2A_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_A2A_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 A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off in the air. -- local TakeoffMethod = A2ADispatcher:GetSquadronTakeoff( "SquadronName" ) -- if TakeOffMethod == , AI_A2A_Dispatcher.Takeoff.InAir then -- ... -- end - -- + -- function AI_A2A_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_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default take-off in the air. -- A2ADispatcher:SetDefaultTakeoffInAir() - -- + -- -- @return #AI_A2A_DISPATCHER self - -- + -- function AI_A2A_DISPATCHER:SetDefaultTakeoffInAir() self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air ) - + return self end - + --- Sets flights to take-off in the air, as part of the defense system. -- @param #AI_A2A_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 A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off in the air. -- A2ADispatcher:SetSquadronTakeoffInAir( "SquadronName" ) - -- + -- -- @return #AI_A2A_DISPATCHER self - -- + -- function AI_A2A_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Air ) - + if TakeoffAltitude then self:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) end - + return self end @@ -2385,57 +2385,57 @@ do -- AI_A2A_DISPATCHER --- Sets flights by default to take-off from the runway, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default take-off from the runway. -- A2ADispatcher:SetDefaultTakeoffFromRunway() - -- + -- -- @return #AI_A2A_DISPATCHER self - -- + -- function AI_A2A_DISPATCHER:SetDefaultTakeoffFromRunway() self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Runway ) - + return self end - + --- Sets flights to take-off from the runway, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off from the runway. -- A2ADispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) - -- + -- -- @return #AI_A2A_DISPATCHER self - -- + -- function AI_A2A_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) self:SetSquadronTakeoff( SquadronName, AI_A2A_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_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default take-off at a hot parking spot. -- A2ADispatcher:SetDefaultTakeoffFromParkingHot() - -- + -- -- @return #AI_A2A_DISPATCHER self - -- + -- function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingHot() self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Hot ) - + return self end @@ -2443,77 +2443,77 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off in the air. -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) - -- + -- -- @return #AI_A2A_DISPATCHER self - -- + -- function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) self:SetSquadronTakeoff( SquadronName, AI_A2A_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_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off from a cold parking spot. -- A2ADispatcher:SetDefaultTakeoffFromParkingCold() - -- + -- -- @return #AI_A2A_DISPATCHER self - -- + -- function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingCold() self:SetDefaultTakeoff( AI_A2A_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_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off from a cold parking spot. -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) - -- + -- -- @return #AI_A2A_DISPATCHER self - -- + -- function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) self:SetSquadronTakeoff( SquadronName, AI_A2A_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_A2A_DISPATCHER self -- @param #number TakeoffAltitude The altitude in meters above the ground. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Set the default takeoff altitude when taking off in the air. -- A2ADispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. - -- + -- -- @return #AI_A2A_DISPATCHER self - -- + -- function AI_A2A_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude ) self.DefenderDefault.TakeoffAltitude = TakeoffAltitude - + return self end @@ -2522,244 +2522,244 @@ do -- AI_A2A_DISPATCHER -- @param #string SquadronName The name of the squadron. -- @param #number TakeoffAltitude The altitude in meters above the ground. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Set the default takeoff altitude when taking off in the air. -- A2ADispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. - -- + -- -- @return #AI_A2A_DISPATCHER self - -- + -- function AI_A2A_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_A2A_DISPATCHER self -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default despawn near the airbase when returning. -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.NearAirbase ) - -- + -- -- -- Let new flights by default despawn after landing land at the runway. -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.AtRunway ) - -- + -- -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.AtEngineShutdown ) - -- + -- -- @return #AI_A2A_DISPATCHER self function AI_A2A_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_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights despawn near the airbase when returning. -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.NearAirbase ) - -- + -- -- -- Let new flights despawn after landing land at the runway. -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.AtRunway ) - -- + -- -- -- Let new flights despawn after landing and parking, and after engine shutdown. -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.AtEngineShutdown ) - -- + -- -- @return #AI_A2A_DISPATCHER self function AI_A2A_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_A2A_DISPATCHER self -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default despawn near the airbase when returning. -- local LandingMethod = A2ADispatcher:GetDefaultLanding( AI_A2A_Dispatcher.Landing.NearAirbase ) -- if LandingMethod == AI_A2A_Dispatcher.Landing.NearAirbase then -- ... -- end - -- + -- function AI_A2A_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_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights despawn near the airbase when returning. -- local LandingMethod = A2ADispatcher:GetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.NearAirbase ) -- if LandingMethod == AI_A2A_Dispatcher.Landing.NearAirbase then -- ... -- end - -- + -- function AI_A2A_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_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let flights by default to land near the airbase and despawn. -- A2ADispatcher:SetDefaultLandingNearAirbase() - -- + -- -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLandingNearAirbase() self:SetDefaultLanding( AI_A2A_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_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let flights to land near the airbase and despawn. -- A2ADispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) - -- + -- -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) self:SetSquadronLanding( SquadronName, AI_A2A_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_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let flights by default land at the runway and despawn. -- A2ADispatcher:SetDefaultLandingAtRunway() - -- + -- -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLandingAtRunway() self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtRunway ) - + return self end - + --- Sets flights to land and despawn at the runway, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let flights land at the runway and despawn. -- A2ADispatcher:SetSquadronLandingAtRunway( "SquadronName" ) - -- + -- -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) self:SetSquadronLanding( SquadronName, AI_A2A_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_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let flights by default land and despawn at engine shutdown. -- A2ADispatcher:SetDefaultLandingAtEngineShutdown() - -- + -- -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLandingAtEngineShutdown() self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtEngineShutdown ) - + return self end - + --- Sets flights to land and despawn at engine shutdown, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let flights land and despawn at engine shutdown. -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) - -- + -- -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) self:SetSquadronLanding( SquadronName, AI_A2A_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_A2A_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_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the default fuel treshold. -- A2ADispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- + -- function AI_A2A_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) - + self.DefenderDefault.FuelThreshold = FuelThreshold - + return self - end + end --- Set the fuel treshold for the squadron when defenders will RTB or Refuel in the air. @@ -2769,41 +2769,41 @@ do -- AI_A2A_DISPATCHER -- @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_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the default fuel treshold. -- A2ADispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- + -- function AI_A2A_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) - + local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.FuelThreshold = FuelThreshold - + return self - end + end --- Set the default tanker where defenders will Refuel in the air. -- @param #AI_A2A_DISPATCHER self -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the default fuel treshold. -- A2ADispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- + -- -- -- Now Setup the default tanker. -- A2ADispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. function AI_A2A_DISPATCHER:SetDefaultTanker( TankerName ) - + self.DefenderDefault.TankerName = TankerName - + return self - end + end --- Set the squadron tanker where defenders will Refuel in the air. @@ -2812,22 +2812,22 @@ do -- AI_A2A_DISPATCHER -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. -- @return #AI_A2A_DISPATCHER -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the squadron fuel treshold. -- A2ADispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- + -- -- -- Now Setup the squadron tanker. -- A2ADispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. function AI_A2A_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) - + local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.TankerName = TankerName - + return self - end + end --- Set the squadron language. @@ -2836,26 +2836,26 @@ do -- AI_A2A_DISPATCHER -- @param #string Language A string defining the language to be embedded within the miz file. -- @return #AI_A2A_DISPATCHER -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Set for English. -- A2ADispatcher:SetSquadronLanguage( "SquadronName", "EN" ) -- This squadron speaks English. - -- + -- -- -- Set for Russian. -- A2ADispatcher:SetSquadronLanguage( "SquadronName", "RU" ) -- This squadron speaks Russian. function AI_A2A_DISPATCHER:SetSquadronLanguage( SquadronName, Language ) - + local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.Language = Language - + if DefenderSquadron.RadioQueue then DefenderSquadron.RadioQueue:SetLanguage( Language ) end - + return self - end + end --- Set the frequency of communication and the mode of communication for voice overs. @@ -2865,10 +2865,10 @@ do -- AI_A2A_DISPATCHER -- @param #number RadioModulation The modulation of communication. -- @param #number RadioPower The power in Watts of communication. function AI_A2A_DISPATCHER:SetSquadronRadioFrequency( SquadronName, RadioFrequency, RadioModulation, RadioPower ) - + local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.RadioFrequency = RadioFrequency - DefenderSquadron.RadioModulation = RadioModulation or radio.modulation.AM + DefenderSquadron.RadioModulation = RadioModulation or radio.modulation.AM DefenderSquadron.RadioPower = RadioPower or 100 if DefenderSquadron.RadioQueue then @@ -2880,9 +2880,9 @@ do -- AI_A2A_DISPATCHER DefenderSquadron.RadioQueue = RADIOSPEECH:New( DefenderSquadron.RadioFrequency, DefenderSquadron.RadioModulation ) DefenderSquadron.RadioQueue.power = DefenderSquadron.RadioPower DefenderSquadron.RadioQueue:Start( 0.5 ) - + DefenderSquadron.RadioQueue:SetLanguage( DefenderSquadron.Language ) - end + end --- Add defender to squadron. Resource count will get smaller. -- @param #AI_A2A_DISPATCHER self @@ -2921,17 +2921,17 @@ do -- AI_A2A_DISPATCHER self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() self:F( { DefenderName = DefenderName } ) - return self.Defenders[ DefenderName ] + return self.Defenders[ DefenderName ] end - + --- Creates an SWEEP task when there are targets for it. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. function AI_A2A_DISPATCHER:EvaluateSWEEP( DetectedItem ) self:F( { DetectedItem.ItemID } ) - + local DetectedSet = DetectedItem.Set local DetectedZone = DetectedItem.Zone @@ -2942,10 +2942,10 @@ do -- AI_A2A_DISPATCHER 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 @@ -2956,7 +2956,7 @@ do -- AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:CountCapAirborne( SquadronName ) local CapCount = 0 - + local DefenderSquadron = self.DefenderSquadrons[SquadronName] if DefenderSquadron then for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do @@ -2978,8 +2978,8 @@ do -- AI_A2A_DISPATCHER return CapCount end - - + + --- Count number of engaging defender groups. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detection object. @@ -2988,21 +2988,21 @@ do -- AI_A2A_DISPATCHER -- First, count the active AIGroups Units, targetting the DetectedSet local DefenderCount = 0 - + local DetectedSet = AttackerDetection.Set --DetectedSet:Flush() - + local DefenderTasks = self:GetDefenderTasks() - + for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do local Defender = DefenderGroup -- Wrapper.Group#GROUP local DefenderTaskTarget = DefenderTask.Target --Functional.Detection#DETECTION_BASE.DetectedItem 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 @@ -3017,24 +3017,24 @@ do -- AI_A2A_DISPATCHER return DefenderCount end - + --- Count defenders to be engaged if number of attackers larger than number of defenders. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. -- @param #number DefenderCount Number of defenders. -- @return #table Table of friendly groups. function AI_A2A_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 + 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. @@ -3053,7 +3053,7 @@ do -- AI_A2A_DISPATCHER end end end - end + end end else break @@ -3071,40 +3071,40 @@ do -- AI_A2A_DISPATCHER -- @return Wrapper.Group#GROUP The defender group. -- @return #boolean Grouping. 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 - + --env.info(string.format("FF resource activate: Squadron=%s grouping=%d needed=%d visible=%s", SquadronName, DefenderGrouping, DefendersNeeded, tostring(self:IsSquadronVisible( SquadronName )))) - + if self:IsSquadronVisible( SquadronName ) then - + local n=#self.uncontrolled[SquadronName] - + if n>0 then -- Random number 1,...n local id=math.random(n) - + -- Pick a random defender group. local Defender=self.uncontrolled[SquadronName][id].group --Wrapper.Group#GROUP - + -- Start uncontrolled group. Defender:StartUncontrolled() - + -- Get grouping. DefenderGrouping=self.uncontrolled[SquadronName][id].grouping - + -- Add defender to squadron. self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) - + -- Remove defender from uncontrolled table. table.remove(self.uncontrolled[SquadronName], id) - + return Defender, DefenderGrouping else return nil,0 @@ -3117,7 +3117,7 @@ do -- AI_A2A_DISPATCHER --[[ -- 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 @@ -3141,9 +3141,9 @@ do -- AI_A2A_DISPATCHER if DefenderUnitIndex > DefenderGrouping then break end - - end - + + end + if DefenderCAPTemplate then local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) local SpawnGroup = GROUP:Register( DefenderName ) @@ -3153,38 +3153,38 @@ do -- AI_A2A_DISPATCHER 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 - + ---------------------------- --- Squadron not visible --- ---------------------------- - + 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 - + --- On after "CAP" event. -- @param #AI_A2A_DISPATCHER self -- @param #string From From state. @@ -3192,23 +3192,23 @@ do -- AI_A2A_DISPATCHER -- @param #string To To state. -- @param #string SquadronName Name of the squadron. function AI_A2A_DISPATCHER:onafterCAP( From, Event, To, SquadronName ) - + self:F({SquadronName = SquadronName}) - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} - + local DefenderSquadron = self:CanCAP( SquadronName ) - + if DefenderSquadron then - + local Cap = DefenderSquadron.Cap - + if Cap then - local DefenderCAP, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) - + local DefenderCAP, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) + if DefenderCAP then - + local AI_A2A_Fsm = AI_A2A_CAP:New2( DefenderCAP, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.EngageFloorAltitude, Cap.EngageCeilingAltitude, Cap.EngageAltType, Cap.Zone, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.PatrolFloorAltitude, Cap.PatrolCeilingAltitude, Cap.PatrolAltType ) AI_A2A_Fsm:SetDispatcher( self ) AI_A2A_Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) @@ -3226,19 +3226,19 @@ do -- AI_A2A_DISPATCHER DefenderSquadron.RacetrackCoordinates or self.DefenderDefault.RacetrackCoordinates) end AI_A2A_Fsm:Start() - + self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", AI_A2A_Fsm ) function AI_A2A_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) - -- Issue GetCallsign() returns nil, see https://github.com/FlightControl-Master/MOOSE/issues/1228 + -- Issue GetCallsign() returns nil, see https://github.com/FlightControl-Master/MOOSE/issues/1228 if DefenderGroup and DefenderGroup:IsAlive() then self:F({"CAP Takeoff", DefenderGroup:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = AI_A2A_Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - + if Squadron then Dispatcher:MessageToPlayers( Squadron, DefenderName .. " Wheels up.", DefenderGroup ) AI_A2A_Fsm:__Patrol( 2 ) -- Start Patrolling @@ -3250,14 +3250,14 @@ do -- AI_A2A_DISPATCHER if DefenderGroup and DefenderGroup:IsAlive() then self:F({"CAP PatrolRoute", DefenderGroup:GetName()}) self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To ) - + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup ) end - + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end end @@ -3265,9 +3265,9 @@ do -- AI_A2A_DISPATCHER function AI_A2A_Fsm:onafterRTB( DefenderGroup, From, Event, To ) if DefenderGroup and DefenderGroup:IsAlive() then self:F({"CAP RTB", DefenderGroup:GetName()}) - + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) - + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) @@ -3277,21 +3277,21 @@ do -- AI_A2A_DISPATCHER Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end end - + --- @param #AI_A2A_DISPATCHER self function AI_A2A_Fsm:onafterHome( Defender, From, Event, To, Action ) if Defender and Defender:IsAlive() then self:F({"CAP Home", Defender:GetName()}) self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - + local Dispatcher = self:GetDispatcher() -- #AI_A2A_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_A2A_DISPATCHER.Landing.NearAirbase then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) Defender:Destroy() @@ -3302,7 +3302,7 @@ do -- AI_A2A_DISPATCHER end end end - + end @@ -3315,7 +3315,7 @@ do -- AI_A2A_DISPATCHER -- @param #table Defenders Defenders table. function AI_A2A_DISPATCHER:onafterENGAGE( From, Event, To, AttackerDetection, Defenders ) self:F("ENGAGING Detection ID="..tostring(AttackerDetection.ID)) - + if Defenders then for DefenderID, Defender in pairs( Defenders ) do @@ -3327,7 +3327,7 @@ do -- AI_A2A_DISPATCHER self:SetDefenderTaskTarget( Defender, AttackerDetection ) end - + end end @@ -3340,38 +3340,38 @@ do -- AI_A2A_DISPATCHER -- @param #number DefendersMissing Number of missing defenders. -- @param #table DefenderFriendlies Friendly defenders. function AI_A2A_DISPATCHER:onafterGCI( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies ) - + self:F("GCI Detection ID="..tostring(AttackerDetection.ID)) self:F( { From, Event, To, AttackerDetection.Index, DefendersMissing, 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 Fsm = self:GetDefenderTaskFsm( DefenderGroup ) Fsm:__EngageRoute( 0.1, AttackerSet ) -- Engage on the TargetSetUnit - + self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) - + DefenderCount = DefenderCount + DefenderGroup:GetSize() 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 @@ -3379,7 +3379,7 @@ do -- AI_A2A_DISPATCHER self:F( { GCI = DefenderSquadron.Gci } ) for InterceptID, Intercept in pairs( DefenderSquadron.Gci or {} ) do - + self:F( { DefenderSquadron } ) local SpawnCoord = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE local AttackerCoord = AttackerUnit:GetCoordinate() @@ -3389,9 +3389,9 @@ do -- AI_A2A_DISPATCHER 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.GciRadius then ClosestDistance = InterceptDistance @@ -3401,42 +3401,42 @@ do -- AI_A2A_DISPATCHER end end end - + if ClosestDefenderSquadronName then - + local DefenderSquadron = self:CanGCI( ClosestDefenderSquadronName ) - + if DefenderSquadron then - + local Gci = self.DefenderSquadrons[ClosestDefenderSquadronName].Gci - + if Gci 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 } ) - + -- 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 DefenderGCI, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) - + + local DefenderGCI, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + DefendersNeeded = DefendersNeeded - DefenderGrouping - + if DefenderGCI then - + DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead - + local Fsm = AI_A2A_GCI:New2( DefenderGCI, Gci.EngageMinSpeed, Gci.EngageMaxSpeed, Gci.EngageFloorAltitude, Gci.EngageCeilingAltitude, Gci.EngageAltType ) Fsm:SetDispatcher( self ) Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) @@ -3444,20 +3444,20 @@ do -- AI_A2A_DISPATCHER Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) Fsm:SetDisengageRadius( self.DisengageRadius ) Fsm:Start() - - + + self:SetDefenderTask( ClosestDefenderSquadronName, DefenderGCI, "GCI", Fsm, AttackerDetection ) - - + + function Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) self:F({"GCI Birth", DefenderGroup:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup ) - + if DefenderTarget then if Squadron.Language == "EN" then Dispatcher:MessageToPlayers( Squadron, DefenderName .. " wheels up.", DefenderGroup ) @@ -3468,18 +3468,18 @@ do -- AI_A2A_DISPATCHER Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end - + function Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) self:F({"GCI Route", DefenderGroup:GetName()}) - + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - + if Squadron and AttackSetUnit:Count() > 0 then local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - + if Squadron.Language == "EN" then Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", intercepting bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) elseif Squadron.Language == "RU" then @@ -3490,18 +3490,18 @@ do -- AI_A2A_DISPATCHER end self:GetParent( Fsm ).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) end - + function Fsm:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) self:F({"GCI Engage", DefenderGroup:GetName()}) - + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - + if Squadron and AttackSetUnit:Count() > 0 then local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - + if Squadron.Language == "EN" then Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) elseif Squadron.Language == "RU" then @@ -3510,11 +3510,11 @@ do -- AI_A2A_DISPATCHER end self:GetParent( Fsm ).onafterEngage( self, DefenderGroup, From, Event, To, AttackSetUnit ) end - + function Fsm:onafterRTB( DefenderGroup, From, Event, To ) self:F({"GCI RTB", DefenderGroup:GetName()}) self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) - + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) @@ -3528,12 +3528,12 @@ do -- AI_A2A_DISPATCHER end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end - + --- @param #AI_A2A_DISPATCHER self function Fsm:onafterLostControl( Defender, From, Event, To ) self:F({"GCI LostControl", Defender:GetName()}) self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) if Defender:IsAboveRunway() then @@ -3541,12 +3541,12 @@ do -- AI_A2A_DISPATCHER Defender:Destroy() end end - + --- @param #AI_A2A_DISPATCHER self function Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) self:F({"GCI Home", DefenderGroup:GetName()}) self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) - + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) @@ -3556,19 +3556,19 @@ do -- AI_A2A_DISPATCHER elseif Squadron.Language == "RU" then Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие Ñамолеты в поÑадка на базу.", DefenderGroup ) end - + if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) DefenderGroup:Destroy() end - + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) DefenderGroup:Destroy() Dispatcher:ParkDefender( Squadron ) end end - + end -- if DefenderGCI then end -- while ( DefendersNeeded > 0 ) do end @@ -3594,25 +3594,25 @@ do -- AI_A2A_DISPATCHER -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units or nil. function AI_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem ) self:F( { DetectedItem.ItemID } ) - + -- First, count the active AIGroups Units, targetting the DetectedSet local DefenderCount = self:CountDefendersEngaged( DetectedItem ) local DefenderGroups = self:CountDefendersToBeEngaged( DetectedItem, DefenderCount ) self:F( { DefenderCount = DefenderCount } ) - + -- Only allow ENGAGE when: -- 1. There are friendly units near the detected attackers. -- 2. There is sufficient fuel -- 3. There is sufficient ammo -- 4. The plane is not damaged - if DefenderGroups and DetectedItem.IsDetected == true then + if DefenderGroups and DetectedItem.IsDetected == true then return DefenderGroups end - + return nil end - + --- Creates an GCI task when there are targets for it. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. @@ -3620,7 +3620,7 @@ do -- AI_A2A_DISPATCHER -- @return #table Table of friendly groups. function AI_A2A_DISPATCHER:EvaluateGCI( DetectedItem ) self:F( { DetectedItem.ItemID } ) - + local AttackerSet = DetectedItem.Set local AttackerCount = AttackerSet:Count() @@ -3632,10 +3632,10 @@ do -- AI_A2A_DISPATCHER local Friendlies = self:CountDefendersToBeEngaged( DetectedItem, DefenderCount ) if DetectedItem.IsDetected == true then - + return DefendersMissing, Friendlies end - + return nil, nil end @@ -3643,36 +3643,36 @@ do -- AI_A2A_DISPATCHER --- Assigns A2G AI Tasks in relation to the detected items. -- @param #AI_A2G_DISPATCHER self function AI_A2A_DISPATCHER:Order( DetectedItem ) - + local detection=self.Detection -- Functional.Detection#DETECTION_AREAS - + local ShortestDistance = 999999999 - + -- Get coordinate (or nil). local AttackCoordinate = detection:GetDetectedItemCoordinate( DetectedItem ) - + -- Issue https://github.com/FlightControl-Master/MOOSE/issues/1232 if AttackCoordinate then - + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do - + self:T( { DefenderSquadron = DefenderSquadron.Name } ) - + local Airbase = DefenderSquadron.Airbase local AirbaseCoordinate = Airbase:GetCoordinate() - + local EvaluateDistance = AttackCoordinate:Get2DDistance( AirbaseCoordinate ) - + if EvaluateDistance <= ShortestDistance then ShortestDistance = EvaluateDistance end end - + end - + return ShortestDistance end - + --- Shows the tactical display. -- @param #AI_A2A_DISPATCHER self @@ -3682,17 +3682,17 @@ do -- AI_A2A_DISPATCHER local AreaMsg = {} local TaskMsg = {} local ChangeMsg = {} - + local TaskReport = REPORT:New() - + local Report = REPORT:New( "Tactical Overview:" ) local DefenderGroupCount = 0 -- Now that all obsolete tasks are removed, loop through the detected targets. --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do - + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT local DetectedCount = DetectedSet:Count() @@ -3704,7 +3704,7 @@ do -- AI_A2A_DISPATCHER local DetectedID = DetectedItem.ID local DetectionIndex = DetectedItem.Index local DetectedItemChanged = DetectedItem.Changed - + -- 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 @@ -3714,15 +3714,15 @@ do -- AI_A2A_DISPATCHER DefenderGroupCount = DefenderGroupCount + 1 local Fuel = Defender:GetFuelMin() * 100 local Damage = Defender:GetLife() / Defender:GetLife0() * 100 - Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", + Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", Defender:GetName(), Defender:GetSize(), Defender:GetInitialSize(), - DefenderTask.Type, - DefenderTask.Fsm:GetState(), - Defender:GetSize(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), Fuel, - Damage, + Damage, Defender:HasTask() == true and "Executing" or "Idle" ) ) end end @@ -3740,15 +3740,15 @@ do -- AI_A2A_DISPATCHER local Fuel = Defender:GetFuelMin() * 100 local Damage = Defender:GetLife() / Defender:GetLife0() * 100 DefenderGroupCount = DefenderGroupCount + 1 - Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", - Defender:GetName(), + Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", + Defender:GetName(), Defender:GetSize(), Defender:GetInitialSize(), - DefenderTask.Type, - DefenderTask.Fsm:GetState(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), Defender:GetSize(), Fuel, - Damage, + Damage, Defender:HasTask() == true and "Executing" or "Idle" ) ) end end @@ -3757,9 +3757,9 @@ do -- AI_A2A_DISPATCHER self:F( Report:Text( "\n" ) ) trigger.action.outText( Report:Text( "\n" ), 25 ) - + return true - + end --- Assigns A2A AI Tasks in relation to the detected items. @@ -3767,14 +3767,14 @@ do -- AI_A2A_DISPATCHER -- @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_A2A_DISPATCHER:ProcessDetected( Detection ) - + local AreaMsg = {} local TaskMsg = {} local ChangeMsg = {} - + local TaskReport = REPORT:New() - + for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do local AIGroup = AIGroup -- Wrapper.Group#GROUP if not AIGroup:IsAlive() then @@ -3810,7 +3810,7 @@ do -- AI_A2A_DISPATCHER -- Closest detected targets to be considered first! --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do - + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT local DetectedCount = DetectedSet:Count() @@ -3822,8 +3822,8 @@ do -- AI_A2A_DISPATCHER local DetectedID = DetectedItem.ID local DetectionIndex = DetectedItem.Index local DetectedItemChanged = DetectedItem.Changed - - do + + do local Friendlies = self:EvaluateENGAGE( DetectedItem ) -- Returns a SetUnit if there are targets to be GCIed... if Friendlies then self:F( { AIGroups = Friendlies } ) @@ -3843,7 +3843,7 @@ do -- AI_A2A_DISPATCHER if self.TacticalDisplay then self:ShowTacticalDisplay( Detection ) end - + return true end @@ -3856,10 +3856,10 @@ do -- @param Functional.Detection#DETECTION_BASE.DetectedItem 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_A2A_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) - + local DetectedSet = DetectedItem.Set local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) - + local PlayerTypes = {} local PlayersCount = 0 @@ -3878,13 +3878,13 @@ do 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 ) ) @@ -3892,8 +3892,8 @@ do else PlayerTypesReport:Add( "-" ) end - - + + return PlayersCount, PlayerTypesReport end @@ -3902,10 +3902,10 @@ do -- @param Functional.Detection#DETECTION_BASE.DetectedItem 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_A2A_DISPATCHER:GetFriendliesNearBy( DetectedItem ) - + local DetectedSet = DetectedItem.Set local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem ) - + local FriendlyTypes = {} local FriendliesCount = 0 @@ -3922,13 +3922,13 @@ do 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 ) ) @@ -3936,8 +3936,8 @@ do else FriendlyTypesReport:Add( "-" ) end - - + + return FriendliesCount, FriendlyTypesReport end @@ -3955,256 +3955,256 @@ do --- @type AI_A2A_GCICAP -- @extends #AI_A2A_DISPATCHER - --- Create an automatic air defence system for a coalition setting up GCI and CAP air defenses. + --- Create an automatic air defence system for a coalition setting up GCI and CAP air defenses. -- The class derives from @{#AI_A2A_DISPATCHER} and thus, all the methods that are defined in the @{#AI_A2A_DISPATCHER} class, can be used also in AI\_A2A\_GCICAP. - -- + -- -- === - -- + -- -- # Demo Missions - -- + -- -- ### [AI\_A2A\_GCICAP for Caucasus](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-200%20-%20AI_A2A%20-%20GCICAP%20Demonstration) -- ### [AI\_A2A\_GCICAP for NTTR](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-210%20-%20NTTR%20AI_A2A_GCICAP%20Demonstration) -- ### [AI\_A2A\_GCICAP for Normandy](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-220%20-%20NORMANDY%20AI_A2A_GCICAP%20Demonstration) - -- + -- -- ### [AI\_A2A\_GCICAP for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching) -- -- === - -- + -- -- # YouTube Channel - -- + -- -- ### [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) - -- + -- -- === - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia3.JPG) - -- - -- AI\_A2A\_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. - -- + -- + -- AI\_A2A\_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_A2A_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 A2A 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, + -- + -- The AI_A2A_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 A2A 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). - -- + -- **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\_A2A\_GCICAP in your mission: - -- - -- ## 1) Configure a working AI\_A2A\_GCICAP defense system for ONE coalition. - -- - -- ### 1.1) Define which airbases are for which coalition. - -- + -- + -- ## 1) Configure a working AI\_A2A\_GCICAP defense system for ONE coalition. + -- + -- ### 1.1) Define which airbases are for which coalition. + -- -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_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. - -- + -- + -- ### 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_A2A_DISPATCHER\AI_A2A_GCICAP-ME_2.JPG) - -- - -- **All EWR groups starting with the EWR prefix (text) will be included in the detection system.** - -- + -- + -- **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. + -- 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! + -- 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 + -- 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, + -- + -- 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. - -- + -- + -- ### 1.3) Place Airplane or Helicopter Groups with late activation switched on above the airbases to define Squadrons. + -- -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_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, + -- + -- 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_A2A_DISPATCHER\AI_A2A_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. - -- + -- + -- **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_A2A_DISPATCHER\AI_A2A_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. - -- + -- + -- **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_A2A_DISPATCHER\AI_A2A_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_A2A_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\_A2A\_DISPATCHER: - -- + -- -- * @{#AI_A2A_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_A2A_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. -- * @{#AI_A2A_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\_A2A\_DISPATCHER: - -- + -- -- * @{#AI_A2A_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_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. -- * @{#AI_A2A_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_A2A_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. + -- + -- ### 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\_A2A\_DISPATCHER: - -- + -- -- The method @{#AI_A2A_DISPATCHER.SetSquadronCap}() 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. - -- + -- + -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. + -- -- The @{#AI_A2A_DISPATCHER.SetSquadronCapInterval}() 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. - -- + -- + -- 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": - -- + -- -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) -- A2ADispatcher:SetSquadronCapInterval( "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\_A2A\_DISPATCHER: - -- + -- -- The method @{#AI_A2A_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, + -- 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": - -- + -- -- A2ADispatcher: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. + -- 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 A2A defense system, **two AI\_A2A\_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\_A2A\_GCICAP class: - -- + -- -- ### 4.1) An easy setup: - -- + -- -- -- Setup the AI_A2A_GCICAP dispatcher for one coalition, and initialize it. -- GCI_Red = AI_A2A_GCICAP:New( "EWR CCCP", "SQUADRON CCCP", "CAP CCCP", 2 ) - -- -- + -- -- -- The following parameters were given to the :New method of AI_A2A_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. - -- - -- + -- * `2` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- + -- -- ### 4.2) A more advanced setup: - -- + -- -- -- Setup the AI_A2A_GCICAP dispatcher for the blue coalition. - -- - -- A2A_GCICAP_Blue = AI_A2A_GCICAP:New( { "BLUE EWR" }, { "104th", "105th", "106th" }, { "104th CAP" }, 4 ) - -- + -- + -- A2A_GCICAP_Blue = AI_A2A_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 + -- * `{ "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, + -- 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. - -- + -- * `4` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- -- @field #AI_A2A_GCICAP AI_A2A_GCICAP = { ClassName = "AI_A2A_GCICAP", @@ -4218,73 +4218,73 @@ do -- @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 #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. + -- @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_A2A_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. - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2 ) - -- + -- A2ADispatcher = AI_A2A_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. - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000 ) - -- + -- A2ADispatcher = AI_A2A_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, + -- -- 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. - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000 ) - -- + -- A2ADispatcher = AI_A2A_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, + -- -- 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. - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000 ) - -- + -- A2ADispatcher = AI_A2A_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, + -- -- 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. - -- - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) - -- + -- + -- A2ADispatcher = AI_A2A_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. @@ -4294,9 +4294,9 @@ do -- -- 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. - -- - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 ) - -- + -- + -- 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 ) local EWRSetGroup = SET_GROUP:New() @@ -4306,14 +4306,14 @@ do local Detection = DETECTION_AREAS:New( EWRSetGroup, GroupingRadius or 30000 ) local self = BASE:Inherit( self, AI_A2A_DISPATCHER:New( Detection ) ) -- #AI_A2A_GCICAP - + self:SetEngageRadius( EngageRadius ) 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 @@ -4322,25 +4322,25 @@ do if Airbase:GetCoalition() == EWRCoalition then table.insert( AirbaseNames, AirbaseName ) end - end - + end + self.Templates = SET_GROUP :New() :FilterPrefixes( TemplatePrefixes ) :FilterOnce() -- Setup squadrons - + self:I( { Airbases = AirbaseNames } ) - self:I( "Defining Templates for Airbases ..." ) + 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 } ) + self:I( { Airbase = AirbaseName } ) for TemplateID, Template in pairs( self.Templates:GetSet() ) do local Template = Template -- Wrapper.Group#GROUP local TemplateCoord = Template:GetCoordinate() @@ -4356,20 +4356,20 @@ do end -- Setup CAP. - -- Find for each CAP the nearest airbase to the (start or center) of the zone. + -- 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( CapPrefixes ) self.CAPTemplates:FilterOnce() - - self:I( "Setting up CAP ..." ) + + 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 } ) + self:I( { CAPZoneGroup = CAPID } ) for AirbaseID, AirbaseName in pairs( AirbaseNames ) do local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE local AirbaseName = Airbase:GetName() @@ -4377,7 +4377,7 @@ do local Squadron = self.DefenderSquadrons[AirbaseName] if Squadron then local Distance = AirbaseCoord:Get2DDistance( CAPZone:GetCoordinate() ) - self:I( { AirbaseDistance = Distance } ) + self:I( { AirbaseDistance = Distance } ) if Distance < AirbaseDistance then AirbaseDistance = Distance AirbaseClosest = Airbase @@ -4385,35 +4385,35 @@ do end end if AirbaseClosest then - self:I( { CAPAirbase = AirbaseClosest:GetName() } ) + 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 ) - end - end + end + end -- Setup GCI. -- GCI is setup for all Squadrons. - self:I( "Setting up GCI ..." ) + 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 } ) + self:F( { Airbase = AirbaseName } ) if Squadron then - self:I( { GCIAirbase = AirbaseName } ) + 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 @@ -4424,24 +4424,24 @@ do -- @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 #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. + -- @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_A2A_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. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2 ) - -- + -- + -- A2ADispatcher = AI_A2A_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. @@ -4449,11 +4449,11 @@ do -- -- 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. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000 ) - -- + -- + -- A2ADispatcher = AI_A2A_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. @@ -4461,13 +4461,13 @@ do -- -- 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, + -- -- 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. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000 ) - -- + -- + -- A2ADispatcher = AI_A2A_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. @@ -4475,14 +4475,14 @@ do -- -- 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, + -- -- 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. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000 ) - -- + -- + -- A2ADispatcher = AI_A2A_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. @@ -4490,15 +4490,15 @@ do -- -- 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, + -- -- 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. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) - -- + -- + -- A2ADispatcher = AI_A2A_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. @@ -4509,9 +4509,9 @@ do -- -- 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. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 ) - -- + -- + -- 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 ) local self = AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) @@ -4519,10 +4519,9 @@ do 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_A2A_Gci.lua b/Moose Development/Moose/AI/AI_A2A_Gci.lua index f0ddb1d13..4a3f4570b 100644 --- a/Moose Development/Moose/AI/AI_A2A_Gci.lua +++ b/Moose Development/Moose/AI/AI_A2A_Gci.lua @@ -1,12 +1,12 @@ --- **AI** -- (R2.2) - Models the process of Ground Controlled Interception (GCI) for airplanes. -- -- This is a class used in the @{AI_A2A_Dispatcher}. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- --- === +-- +-- === -- -- @module AI.AI_A2A_GCI -- @image AI_Ground_Control_Intercept.JPG @@ -18,52 +18,52 @@ --- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. --- +-- -- ![Process](..\Presentations\AI_GCI\Dia3.JPG) --- +-- -- The AI_A2A_GCI is assigned a @{Wrapper.Group} and this must be done before the AI_A2A_GCI 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_A2A_GCI constructor --- +-- -- * @{#AI_A2A_GCI.New}(): Creates a new AI_A2A_GCI object. --- +-- -- ## 2. AI_A2A_GCI is a FSM --- +-- -- ![Process](..\Presentations\AI_GCI\Dia2.JPG) --- +-- -- ### 2.1 AI_A2A_GCI 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_A2A_GCI 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_A2A_GCI.Engage}**: Let the AI engage the bogeys. @@ -76,25 +76,25 @@ -- * **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_GCI\Dia11.JPG) --- --- An optional range can be set in meters, +-- +-- 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_A2A_GCI.SetEngageRange}() to define that range. -- -- ## 4. Set the Zone of Engagement --- +-- -- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) --- --- An optional @{Zone} can be set, +-- +-- 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_A2A_GCI.SetEngageZone}() to define that Zone. --- +-- -- === --- +-- -- @field #AI_A2A_GCI AI_A2A_GCI = { ClassName = "AI_A2A_GCI", @@ -116,7 +116,7 @@ function AI_A2A_GCI:New2( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFlo local AI_Air = AI_AIR:New( AIIntercept ) local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air, AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) local self = BASE:Inherit( self, AI_Air_Engage ) -- #AI_A2A_GCI - + self:SetFuelThreshold( .2, 60 ) self:SetDamageThreshold( 0.4 ) self:SetDisengageRadius( 70000 ) @@ -150,7 +150,7 @@ function AI_A2A_GCI:onafterStart( AIIntercept, From, Event, To ) end ---- Evaluate the attack and create an AttackUnitTask list. +--- Evaluate the attack and create an AttackUnitTask list. -- @param #AI_A2A_GCI self -- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. -- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. @@ -165,14 +165,10 @@ function AI_A2A_GCI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageA self:T( { "Attacking Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) if AttackUnit:IsAlive() and AttackUnit:IsAir() then -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition() - -- Maybe the detected set also contains + -- Maybe the detected set also contains AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) end end - + return AttackUnitTasks end - - - - diff --git a/Moose Development/Moose/Actions/Act_Account.lua b/Moose Development/Moose/Actions/Act_Account.lua index b0d26c9ea..835b52871 100644 --- a/Moose Development/Moose/Actions/Act_Account.lua +++ b/Moose Development/Moose/Actions/Act_Account.lua @@ -1,34 +1,34 @@ --- **Actions** - ACT_ACCOUNT_ classes **account for** (detect, count & report) various DCS events occuring on @{Wrapper.Unit}s. --- +-- -- ![Banner Image](..\Presentations\ACT_ACCOUNT\Dia1.JPG) --- --- === --- +-- +-- === +-- -- @module Actions.Account -- @image MOOSE.JPG do -- ACT_ACCOUNT - + --- # @{#ACT_ACCOUNT} FSM class, extends @{Core.Fsm#FSM_PROCESS} - -- - -- ## ACT_ACCOUNT state machine: - -- + -- + -- ## ACT_ACCOUNT state machine: + -- -- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. -- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. - -- Each derived class follows exactly the same process, using the same events and following the same state transitions, + -- Each derived class follows exactly the same process, using the same events and following the same state transitions, -- but will have **different implementation behaviour** upon each event or state transition. - -- - -- ### ACT_ACCOUNT States - -- + -- + -- ### ACT_ACCOUNT States + -- -- * **Asigned**: The player is assigned. -- * **Waiting**: Waiting for an event. -- * **Report**: Reporting. -- * **Account**: Account for an event. -- * **Accounted**: All events have been accounted for, end of the process. -- * **Failed**: Failed the process. - -- - -- ### ACT_ACCOUNT Events - -- + -- + -- ### ACT_ACCOUNT Events + -- -- * **Start**: Start the process. -- * **Wait**: Wait for an event. -- * **Report**: Report the status of the accounting. @@ -36,32 +36,32 @@ do -- ACT_ACCOUNT -- * **More**: More targets. -- * **NoMore (*)**: No more targets. -- * **Fail (*)**: The action process has failed. - -- + -- -- (*) End states of the process. - -- + -- -- ### ACT_ACCOUNT state transition methods: - -- + -- -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. -- There are 2 moments when state transition methods will be called by the state machine: - -- - -- * **Before** the state transition. - -- The state transition method needs to start with the name **OnBefore + the name of the state**. + -- + -- * **Before** the state transition. + -- The state transition method needs to start with the name **OnBefore + the name of the state**. -- If the state transition method returns false, then the processing of the state transition will not be done! - -- If you want to change the behaviour of the AIControllable at this event, return false, + -- If you want to change the behaviour of the AIControllable at this event, return false, -- but then you'll need to specify your own logic using the AIControllable! - -- - -- * **After** the state transition. - -- The state transition method needs to start with the name **OnAfter + the name of the state**. + -- + -- * **After** the state transition. + -- The state transition method needs to start with the name **OnAfter + the name of the state**. -- These state transition methods need to provide a return value, which is specified at the function description. - -- + -- -- @type ACT_ACCOUNT -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Core.Fsm#FSM_PROCESS - ACT_ACCOUNT = { + ACT_ACCOUNT = { ClassName = "ACT_ACCOUNT", TargetSetUnit = nil, } - + --- Creates a new DESTROY process. -- @param #ACT_ACCOUNT self -- @return #ACT_ACCOUNT @@ -69,7 +69,7 @@ do -- ACT_ACCOUNT -- Inherits from BASE local self = BASE:Inherit( self, FSM_PROCESS:New() ) -- Core.Fsm#FSM_PROCESS - + self:AddTransition( "Assigned", "Start", "Waiting" ) self:AddTransition( "*", "Wait", "Waiting" ) self:AddTransition( "*", "Report", "Report" ) @@ -79,16 +79,16 @@ do -- ACT_ACCOUNT self:AddTransition( { "Account", "AccountForPlayer", "AccountForOther" }, "More", "Wait" ) self:AddTransition( { "Account", "AccountForPlayer", "AccountForOther" }, "NoMore", "Accounted" ) self:AddTransition( "*", "Fail", "Failed" ) - + self:AddEndState( "Failed" ) - - self:SetStartState( "Assigned" ) - + + self:SetStartState( "Assigned" ) + return self end --- Process Events - + --- StateMachine callback function -- @param #ACT_ACCOUNT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -104,7 +104,7 @@ do -- ACT_ACCOUNT self:__Wait( 1 ) end - + --- StateMachine callback function -- @param #ACT_ACCOUNT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -112,17 +112,17 @@ do -- ACT_ACCOUNT -- @param #string From -- @param #string To function ACT_ACCOUNT:onenterWaiting( ProcessUnit, From, Event, To ) - + if self.DisplayCount >= self.DisplayInterval then self:Report() self.DisplayCount = 1 else self.DisplayCount = self.DisplayCount + 1 end - + return true -- Process always the event. end - + --- StateMachine callback function -- @param #ACT_ACCOUNT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -130,30 +130,30 @@ do -- ACT_ACCOUNT -- @param #string From -- @param #string To function ACT_ACCOUNT:onafterEvent( ProcessUnit, From, Event, To, Event ) - + self:__NoMore( 1 ) end - + end -- ACT_ACCOUNT do -- ACT_ACCOUNT_DEADS --- # @{#ACT_ACCOUNT_DEADS} FSM class, extends @{Core.Fsm.Account#ACT_ACCOUNT} - -- + -- -- The ACT_ACCOUNT_DEADS class accounts (detects, counts and reports) successful kills of DCS units. -- The process is given a @{Set} of units that will be tracked upon successful destruction. -- The process will end after each target has been successfully destroyed. -- Each successful dead will trigger an Account state transition that can be scored, modified or administered. - -- - -- + -- + -- -- ## ACT_ACCOUNT_DEADS constructor: - -- + -- -- * @{#ACT_ACCOUNT_DEADS.New}(): Creates a new ACT_ACCOUNT_DEADS object. - -- + -- -- @type ACT_ACCOUNT_DEADS -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends #ACT_ACCOUNT - ACT_ACCOUNT_DEADS = { + ACT_ACCOUNT_DEADS = { ClassName = "ACT_ACCOUNT_DEADS", } @@ -165,24 +165,24 @@ do -- ACT_ACCOUNT_DEADS function ACT_ACCOUNT_DEADS:New() -- Inherits from BASE local self = BASE:Inherit( self, ACT_ACCOUNT:New() ) -- #ACT_ACCOUNT_DEADS - + self.DisplayInterval = 30 self.DisplayCount = 30 self.DisplayMessage = true self.DisplayTime = 10 -- 10 seconds is the default self.DisplayCategory = "HQ" -- Targets is the default display category - + return self end - + function ACT_ACCOUNT_DEADS:Init( FsmAccount ) - - self.Task = self:GetTask() + + self.Task = self:GetTask() self.TaskName = self.Task:GetName() end --- Process Events - + --- StateMachine callback function -- @param #ACT_ACCOUNT_DEADS self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -190,12 +190,12 @@ do -- ACT_ACCOUNT_DEADS -- @param #string From -- @param #string To function ACT_ACCOUNT_DEADS:onenterReport( ProcessUnit, Task, From, Event, To ) - + local MessageText = "Your group with assigned " .. self.TaskName .. " task has " .. Task.TargetSetUnit:GetUnitTypesText() .. " targets left to be destroyed." self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) end - - + + --- StateMachine callback function -- @param #ACT_ACCOUNT_DEADS self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -206,7 +206,7 @@ do -- ACT_ACCOUNT_DEADS -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onafterEvent( ProcessUnit, Task, From, Event, To, EventData ) self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } ) - + if Task.TargetSetUnit:FindUnit( EventData.IniUnitName ) then local PlayerName = ProcessUnit:GetPlayerName() local PlayerHit = self.PlayerHits and self.PlayerHits[EventData.IniUnitName] @@ -228,14 +228,14 @@ do -- ACT_ACCOUNT_DEADS -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onenterAccountForPlayer( ProcessUnit, Task, From, Event, To, EventData ) self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } ) - + local TaskGroup = ProcessUnit:GetGroup() Task.TargetSetUnit:Remove( EventData.IniUnitName ) - + local MessageText = "You have destroyed a target.\nYour group assigned with task " .. self.TaskName .. " has\n" .. Task.TargetSetUnit:Count() .. " targets ( " .. Task.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed." self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) - + local PlayerName = ProcessUnit:GetPlayerName() Task:AddProgress( PlayerName, "Destroyed " .. EventData.IniTypeName, timer.getTime(), 1 ) @@ -256,10 +256,10 @@ do -- ACT_ACCOUNT_DEADS -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onenterAccountForOther( ProcessUnit, Task, From, Event, To, EventData ) self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } ) - + local TaskGroup = ProcessUnit:GetGroup() Task.TargetSetUnit:Remove( EventData.IniUnitName ) - + local MessageText = "One of the task targets has been destroyed.\nYour group assigned with task " .. self.TaskName .. " has\n" .. Task.TargetSetUnit:Count() .. " targets ( " .. Task.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed." self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) @@ -270,9 +270,9 @@ do -- ACT_ACCOUNT_DEADS end end - + --- DCS Events - + --- @param #ACT_ACCOUNT_DEADS self -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:OnEventHit( EventData ) @@ -282,8 +282,8 @@ do -- ACT_ACCOUNT_DEADS self.PlayerHits = self.PlayerHits or {} self.PlayerHits[EventData.TgtDCSUnitName] = EventData.IniPlayerName end - end - + end + --- @param #ACT_ACCOUNT_DEADS self -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onfuncEventDead( EventData ) @@ -295,7 +295,7 @@ do -- ACT_ACCOUNT_DEADS end --- DCS Events - + --- @param #ACT_ACCOUNT_DEADS self -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onfuncEventCrash( EventData ) diff --git a/Moose Development/Moose/Actions/Act_Assign.lua b/Moose Development/Moose/Actions/Act_Assign.lua index 9a4c907db..c6748cba8 100644 --- a/Moose Development/Moose/Actions/Act_Assign.lua +++ b/Moose Development/Moose/Actions/Act_Assign.lua @@ -1,82 +1,82 @@ --- (SP) (MP) (FSM) Accept or reject process for player (task) assignments. --- +-- -- === --- +-- -- # @{#ACT_ASSIGN} FSM template class, extends @{Core.Fsm#FSM_PROCESS} --- +-- -- ## ACT_ASSIGN state machine: --- +-- -- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. -- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, -- but will have **different implementation behaviour** upon each event or state transition. --- +-- -- ### ACT_ASSIGN **Events**: --- +-- -- These are the events defined in this class: --- +-- -- * **Start**: Start the tasking acceptance process. -- * **Assign**: Assign the task. -- * **Reject**: Reject the task.. --- +-- -- ### ACT_ASSIGN **Event methods**: --- +-- -- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. -- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- +-- -- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- -- ### ACT_ASSIGN **States**: --- +-- -- * **UnAssigned**: The player has not accepted the task. -- * **Assigned (*)**: The player has accepted the task. -- * **Rejected (*)**: The player has not accepted the task. -- * **Waiting**: The process is awaiting player feedback. -- * **Failed (*)**: The process has failed. --- +-- -- (*) End states of the process. --- +-- -- ### ACT_ASSIGN state transition methods: --- +-- -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. -- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. -- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, +-- If you want to change the behaviour of the AIControllable at this event, return false, -- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. -- These state transition methods need to provide a return value, which is specified at the function description. --- +-- -- === --- +-- -- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Core.Fsm.Assign#ACT_ASSIGN} --- +-- -- The ACT_ASSIGN_ACCEPT class accepts by default a task for a player. No player intervention is allowed to reject the task. --- +-- -- ## 1.1) ACT_ASSIGN_ACCEPT constructor: --- +-- -- * @{#ACT_ASSIGN_ACCEPT.New}(): Creates a new ACT_ASSIGN_ACCEPT object. --- +-- -- === --- +-- -- # 2) @{#ACT_ASSIGN_MENU_ACCEPT} class, extends @{Core.Fsm.Assign#ACT_ASSIGN} --- +-- -- The ACT_ASSIGN_MENU_ACCEPT class accepts a task when the player accepts the task through an added menu option. -- This assignment type is useful to conditionally allow the player to choose whether or not he would accept the task. -- The assignment type also allows to reject the task. --- +-- -- ## 2.1) ACT_ASSIGN_MENU_ACCEPT constructor: -- ----------------------------------------- --- +-- -- * @{#ACT_ASSIGN_MENU_ACCEPT.New}(): Creates a new ACT_ASSIGN_MENU_ACCEPT object. --- +-- -- === --- +-- -- @module Actions.Assign -- @image MOOSE.JPG @@ -89,11 +89,11 @@ do -- ACT_ASSIGN -- @field Wrapper.Unit#UNIT ProcessUnit -- @field Core.Zone#ZONE_BASE TargetZone -- @extends Core.Fsm#FSM_PROCESS - ACT_ASSIGN = { + ACT_ASSIGN = { ClassName = "ACT_ASSIGN", } - - + + --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. -- @param #ACT_ASSIGN self -- @return #ACT_ASSIGN The task acceptance process. @@ -106,16 +106,16 @@ do -- ACT_ASSIGN self:AddTransition( "Waiting", "Assign", "Assigned" ) self:AddTransition( "Waiting", "Reject", "Rejected" ) self:AddTransition( "*", "Fail", "Failed" ) - + self:AddEndState( "Assigned" ) self:AddEndState( "Rejected" ) self:AddEndState( "Failed" ) - - self:SetStartState( "UnAssigned" ) - + + self:SetStartState( "UnAssigned" ) + return self end - + end -- ACT_ASSIGN @@ -128,26 +128,26 @@ do -- ACT_ASSIGN_ACCEPT -- @field Wrapper.Unit#UNIT ProcessUnit -- @field Core.Zone#ZONE_BASE TargetZone -- @extends #ACT_ASSIGN - ACT_ASSIGN_ACCEPT = { + ACT_ASSIGN_ACCEPT = { ClassName = "ACT_ASSIGN_ACCEPT", } - - + + --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. -- @param #ACT_ASSIGN_ACCEPT self -- @param #string TaskBriefing function ACT_ASSIGN_ACCEPT:New( TaskBriefing ) - + local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_ACCEPT self.TaskBriefing = TaskBriefing - + return self end function ACT_ASSIGN_ACCEPT:Init( FsmAssign ) - - self.TaskBriefing = FsmAssign.TaskBriefing + + self.TaskBriefing = FsmAssign.TaskBriefing end --- StateMachine callback function @@ -157,8 +157,8 @@ do -- ACT_ASSIGN_ACCEPT -- @param #string From -- @param #string To function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, Task, From, Event, To ) - - self:__Assign( 1 ) + + self:__Assign( 1 ) end --- StateMachine callback function @@ -168,10 +168,10 @@ do -- ACT_ASSIGN_ACCEPT -- @param #string From -- @param #string To function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To, TaskGroup ) - + self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() ) end - + end -- ACT_ASSIGN_ACCEPT @@ -183,7 +183,7 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @field Wrapper.Unit#UNIT ProcessUnit -- @field Core.Zone#ZONE_BASE TargetZone -- @extends #ACT_ASSIGN - ACT_ASSIGN_MENU_ACCEPT = { + ACT_ASSIGN_MENU_ACCEPT = { ClassName = "ACT_ASSIGN_MENU_ACCEPT", } @@ -197,7 +197,7 @@ do -- ACT_ASSIGN_MENU_ACCEPT local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_MENU_ACCEPT self.TaskBriefing = TaskBriefing - + return self end @@ -207,12 +207,12 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @param #string TaskBriefing -- @return #ACT_ASSIGN_MENU_ACCEPT self function ACT_ASSIGN_MENU_ACCEPT:Init( TaskBriefing ) - + self.TaskBriefing = TaskBriefing return self end - + --- StateMachine callback function -- @param #ACT_ASSIGN_MENU_ACCEPT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -222,30 +222,30 @@ do -- ACT_ASSIGN_MENU_ACCEPT function ACT_ASSIGN_MENU_ACCEPT:onafterStart( ProcessUnit, Task, From, Event, To ) self:GetCommandCenter():MessageToGroup( "Task " .. self.Task:GetName() .. " has been assigned to you and your group!\nRead the briefing and use the Radio Menu (F10) / Task ... CONFIRMATION menu to accept or reject the task.\nYou have 2 minutes to accept, or the task assignment will be cancelled!", ProcessUnit:GetGroup(), 120 ) - - local TaskGroup = ProcessUnit:GetGroup() + + local TaskGroup = ProcessUnit:GetGroup() self.Menu = MENU_GROUP:New( TaskGroup, "Task " .. self.Task:GetName() .. " CONFIRMATION" ) self.MenuAcceptTask = MENU_GROUP_COMMAND:New( TaskGroup, "Accept task " .. self.Task:GetName(), self.Menu, self.MenuAssign, self, TaskGroup ) self.MenuRejectTask = MENU_GROUP_COMMAND:New( TaskGroup, "Reject task " .. self.Task:GetName(), self.Menu, self.MenuReject, self, TaskGroup ) - + self:__Reject( 120, TaskGroup ) end - + --- Menu function. -- @param #ACT_ASSIGN_MENU_ACCEPT self function ACT_ASSIGN_MENU_ACCEPT:MenuAssign( TaskGroup ) - + self:__Assign( -1, TaskGroup ) end - + --- Menu function. -- @param #ACT_ASSIGN_MENU_ACCEPT self function ACT_ASSIGN_MENU_ACCEPT:MenuReject( TaskGroup ) - + self:__Reject( -1, TaskGroup ) end - + --- StateMachine callback function -- @param #ACT_ASSIGN_MENU_ACCEPT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -253,10 +253,10 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @param #string From -- @param #string To function ACT_ASSIGN_MENU_ACCEPT:onafterAssign( ProcessUnit, Task, From, Event, To, TaskGroup ) - + self.Menu:Remove() end - + --- StateMachine callback function -- @param #ACT_ASSIGN_MENU_ACCEPT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -265,7 +265,7 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @param #string To function ACT_ASSIGN_MENU_ACCEPT:onafterReject( ProcessUnit, Task, From, Event, To, TaskGroup ) self:F( { TaskGroup = TaskGroup } ) - + self.Menu:Remove() --TODO: need to resolve this problem ... it has to do with the events ... --self.Task:UnAssignFromUnit( ProcessUnit )needs to become a callback funtion call upon the event @@ -279,7 +279,7 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @param #string From -- @param #string To function ACT_ASSIGN_MENU_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To, TaskGroup ) - + --self.Task:AssignToGroup( TaskGroup ) self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() ) end diff --git a/Moose Development/Moose/Actions/Act_Assist.lua b/Moose Development/Moose/Actions/Act_Assist.lua index f9cd5fc3a..fb0e81d6b 100644 --- a/Moose Development/Moose/Actions/Act_Assist.lua +++ b/Moose Development/Moose/Actions/Act_Assist.lua @@ -1,65 +1,65 @@ --- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. --- +-- -- ## ACT_ASSIST state machine: --- +-- -- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. -- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, -- but will have **different implementation behaviour** upon each event or state transition. --- +-- -- ### ACT_ASSIST **Events**: --- +-- -- These are the events defined in this class: --- +-- -- * **Start**: The process is started. -- * **Next**: The process is smoking the targets in the given zone. --- +-- -- ### ACT_ASSIST **Event methods**: --- +-- -- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. -- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- +-- -- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- -- ### ACT_ASSIST **States**: --- +-- -- * **None**: The controllable did not receive route commands. -- * **AwaitSmoke (*)**: The process is awaiting to smoke the targets in the zone. -- * **Smoking (*)**: The process is smoking the targets in the zone. -- * **Failed (*)**: The process has failed. --- +-- -- (*) End states of the process. --- +-- -- ### ACT_ASSIST state transition methods: --- +-- -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. -- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. -- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, +-- If you want to change the behaviour of the AIControllable at this event, return false, -- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. -- These state transition methods need to provide a return value, which is specified at the function description. --- +-- -- === --- +-- -- # 1) @{#ACT_ASSIST_SMOKE_TARGETS_ZONE} class, extends @{Core.Fsm.Route#ACT_ASSIST} --- +-- -- The ACT_ASSIST_SMOKE_TARGETS_ZONE class implements the core functions to smoke targets in a @{Zone}. --- The targets are smoked within a certain range around each target, simulating a realistic smoking behaviour. +-- The targets are smoked within a certain range around each target, simulating a realistic smoking behaviour. -- At random intervals, a new target is smoked. --- +-- -- # 1.1) ACT_ASSIST_SMOKE_TARGETS_ZONE constructor: --- +-- -- * @{#ACT_ASSIST_SMOKE_TARGETS_ZONE.New}(): Creates a new ACT_ASSIST_SMOKE_TARGETS_ZONE object. --- +-- -- === --- +-- -- @module Actions.Assist -- @image MOOSE.JPG @@ -69,7 +69,7 @@ do -- ACT_ASSIST --- ACT_ASSIST class -- @type ACT_ASSIST -- @extends Core.Fsm#FSM_PROCESS - ACT_ASSIST = { + ACT_ASSIST = { ClassName = "ACT_ASSIST", } @@ -86,15 +86,15 @@ do -- ACT_ASSIST self:AddTransition( "Smoking", "Next", "AwaitSmoke" ) self:AddTransition( "*", "Stop", "Success" ) self:AddTransition( "*", "Fail", "Failed" ) - + self:AddEndState( "Failed" ) self:AddEndState( "Success" ) - - self:SetStartState( "None" ) + + self:SetStartState( "None" ) return self end - + --- Task Events --- StateMachine callback function @@ -104,17 +104,17 @@ do -- ACT_ASSIST -- @param #string From -- @param #string To function ACT_ASSIST:onafterStart( ProcessUnit, From, Event, To ) - + local ProcessGroup = ProcessUnit:GetGroup() local MissionMenu = self:GetMission():GetMenu( ProcessGroup ) - + local function MenuSmoke( MenuParam ) local self = MenuParam.self local SmokeColor = MenuParam.SmokeColor self.SmokeColor = SmokeColor self:__Next( 1 ) end - + self.Menu = MENU_GROUP:New( ProcessGroup, "Target acquisition", MissionMenu ) self.MenuSmokeBlue = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop blue smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Blue } ) self.MenuSmokeGreen = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop green smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Green } ) @@ -130,10 +130,10 @@ do -- ACT_ASSIST -- @param #string From -- @param #string To function ACT_ASSIST:onafterStop( ProcessUnit, From, Event, To ) - + self.Menu:Remove() -- When stopped, remove the menus end - + end do -- ACT_ASSIST_SMOKE_TARGETS_ZONE @@ -143,17 +143,17 @@ do -- ACT_ASSIST_SMOKE_TARGETS_ZONE -- @field Core.Set#SET_UNIT TargetSetUnit -- @field Core.Zone#ZONE_BASE TargetZone -- @extends #ACT_ASSIST - ACT_ASSIST_SMOKE_TARGETS_ZONE = { + ACT_ASSIST_SMOKE_TARGETS_ZONE = { ClassName = "ACT_ASSIST_SMOKE_TARGETS_ZONE", } - + -- function ACT_ASSIST_SMOKE_TARGETS_ZONE:_Destructor() -- self:E("_Destructor") --- +-- -- self.Menu:Remove() -- self:EventRemoveAll() -- end - + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self -- @param Core.Set#SET_UNIT TargetSetUnit @@ -163,29 +163,29 @@ do -- ACT_ASSIST_SMOKE_TARGETS_ZONE self.TargetSetUnit = TargetSetUnit self.TargetZone = TargetZone - + return self end function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( FsmSmoke ) - + self.TargetSetUnit = FsmSmoke.TargetSetUnit self.TargetZone = FsmSmoke.TargetZone end - + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self -- @param Core.Set#SET_UNIT TargetSetUnit -- @param Core.Zone#ZONE_BASE TargetZone -- @return #ACT_ASSIST_SMOKE_TARGETS_ZONE self function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( TargetSetUnit, TargetZone ) - + self.TargetSetUnit = TargetSetUnit self.TargetZone = TargetZone - + return self end - + --- StateMachine callback function -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit @@ -193,7 +193,7 @@ do -- ACT_ASSIST_SMOKE_TARGETS_ZONE -- @param #string From -- @param #string To function ACT_ASSIST_SMOKE_TARGETS_ZONE:onenterSmoking( ProcessUnit, From, Event, To ) - + self.TargetSetUnit:ForEachUnit( --- @param Wrapper.Unit#UNIT SmokeUnit function( SmokeUnit ) @@ -203,12 +203,12 @@ do -- ACT_ASSIST_SMOKE_TARGETS_ZONE if SmokeUnit:IsAlive() then SmokeUnit:Smoke( self.SmokeColor, 150 ) end - end, {}, math.random( 10, 60 ) + end, {}, math.random( 10, 60 ) ) end end ) - + end - -end \ No newline at end of file + +end diff --git a/Moose Development/Moose/Actions/Act_Route.lua b/Moose Development/Moose/Actions/Act_Route.lua index d719f1f7b..f2c5ebcda 100644 --- a/Moose Development/Moose/Actions/Act_Route.lua +++ b/Moose Development/Moose/Actions/Act_Route.lua @@ -1,20 +1,20 @@ --- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. --- +-- -- === --- +-- -- # @{#ACT_ROUTE} FSM class, extends @{Core.Fsm#FSM_PROCESS} --- +-- -- ## ACT_ROUTE state machine: --- +-- -- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. -- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, -- but will have **different implementation behaviour** upon each event or state transition. --- +-- -- ### ACT_ROUTE **Events**: --- +-- -- These are the events defined in this class: --- +-- -- * **Start**: The process is started. The process will go into the Report state. -- * **Report**: The process is reporting to the player the route to be followed. -- * **Route**: The process is routing the controllable. @@ -22,56 +22,56 @@ -- * **Arrive**: The controllable has arrived at a route point. -- * **More**: There are more route points that need to be followed. The process will go back into the Report state. -- * **NoMore**: There are no more route points that need to be followed. The process will go into the Success state. --- +-- -- ### ACT_ROUTE **Event methods**: --- +-- -- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. -- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- +-- -- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- -- ### ACT_ROUTE **States**: --- +-- -- * **None**: The controllable did not receive route commands. -- * **Arrived (*)**: The controllable has arrived at a route point. -- * **Aborted (*)**: The controllable has aborted the route path. -- * **Routing**: The controllable is understay to the route point. -- * **Pausing**: The process is pausing the routing. AI air will go into hover, AI ground will stop moving. Players can fly around. --- * **Success (*)**: All route points were reached. +-- * **Success (*)**: All route points were reached. -- * **Failed (*)**: The process has failed. --- +-- -- (*) End states of the process. --- +-- -- ### ACT_ROUTE state transition methods: --- +-- -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. -- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. -- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, +-- If you want to change the behaviour of the AIControllable at this event, return false, -- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. -- These state transition methods need to provide a return value, which is specified at the function description. --- +-- -- === --- +-- -- # 1) @{#ACT_ROUTE_ZONE} class, extends @{Core.Fsm.Route#ACT_ROUTE} --- +-- -- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Wrapper.Controllable} player @{Wrapper.Unit} to a @{Zone}. --- The player receives on perioding times messages with the coordinates of the route to follow. +-- The player receives on perioding times messages with the coordinates of the route to follow. -- Upon arrival at the zone, a confirmation of arrival is sent, and the process will be ended. --- +-- -- # 1.1) ACT_ROUTE_ZONE constructor: --- +-- -- * @{#ACT_ROUTE_ZONE.New}(): Creates a new ACT_ROUTE_ZONE object. --- +-- -- === --- +-- -- @module Actions.Route -- @image MOOSE.JPG @@ -85,11 +85,11 @@ do -- ACT_ROUTE -- @field Core.Zone#ZONE_BASE Zone -- @field Core.Point#COORDINATE Coordinate -- @extends Core.Fsm#FSM_PROCESS - ACT_ROUTE = { + ACT_ROUTE = { ClassName = "ACT_ROUTE", } - - + + --- Creates a new routing state machine. The process will route a CLIENT to a ZONE until the CLIENT is within that ZONE. -- @param #ACT_ROUTE self -- @return #ACT_ROUTE self @@ -97,7 +97,7 @@ do -- ACT_ROUTE -- Inherits from BASE local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ROUTE" ) ) -- Core.Fsm#FSM_PROCESS - + self:AddTransition( "*", "Reset", "None" ) self:AddTransition( "None", "Start", "Routing" ) self:AddTransition( "*", "Report", "*" ) @@ -109,23 +109,23 @@ do -- ACT_ROUTE self:AddTransition( "*", "Fail", "Failed" ) self:AddTransition( "", "", "" ) self:AddTransition( "", "", "" ) - + self:AddEndState( "Arrived" ) self:AddEndState( "Failed" ) self:AddEndState( "Cancelled" ) - + self:SetStartState( "None" ) - - self:SetRouteMode( "C" ) - + + self:SetRouteMode( "C" ) + return self end - + --- Set a Cancel Menu item. -- @param #ACT_ROUTE self -- @return #ACT_ROUTE function ACT_ROUTE:SetMenuCancel( MenuGroup, MenuText, ParentMenu, MenuTime, MenuTag ) - + self.CancelMenuGroupCommand = MENU_GROUP_COMMAND:New( MenuGroup, MenuText, @@ -135,47 +135,47 @@ do -- ACT_ROUTE ):SetTime( MenuTime ):SetTag( MenuTag ) ParentMenu:SetTime( MenuTime ) - + ParentMenu:Remove( MenuTime, MenuTag ) return self end - + --- Set the route mode. -- There are 2 route modes supported: - -- + -- -- * SetRouteMode( "B" ): Route mode is Bearing and Range. -- * SetRouteMode( "C" ): Route mode is LL or MGRS according coordinate system setup. - -- + -- -- @param #ACT_ROUTE self -- @return #ACT_ROUTE function ACT_ROUTE:SetRouteMode( RouteMode ) - + self.RouteMode = RouteMode - return self + return self end - + --- Get the routing text to be displayed. -- The route mode determines the text displayed. -- @param #ACT_ROUTE self -- @param Wrapper.Unit#UNIT Controllable -- @return #string function ACT_ROUTE:GetRouteText( Controllable ) - + local RouteText = "" local Coordinate = nil -- Core.Point#COORDINATE - + if self.Coordinate then Coordinate = self.Coordinate end - + if self.Zone then Coordinate = self.Zone:GetPointVec3( self.Altitude ) Coordinate:SetHeading( self.Heading ) end - + local Task = self:GetTask() -- This is to dermine that the coordinates are for a specific task mode (A2A or A2G). local CC = self:GetTask():GetMission():GetCommandCenter() @@ -209,7 +209,7 @@ do -- ACT_ROUTE return RouteText end - + function ACT_ROUTE:MenuCancel() self:F("Cancelled") self.CancelMenuGroupCommand:Remove() @@ -225,11 +225,11 @@ do -- ACT_ROUTE -- @param #string From -- @param #string To function ACT_ROUTE:onafterStart( ProcessUnit, From, Event, To ) - + self:__Route( 1 ) end - + --- Check if the controllable has arrived. -- @param #ACT_ROUTE self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -237,7 +237,7 @@ do -- ACT_ROUTE function ACT_ROUTE:onfuncHasArrived( ProcessUnit ) return false end - + --- StateMachine callback function -- @param #ACT_ROUTE self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -245,7 +245,7 @@ do -- ACT_ROUTE -- @param #string From -- @param #string To function ACT_ROUTE:onbeforeRoute( ProcessUnit, From, Event, To ) - + if ProcessUnit:IsAlive() then local HasArrived = self:onfuncHasArrived( ProcessUnit ) -- Polymorphic if self.DisplayCount >= self.DisplayInterval then @@ -257,18 +257,18 @@ do -- ACT_ROUTE else self.DisplayCount = self.DisplayCount + 1 end - + if HasArrived then self:__Arrive( 1 ) else self:__Route( 1 ) end - + return HasArrived -- if false, then the event will not be executed... end - + return false - + end end -- ACT_ROUTE @@ -280,12 +280,12 @@ do -- ACT_ROUTE_POINT -- @type ACT_ROUTE_POINT -- @field Tasking.Task#TASK TASK -- @extends #ACT_ROUTE - ACT_ROUTE_POINT = { + ACT_ROUTE_POINT = { ClassName = "ACT_ROUTE_POINT", } - --- Creates a new routing state machine. + --- Creates a new routing state machine. -- The task will route a controllable to a Coordinate until the controllable is within the Range. -- @param #ACT_ROUTE_POINT self -- @param Core.Point#COORDINATE The Coordinate to Target. @@ -296,29 +296,29 @@ do -- ACT_ROUTE_POINT self.Coordinate = Coordinate self.Range = Range or 0 - + self.DisplayInterval = 30 self.DisplayCount = 30 self.DisplayMessage = true self.DisplayTime = 10 -- 10 seconds is the default - + return self end - - --- Creates a new routing state machine. + + --- Creates a new routing state machine. -- The task will route a controllable to a Coordinate until the controllable is within the Range. -- @param #ACT_ROUTE_POINT self function ACT_ROUTE_POINT:Init( FsmRoute ) - + self.Coordinate = FsmRoute.Coordinate self.Range = FsmRoute.Range or 0 - + self.DisplayInterval = 30 self.DisplayCount = 30 self.DisplayMessage = true self.DisplayTime = 10 -- 10 seconds is the default self:SetStartState("None") - end + end --- Set Coordinate -- @param #ACT_ROUTE_POINT self @@ -326,7 +326,7 @@ do -- ACT_ROUTE_POINT function ACT_ROUTE_POINT:SetCoordinate( Coordinate ) self:F2( { Coordinate } ) self.Coordinate = Coordinate - end + end --- Get Coordinate -- @param #ACT_ROUTE_POINT self @@ -334,7 +334,7 @@ do -- ACT_ROUTE_POINT function ACT_ROUTE_POINT:GetCoordinate() self:F2( { self.Coordinate } ) return self.Coordinate - end + end --- Set Range around Coordinate -- @param #ACT_ROUTE_POINT self @@ -342,16 +342,16 @@ do -- ACT_ROUTE_POINT function ACT_ROUTE_POINT:SetRange( Range ) self:F2( { Range } ) self.Range = Range or 10000 - end - + end + --- Get Range around Coordinate -- @param #ACT_ROUTE_POINT self -- @return #number The Range to consider the arrival. Default is 10000 meters. function ACT_ROUTE_POINT:GetRange() self:F2( { self.Range } ) return self.Range - end - + end + --- Method override to check if the controllable has arrived. -- @param #ACT_ROUTE_POINT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -360,7 +360,7 @@ do -- ACT_ROUTE_POINT if ProcessUnit:IsAlive() then local Distance = self.Coordinate:Get2DDistance( ProcessUnit:GetCoordinate() ) - + if Distance <= self.Range then local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", you have arrived." self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) @@ -370,9 +370,9 @@ do -- ACT_ROUTE_POINT return false end - + --- Task Events - + --- StateMachine callback function -- @param #ACT_ROUTE_POINT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -380,9 +380,9 @@ do -- ACT_ROUTE_POINT -- @param #string From -- @param #string To function ACT_ROUTE_POINT:onafterReport( ProcessUnit, From, Event, To ) - + local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", " .. self:GetRouteText( ProcessUnit ) - + self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Update ) end @@ -397,7 +397,7 @@ do -- ACT_ROUTE_ZONE -- @field Wrapper.Unit#UNIT ProcessUnit -- @field Core.Zone#ZONE_BASE Zone -- @extends #ACT_ROUTE - ACT_ROUTE_ZONE = { + ACT_ROUTE_ZONE = { ClassName = "ACT_ROUTE_ZONE", } @@ -409,25 +409,25 @@ do -- ACT_ROUTE_ZONE local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_ZONE self.Zone = Zone - + self.DisplayInterval = 30 self.DisplayCount = 30 self.DisplayMessage = true self.DisplayTime = 10 -- 10 seconds is the default - + return self end - + function ACT_ROUTE_ZONE:Init( FsmRoute ) - + self.Zone = FsmRoute.Zone - + self.DisplayInterval = 30 self.DisplayCount = 30 self.DisplayMessage = true self.DisplayTime = 10 -- 10 seconds is the default - end - + end + --- Set Zone -- @param #ACT_ROUTE_ZONE self -- @param Core.Zone#ZONE_BASE Zone The Zone object where to route to. @@ -437,14 +437,14 @@ do -- ACT_ROUTE_ZONE self.Zone = Zone self.Altitude = Altitude self.Heading = Heading - end + end --- Get Zone -- @param #ACT_ROUTE_ZONE self -- @return Core.Zone#ZONE_BASE Zone The Zone object where to route to. function ACT_ROUTE_ZONE:GetZone() - return self.Zone - end + return self.Zone + end --- Method override to check if the controllable has arrived. -- @param #ACT_ROUTE self @@ -459,9 +459,9 @@ do -- ACT_ROUTE_ZONE return ProcessUnit:IsInZone( self.Zone ) end - + --- Task Events - + --- StateMachine callback function -- @param #ACT_ROUTE_ZONE self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -470,7 +470,7 @@ do -- ACT_ROUTE_ZONE -- @param #string To function ACT_ROUTE_ZONE:onafterReport( ProcessUnit, From, Event, To ) self:F( { ProcessUnit = ProcessUnit } ) - + local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", " .. self:GetRouteText( ProcessUnit ) self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Update ) end From c30ca2b18a71e9f671cd055457ad187c4f8f6c16 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 16 May 2020 13:09:04 +0200 Subject: [PATCH 485/485] Fixes RADIOQUEUE - Fix for issue #1321 USERFLAG - Added delay parameter CLEANUP - Fixes iniunit nil #1324 WAREHOUSE v1.0.2 - Fixes #1322 --- Moose Development/Moose/Core/Point.lua | 41 +++++++++++++++++++ Moose Development/Moose/Core/RadioQueue.lua | 2 +- Moose Development/Moose/Core/UserFlag.lua | 9 +++- .../Moose/Functional/CleanUp.lua | 2 +- .../Moose/Functional/Warehouse.lua | 4 +- .../Moose/Wrapper/Positionable.lua | 20 +++++++++ 6 files changed, 72 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 65022dec4..5139d50a2 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -446,6 +446,47 @@ do -- COORDINATE return gotunits, gotstatics, gotscenery, Units, Statics, Scenery end + + --- Scan/find UNITS 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. + -- @return Core.Set#SET_UNIT Set of units. + function COORDINATE:ScanUnits(radius) + + local _,_,_,units=self:ScanObjects(radius, true, false, false) + + local set=SET_UNIT:New() + + for _,unit in pairs(units) do + set:AddUnit(unit) + end + + return set + end + + --- Find the closest unit to the COORDINATE within a certain radius. + -- @param #COORDINATE self + -- @param #number radius Scan radius in meters. Default 100 m. + -- @return Wrapper.Unit#UNIT The closest unit or #nil if no unit is inside the given radius. + function COORDINATE:FindClosestUnit(radius) + + local units=self:ScanUnits(radius) + + local umin=nil --Wrapper.Unit#UNIT + local dmin=math.huge + for _,_unit in pairs(units.Set) do + local unit=_unit --Wrapper.Unit#UNIT + local coordinate=unit:GetCoordinate() + local d=self:Get2DDistance(coordinate) + if d0 then + self:ScheduleOnce(Delay, USERFLAG.Set, self, Number) + else + trigger.action.setUserFlag( self.UserFlagName, Number ) + end return self end diff --git a/Moose Development/Moose/Functional/CleanUp.lua b/Moose Development/Moose/Functional/CleanUp.lua index daed34b02..4d87be292 100644 --- a/Moose Development/Moose/Functional/CleanUp.lua +++ b/Moose Development/Moose/Functional/CleanUp.lua @@ -245,7 +245,7 @@ end function CLEANUP_AIRBASE.__:OnEventBirth( EventData ) self:F( { EventData } ) - if EventData.IniUnit:IsAlive() ~= nil then + if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() ~= nil then if self:IsInAirbase( EventData.IniUnit:GetVec2() ) then self.CleanUpList[EventData.IniDCSUnitName] = {} self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnit = EventData.IniUnit diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 433e3f178..06b8c67c4 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1764,7 +1764,7 @@ _WAREHOUSEDB = { --- Warehouse class version. -- @field #string version -WAREHOUSE.version="1.0.1" +WAREHOUSE.version="1.0.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -8404,7 +8404,7 @@ end -- @return #string Text about warehouse stock function WAREHOUSE:_UpdateWarehouseMarkText() - if self.makerOn then + if self.markerOn then -- Create a mark with the current assets in stock. if self.markerid~=nil then diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index db8283a64..be85454a3 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -602,6 +602,26 @@ function POSITIONABLE:IsGround() end +--- Returns if the unit is of ship category. +-- @param #POSITIONABLE self +-- @return #boolean Ship category evaluation result. +function POSITIONABLE:IsShip() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + + local IsShip = ( UnitDescriptor.category == Unit.Category.SHIP ) + + return IsShip + end + + return nil +end + + --- Returns true if the POSITIONABLE is in the air. -- Polymorphic, is overridden in GROUP and UNIT. -- @param Wrapper.Positionable#POSITIONABLE self