From ae754fe334304874e077c805b25cf859f7d67729 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 29 Oct 2018 00:01:53 +0100 Subject: [PATCH 01/95] 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 02/95] 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 03/95] 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 04/95] 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 05/95] 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 06/95] 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 07/95] 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 08/95] 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 09/95] 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 10/95] 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 11/95] 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 12/95] 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 13/95] 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 b21bb3d433e8161f85d6c9c5bff0d9bde1bb36fb Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 7 Nov 2018 16:08:13 +0100 Subject: [PATCH 14/95] 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 15/95] 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 b53b1edd29335c8d5819032619d420ca52360d02 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 8 Nov 2018 16:04:51 +0100 Subject: [PATCH 16/95] 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 17/95] 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 4542c96eaee78cbf0e4fe382bbd5990e2d93ee44 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 9 Nov 2018 16:07:30 +0100 Subject: [PATCH 18/95] 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 cf4be99093accaef2b0f1b77542c09ca40fef95b Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 10 Nov 2018 00:57:14 +0100 Subject: [PATCH 19/95] 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 2d7d19880f5106fc5f0b2915a44291c6e6155a3b Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 11 Nov 2018 23:55:19 +0100 Subject: [PATCH 20/95] 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 e3121781d0d99b34a38628662d36c7194cb9bd6d Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Nov 2018 00:00:20 +0100 Subject: [PATCH 21/95] 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 22/95] 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 fa0535288232bb5dc19bc7b2fb0ff272aa442a06 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Nov 2018 23:39:29 +0100 Subject: [PATCH 23/95] 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 24/95] 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 25/95] 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 26/95] 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 27/95] 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 28/95] 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 29/95] 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 30/95] 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 31/95] 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: Wed, 21 Nov 2018 00:55:00 +0100 Subject: [PATCH 32/95] 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 33/95] 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 34/95] 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 35/95] 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 36/95] 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 37/95] 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 38/95] 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 39/95] 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 40/95] 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 41/95] 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 42/95] 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 43/95] 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 44/95] 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 45/95] 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 46/95] 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 47/95] 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 48/95] 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 49/95] 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 50/95] 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 51/95] 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 52/95] 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 53/95] 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 54/95] 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 55/95] 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 56/95] 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 57/95] 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 58/95] 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 59/95] 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 60/95] 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 61/95] 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 62/95] 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 63/95] 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 64/95] 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 65/95] 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 66/95] 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 67/95] 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 68/95] 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 69/95] 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 70/95] 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 71/95] 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 72/95] 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 73/95] 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 74/95] 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 75/95] 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 d607b960217f72c444e2f93ae666274875f862b9 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 12 Dec 2018 00:50:42 +0100 Subject: [PATCH 76/95] 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 8969c58ca5ed8878159a397295d0fe11ab5b383a Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 12 Dec 2018 16:03:37 +0100 Subject: [PATCH 77/95] 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 78/95] 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 79/95] 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 80/95] 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 81/95] 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 82/95] 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 83/95] 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 84/95] 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 75ac76a8e5c664995982ed903bd3a52537b8bef7 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 18 Dec 2018 14:04:31 +0100 Subject: [PATCH 85/95] 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 86/95] 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 fd6a31992852d8d986ae01238ed99784bd9d5dac Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 19 Dec 2018 17:02:58 +0100 Subject: [PATCH 87/95] 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 88/95] 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 89/95] 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 90/95] 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 91/95] 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 92/95] 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 93/95] 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 94/95] 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 95/95] 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)