funkyfranky 5a29b272dc First implementation of "air" start.
Added possibility to spawn in zones.
Other improvements and fixes.
2017-08-28 23:51:54 +02:00

1255 lines
45 KiB
Lua

--- ** AI **
-- @module AI_RAT
--- Some ID to identify where we are
-- #string myid
myid="RAT | "
--- RAT class
-- @type RAT
-- @field ClassName
-- @field #string prefix
-- @field #RAT
-- @extends #SPAWN
--- Table of RAT
-- @field #RAT RAT
-- @param #string ClassName Name of class.
RAT={
ClassName = "RAT", -- Name of class: RAT = Random Air Traffic.
debug=true, -- Turn debug messages on or off.
prefix=nil, -- Prefix of the template group defined in the mission editor.
spawndelay=5, -- Delay time in seconds before first spawning happens.
spawninterval=2, -- Interval between spawning units/groups. Note that we add a randomization of 10%.
coalition = nil, -- Coalition of spawn group template.
category = nil, -- Category of aircarft: "plane" or "heli".
friendly = "same", -- Possible departure/destination airport: all=blue+red+neutral, same=spawn+neutral, spawnonly=spawn, blue=blue+neutral, blueonly=blue, red=red+neutral, redonly=red.
ctable = {}, -- Table with the valid coalitons from choice self.friendly.
aircraft = {}, -- Table which holds the basic aircraft properties (speed, range, ...).
Vcruisemax=250, -- Max cruise speed in m/s (250 m/s = 900 km/h = 486 kt)
Vclimb=1500, -- Default climb rate in ft/min.
AlphaDescent=3.6, -- Default angle of descenti in degrees. A value of 3.6 follows the 3:1 rule of 3 miles of travel and 1000 ft descent.
FLcruise=200, -- Default cruise flight level in. FL200 = 20000 ft.
roe = "hold", -- ROE of spawned groups, default is weapon hold (this is a peaceful class for civil aircraft or ferry missions).
takeoff = "hot", -- Takeoff type: "hot", "cold", "runway", "air", "random".
mindist = 10000, -- Min distance from departure to destination in meters.
airports_map={}, -- All airports available on current map (Caucasus, Nevada, Normandy, ...).
airports={}, -- All airports of friedly coalitions.
airports_departure={}, -- Possible departure airports if unit/group is spawned at airport, spawnpoint=air or spawnpoint=airport.
airports_destination={}, -- Possible destination airports if unit does not fly "overseas", destpoint=overseas or destpoint=airport.
departure_name="random", -- Name of the departure airport. Default is "random" for a randomly chosen one of the coalition airports.
destination_name="random",-- Name of the destination airport. Default is "random" for a randomly chosen one of the coalition airports.
zones_departure={}, -- Departure zones for air start.
Rzone=5000, -- Radius of departure zones in meters.
ratcraft={}, -- Array with the spawned RAT aircraft.
}
--TODO list:
--TODO: Add scheduled spawn and corresponding user functions.
--TODO: Add possibility to spawn in air.
--TODO: Add from zones and to zones for aircraft to fly from/to.
--TODO: Make more functions to adjust/set parameters.
--TODO: Clean up debug messages.
--DONE: Improve flight plan. Especially check FL against route length.
--TODO: Integrate RAT:DB? No, not now.
--DONE: Add event handlers.
--DONE: Respawn units when they have landed.
--DONE: Change ROE state.
--TODO: Make ROE state user function
--TODO: Add status reports.
--TODO: Check compatibility with #SPAWN functions.
--TODO: Add possibility to continue journey at destination. Need "place" in event data for that.
--DONE: Check that FARPS are not used as airbases for planes. Don't know if they appear in list of airports.
--DONE: Add cases for helicopters.
--TODO: Lots of other little things...
--- Creates a new RAT object.
-- @param #RAT self
-- @param #string prefix Prefix of the (template) group name defined in the mission editor.
-- @param #string friendly Friendly coalitions from which airports can be used.
-- "all"=neutral+red+blue, "same"=spawn coalition+neutral, "sameonly"=spawn coalition, "blue"=blue+neutral, "blueonly"=blue, "red"=red+neutral, "redonly"=red, "neutral"=neutral.
-- Default is "same", so aircraft will use airports of the coalition their spawn template has plus all neutral airports.
-- @return #RAT self Object of RAT class.
-- @return #nil Nil if the group does not exists in the mission editor.
function RAT:New(prefix, friendly)
-- Inherit SPAWN clase.
local self=BASE:Inherit(self, SPAWN:New(prefix)) -- #RAT
-- Set prefix.
--TODO: Replace this by SpawnTemplatePrefix.
self.prefix=prefix
-- Set friendly coalitions. Default is "same", i.e. same coalition as template group plus neutrals.
self.friendly = friendly or "same"
-- Get template group defined in the mission editor.
local DCSgroup=Group.getByName(prefix)
-- Check the group actually exists.
if DCSgroup==nil then
error("Group with name "..prefix.." does not exist in the mission editor!")
return nil
end
-- Set own coalition.
self.coalition=DCSgroup:getCoalition()
-- Initialize aircraft parameters based on ME group template.
self:_InitAircraft(DCSgroup)
-- Get all airports of current map (Caucasus, NTTR, Normandy, ...).
self:_GetAirportsOfMap()
-- Set the coalition table based on choice of self.coalition and self.friendly.
self:_SetCoalitionTable()
-- Get all airports of this map beloning to friendly coalition(s).
self:_GetAirportsOfCoalition()
return self
end
--- Initialize basic parameters of the aircraft based on its (template) group in the mission editor.
-- @param #RAT self
-- @param Dcs.DCSWrapper.Group#Group DCSgroup Group of the aircraft in the mission editor.
function RAT:_InitAircraft(DCSgroup)
local DCSunit=DCSgroup:getUnit(1)
local DCSdesc=DCSunit:getDesc()
local DCScategory=DCSgroup:getCategory()
local DCStype=DCSunit:getTypeName()
-- Ddescriptors table of unit.
if self.debug then
self:E({"DCSdesc", DCSdesc})
end
-- unit conversions
local ft2meter=0.305
local kmh2ms=0.278
local FL2m=30.48
local nm2km=1.852
local nm2m=1852
-- set category
if DCScategory==Group.Category.AIRPLANE then
self.category="plane"
elseif DCScategory==Group.Category.HELICOPTER then
self.category="heli"
else
self.category="other"
error(myid.."Group of RAT is neither airplane nor helicopter!")
end
-- Define a first departure zone around the point where the group template in the ME was placed.
local ZoneTemplate = ZONE_GROUP:New( "Template", GROUP:FindByName(self.prefix), self.Rzone)
table.insert(self.zones_departure, ZoneTemplate)
-- Get type of aircraft.
self.aircraft.type=DCStype
-- inital fuel in %
self.aircraft.fuel=DCSunit:getFuel()
-- operational range in NM converted to m
self.aircraft.Rmax = DCSdesc.range*nm2m
-- effective range taking fuel into accound and a 10% reserve
self.aircraft.Reff = self.aircraft.Rmax*self.aircraft.fuel*0.9
-- max airspeed from group
self.aircraft.Vmax = DCSdesc.speedMax
-- min cruise airspeed = 75% of max
self.aircraft.Vmin = self.aircraft.Vmax*0.75
-- actual travel speed (random between ASmin and ASmax)
--TODO: This needs to be placed somewhere else! Randomization should not happen here.
self.aircraft.Vcruise = math.random(self.aircraft.Vmin, self.aircraft.Vmax)
-- limit travel speed to ~900 km/h
self.aircraft.Vcruise = math.min(self.aircraft.Vcruise, self.Vcruisemax)
-- max climb speed in m/s
self.aircraft.Vymax=DCSdesc.VyMax
-- Reasonably civil climb speed Vy=1500 ft/min but max aircraft specific climb rate.
self.aircraft.Vclimb=math.min(self.Vclimb*ft2meter/60, self.aircraft.Vymax)
-- Climb angle in rad.
self.aircraft.AlphaClimb=math.asin(self.aircraft.Vclimb/self.aircraft.Vmax)
-- Descent angle in rad.
self.aircraft.AlphaDescent=math.rad(self.AlphaDescent)
-- service ceiling in meters
self.aircraft.ceiling=DCSdesc.Hmax
-- default flight levels (ASL)
if self.category=="plane" then
self.aircraft.FLcruise=self.FLcruise*FL2m
else
--TODO: need to check these parameters for helos: FL005 = 152 m
self.aircraft.FLcruise=005*FL2m
end
-- send debug message
local text=string.format("Aircraft parameters:\n")
text=text..string.format("Category = %s\n", self.category)
text=text..string.format("Max speed = %6.1f m/s.\n", self.aircraft.Vmax)
text=text..string.format("Max cruise speed = %6.1f m/s.\n", self.aircraft.Vcruise)
text=text..string.format("Max climb speed = %6.1f m/s.\n", self.aircraft.Vymax)
text=text..string.format("Climb speed = %6.1f m/s.\n", self.aircraft.Vclimb)
text=text..string.format("Angle of climb = %6.1f Deg.\n", math.deg(self.aircraft.AlphaClimb))
text=text..string.format("Angle of descent = %6.1f Deg.\n", math.deg(self.aircraft.AlphaDescent))
text=text..string.format("Initial Fuel = %6.1f.\n", self.aircraft.fuel*100)
text=text..string.format("Max range = %6.1f km.\n", self.aircraft.Rmax/1000)
text=text..string.format("Eff range = %6.1f km.\n", self.aircraft.Reff/1000)
text=text..string.format("Ceiling = FL%3.0f = %6.1f km.\n", self.aircraft.ceiling/FL2m, self.aircraft.ceiling/1000)
text=text..string.format("FL cruise = FL%3.0f = %6.1f km.", self.aircraft.FLcruise/FL2m, self.aircraft.FLcruise/1000)
env.info(myid..text)
if self.debug then
MESSAGE:New(text, 60):ToAll()
end
end
--- Spawn the AI aircraft.
-- @param #RAT self
-- @param #number naircraft (Optional) Number of aircraft to spawn. Default is one aircraft.
-- @param #string name (Optional) Name of the spawn group (for debugging only).
function RAT:Spawn(naircraft, name)
-- Number of aircraft to spawn. Default is one.
naircraft=naircraft or 1
-- some of group for debugging
--TODO: remove name from input parameter and make better unique RAT AI name
name=name or "RAT AI "..self.aircraft.type
-- debug message
local text="Spawning "..naircraft.." aircraft of group "..self.prefix.." with name "..name.." of type "..self.aircraft.type..".\n"
text=text.."Takeoff type: "..self.takeoff.."\n"
text=text.."Friendly airports: "..self.friendly
env.info(myid..text)
if self.debug then
MESSAGE:New(text, 60, "Info"):ToAll()
end
-- Schedule spawning of aircraft.
--TODO: make self.SpawnInterval and sef.spawndelay user input
local Tstart=self.spawndelay
local dt=self.spawninterval
if self.takeoff:lower()=="takeoff-runway" or self.takeoff:lower()=="runway" then
-- Ensure that interval is >= 180 seconds if spawn at runway is chosen. Aircraft need time to takeoff or the runway gets jammed.
dt=math.max(dt, 180)
end
local Tstop=Tstart+dt*(naircraft-1)
SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.1, Tstop)
-- Status report scheduler.
SCHEDULER:New(nil, self.Status, {self}, 30, 10, 0.0)
end
--- Spawn the AI aircraft with a route.
-- @param #RAT self
function RAT:_SpawnWithRoute()
-- Set flight plan.
local departure, destination, waypoints = self:_SetRoute()
-- Modify the spawn template to follow the flight plan.
self:_ModifySpawnTemplate(waypoints)
-- Actually spawn the group.
local group=self:SpawnWithIndex(self.SpawnIndex) -- Core.Group#GROUP
-- set ROE to "weapon hold" and ROT to "no reaction"
-- TODO: make user function to set this
group:OptionROEReturnFire()
--group:OptionROEHoldFire()
group:OptionROTNoReaction()
--group:OptionROTPassiveDefense()
self.ratcraft[self.SpawnIndex]={}
self.ratcraft[self.SpawnIndex]["group"]=group
self.ratcraft[self.SpawnIndex]["destination"]=destination
self.ratcraft[self.SpawnIndex]["departure"]=departure
self.ratcraft[self.SpawnIndex]["status"]="spawned"
-- Handle events.
-- TODO: add hit event?
self:HandleEvent(EVENTS.Birth, self._OnBirthDay)
self:HandleEvent(EVENTS.EngineStartup, self._EngineStartup)
self:HandleEvent(EVENTS.Takeoff, self._OnTakeoff)
self:HandleEvent(EVENTS.Land, self._OnLand)
self:HandleEvent(EVENTS.EngineShutdown, self._OnEngineShutdown)
self:HandleEvent(EVENTS.Dead, self._OnDead)
self:HandleEvent(EVENTS.Crash, self._OnCrash)
end
--- Report status of RAT groups.
-- @param #RAT self
function RAT:Status()
local ngroups=#self.SpawnGroups
MESSAGE:New("Number of groups spawned = "..ngroups, 60):ToAll()
for i=1, ngroups do
local group=self.SpawnGroups[i].Group
local prefix=self:_GetPrefixFromGroup(group)
local life=self:_GetLife(group)
local text=string.format("Group %s ID %i:", prefix, i)
text=text..string.format("Life = %3.0f\n", life)
text=text..string.format("Status = %s\n", self.ratcraft[i].status)
text=text..string.format("Flying from %s to %s.",self.ratcraft[i].departure:GetName(), self.ratcraft[i].destination:GetName())
MESSAGE:New(text, 60):ToAll()
env.info(myid..text)
end
end
--- Get (relative) life of unit.
-- @param #RAT self
-- @return #number Life of unit in percent.
function RAT:_GetLife(group)
local life=0.0
if group then
local unit=group:GetUnit(1)
if unit then
life=unit:GetLife()/unit:GetLife0()*100
else
error(myid.."Unit does not exists in RAT_Getlife(). Returning zero.")
end
else
error(myid.."Group does not exists in RAT_Getlife(). Returning zero.")
end
return life
end
--- Set status of group.
-- @param #RAT self
function RAT:_SetStatus(group, status)
local index=self:GetSpawnIndexFromGroup(group)
env.info(myid.."Index for group "..group:GetName().." "..index.." status: "..status)
self.ratcraft[index].status=status
end
--- Function is executed when a unit is spawned.
-- @param #RAT self
function RAT:_OnBirthDay(EventData)
env.info(myid.."It's a birthday")
local SpawnGroup = EventData.IniGroup
if SpawnGroup then
local index=self:GetSpawnIndexFromGroup(SpawnGroup)
local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup)
local text="Event: Group "..SpawnGroup:GetName().." was born."
env.info(myid..text)
--MESSAGE:New(text, 180):ToAll()
self:_SetStatus(SpawnGroup, "starting engines (born)")
else
error("Group does not exist in RAT:_EngineStartup().")
end
end
--- Function is executed when a unit starts its engines.
-- @param #RAT self
function RAT:_EngineStartup(EventData)
local SpawnGroup = EventData.IniGroup
if SpawnGroup then
local text="Event: Group "..SpawnGroup:GetName().." started engines. Life="..self:_GetLife(SpawnGroup)
env.info(myid..text)
--MESSAGE:New(text, 180):ToAll()
self:_SetStatus(SpawnGroup, "taxi (engines started)")
else
error("Group does not exist in RAT:_EngineStartup().")
end
end
--- Function is executed when a unit takes off.
-- @param #RAT self
function RAT:_OnTakeoff(EventData)
local SpawnGroup = EventData.IniGroup
if SpawnGroup then
local text="Event: Group "..SpawnGroup:GetName().." took off. Life="..self:_GetLife(SpawnGroup)
env.info(myid..text)
--MESSAGE:New(text, 180):ToAll()
self:_SetStatus(SpawnGroup, "airborn (took off)")
else
error("Group does not exist in RAT:_OnTakeoff().")
end
end
--- Function is executed when a unit lands.
-- @param #RAT self
function RAT:_OnLand(EventData)
local SpawnGroup = EventData.IniGroup
if SpawnGroup then
local text="Event: Group "..SpawnGroup:GetName().." landed. Life="..self:_GetLife(SpawnGroup)
env.info(myid..text)
--MESSAGE:New(text, 180):ToAll()
self:_SetStatus(SpawnGroup, "landed")
text="Event: Group "..SpawnGroup:GetName().." will be respawned."
env.info(myid..text)
--MESSAGE:New(text, 180):ToAll()
self:_SpawnWithRoute()
else
error("Group does not exist in RAT:_OnLand().")
end
end
--- Function is executed when a unit shuts down its engines.
-- @param #RAT self
function RAT:_OnEngineShutdown(EventData)
local SpawnGroup = EventData.IniGroup
if SpawnGroup then
local text="Event: Group "..SpawnGroup:GetName().." shut down its engines. Life="..self:_GetLife(SpawnGroup)
env.info(myid..text)
--MESSAGE:New(text, 180):ToAll()
self:_SetStatus(SpawnGroup, "arrived (engines shut down)")
text="Event: Group "..SpawnGroup:GetName().." will be destroyed now."
env.info(myid..text)
--MESSAGE:New(text, 180):ToAll()
SpawnGroup:Destroy()
else
error("Group does not exist in RAT:_OnEngineShutdown().")
end
end
--- Function is executed when a unit is dead.
-- @param #RAT self
function RAT:_OnDead(EventData)
local SpawnGroup = EventData.IniGroup
if SpawnGroup then
local text="Event: Group "..SpawnGroup:GetName().." was died. Life="..self:_GetLife(SpawnGroup)
env.info(myid..text)
self:_SetStatus(SpawnGroup, "dead (died)")
--MESSAGE:New(text, 180):ToAll()
else
error("Group does not exist in RAT:_OnDead().")
end
end
--- Function is executed when a unit crashes.
-- @param #RAT self
function RAT:_OnCrash(EventData)
local SpawnGroup = EventData.IniGroup
if SpawnGroup then
local text="Event: Group "..SpawnGroup:GetName().." crashed. Life="..self:_GetLife(SpawnGroup)
env.info(myid..text)
--MESSAGE:New(text, 180):ToAll()
self:_SetStatus(SpawnGroup, "crashed")
--TODO: maybe spawn some people at the crash site and send a distress call. And define them as cargo which can be rescued.
else
error("Group does not exist in RAT:_OnCrash().")
end
end
--- Set name of departure airport for the AI aircraft. If no name is given an airport from the coalition is chosen randomly.
-- @param #RAT self
-- @param #string name Name of the departure airport or "random" for a randomly chosen one of the coalition.
function RAT:SetDeparture(name)
if name and AIRBASE:FindByName(name) then
self.departure_name=name
else
self.departure_name="random"
end
end
--- Set names of departure zones for spawning the AI aircraft.
-- @param #RAT self
-- @param #table zonenames Table of zone names where spawning should happen.
function RAT:SetDepartureZones(zonenames)
self.zones_departure={}
local z
for _,name in pairs(zonenames) do
if name:lower()=="zone template" then
z=ZONE_GROUP:New("Zone Template", GROUP:FindByName(self.prefix), self.Rzone)
else
z=ZONE:New(name)
end
if z then
table.insert(self.zones_departure, z)
else
error(myid.."A zone with name "..name.." does not exist!")
end
end
end
--- Set name of destination airport for the AI aircraft. If no name is given an airport from the coalition is chosen randomly.
-- @param #RAT self
-- @param #string name Name of the destination airport or "random" for a randomly chosen one of the coalition.
function RAT:SetDestination(name)
if name and AIRBASE:FindByName(name) then
self.destination_name=name
else
self.destination_name="random"
end
end
--- Set the departure airport of the AI. If no airport name is given an airport from the coalition is chosen randomly.
-- @param #RAT self
-- @return Wrapper.Airbase#AIRBASE Departure airport if spawning at airport.
function RAT:_SetDeparture()
local departure
local text
if self.takeoff=="air" then
if self.departure_name=="random" then
departure=self.zones_departure[math.random(1, #self.zones_departure)]
else
departure=ZONE:FindByName(self.departure_name)
end
text="Chosen departure zone: "..departure:GetName()
else
if self.departure_name=="random" then
-- Get a random departure airport from all friendly coalition airports.
departure=self.airports[math.random(1, #self.airports)]
elseif AIRBASE:FindByName(self.departure_name) then
-- Take the explicit airport provided.
departure=AIRBASE:FindByName(self.departure_name)
else
-- If nothing else works, we randomly choose from frindly coalition airports.
departure=self.airports[math.random(1, #self.airports)]
end
text="Chosen departure airport: "..departure:GetName().." with ID "..departure:GetID()
end
env.info(myid..text)
MESSAGE:New(text, 60):ToAll()
return departure
end
--- Set the destination airport of the AI. If no airport name is given an airport from the coalition is chosen randomly.
-- @param #RAT self
-- @return Wrapper.Airbase#AIRBASE Destination airport.
function RAT:_SetDestination()
local destination -- Wrapper.Airbase#AIRBASE
if self.destination_name=="random" then
-- Get random destination from all friendly airports within range.
destination=self.airports_destination[math.random(1, #self.airports_destination)]
elseif self.destination_name and AIRBASE:FindByName(self.destination_name) then
-- Take the explicit airport provided.
destination=AIRBASE:FindByName(self.destination_name)
else
-- If nothing else works, we randomly choose from frindly coalition airports.
destination=self.airports_destination[math.random(1, #self.airports_destination)]
end
local text="Chosen destination airport: "..destination:GetName().." with ID "..destination:GetID()
self:E(destination:GetDesc())
env.info(myid..text)
MESSAGE:New(text, 60):ToAll()
return destination
end
--- Get all possible destination airports depending on departure position.
-- The list is sorted w.r.t. distance to departure position.
-- @param #RAT self
-- @param Core.Point#COORDINATE q Coordinate of the departure point.
-- @param #number minrange Minimum range to q in meters.
-- @param #number maxrange Maximum range to q in meters.
function RAT:_GetDestinations(q, minrange, maxrange)
local absolutemin=10000 -- Absolute minimum is 10 km.
minrange=minrange or absolutemin -- Default min is absolute min.
maxrange=maxrange or 10000000 -- Default max 10,000 km.
-- Ensure that minrange is always > 10 km to ensure the destination != departure.
minrange=math.max(absolutemin, minrange)
-- loop over all friendly airports
for _,airport in pairs(self.airports) do
local p=airport:GetCoordinate()
local distance=q:Get2DDistance(p)
-- check if distance form departure to destination is within min/max range
if distance>=minrange and distance<=maxrange then
table.insert(self.airports_destination, airport)
end
end
env.info(myid.."Number of possible destination airports = "..#self.airports_destination)
if #self.airports_destination > 1 then
--- Compare distance of destination airports.
-- @param Core.Point#COORDINATE a Coordinate of point a.
-- @param Core.Point#COORDINATE b Coordinate of point b.
-- @return #list Table sorted by distance.
local function compare(a,b)
local qa=q:Get2DDistance(a:GetCoordinate())
local qb=q:Get2DDistance(b:GetCoordinate())
return qa < qb
end
table.sort(self.airports_destination, compare)
end
end
--- Get all airports of the current map.
-- @param #RAT self
function RAT:_GetAirportsOfMap()
local _coalition
for i=0,2 do -- cycle coalition.side 0=NEUTRAL, 1=RED, 2=BLUE
-- set coalition
if i==0 then
_coalition=coalition.side.NEUTRAL
elseif i==1 then
_coalition=coalition.side.RED
elseif i==2 then
_coalition=coalition.side.BLUE
end
-- get airbases of coalition
local ab=coalition.getAirbases(i)
-- loop over airbases and put them in a table
for _,airbase in pairs(ab) do -- loop over airbases
local _id=airbase:getID()
local _p=airbase:getPosition().p
local _name=airbase:getName()
local _myab=AIRBASE:FindByName(_name)
local text="Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName()
env.info(myid..text)
table.insert(self.airports_map, _myab)
end
end
end
--- Get all "friendly" airports of the current map.
-- @param #RAT self
function RAT:_GetAirportsOfCoalition()
for _,coalition in pairs(self.ctable) do
for _,airport in pairs(self.airports_map) do
if airport:GetCoalition()==coalition then
airport:GetTypeName()
-- remember that planes cannot land on FARPs.
if not (self.category=="plane" and airport:GetTypeName()=="FARP") then
table.insert(self.airports, airport)
end
end
end
end
if #self.airports==0 then
local text="No possible departure/destination airports found!"
MESSAGE:New(text, 180):ToAll()
error(myid..text)
end
end
--- Create a waypoint that can be used with the Route command.
-- @param #RAT self
-- @param #string Type Type of waypoint. takeoff-cold, takeoff-hot, takeoff-runway, climb, cruise, descent, holding, land, landing.
-- @param Core.Point#COORDINATE Coord 3D coordinate of the waypoint.
-- @param #number Speed Speed in m/s.
-- @param #number Altitude Altitude in m.
-- @param Wrapper.Airbase#AIRBASE Airport Airport of object to spawn.
-- @return #table Waypoints for DCS task route or spawn template.
function RAT:Waypoint(Type, Coord, Speed, Altitude, Airport)
-- set altitude to input parameter or take y of 3D-coordinate
local _Altitude=Altitude or Coord.y
--TODO: _Type should be generalized to Grouptemplate.Type
--TODO: Only use _alttype="BARO" and add landheight for _alttype="RADIO". Don't know if "BARO" really works atm.
-- Land height at given coordinate.
local Hland=Coord:GetLandHeight()
-- convert type and action in DCS format
local _Type=nil
local _Action=nil
local _alttype="RADIO"
local _AID=nil
if Type:lower()=="takeoff-cold" or Type:lower()=="cold" then
-- take-off with engine off
_Type="TakeOffParking"
_Action="From Parking Area"
_Altitude = 2
_alttype="RADIO"
_AID = Airport:GetID()
elseif Type:lower()=="takeoff-hot" or Type:lower()=="hot" then
-- take-off with engine on
_Type="TakeOffParkingHot"
_Action="From Parking Area"
_Altitude = 2
_alttype="RADIO"
_AID = Airport:GetID()
elseif Type:lower()=="takeoff-runway" or Type:lower()=="runway" then
-- take-off from runway
_Type="TakeOff"
_Action="From Parking Area" --TODO: Is this correct for a runway start?
_Altitude = 2
_alttype="RADIO"
_AID = Airport:GetID()
elseif Type:lower()=="air" then
-- air start
_Type="Turning Point"
_Action="Turning Point"
_Altitude = 5000
_alttype="BARO"
elseif Type:lower()=="climb" or Type:lower()=="cruise" then
_Type="Turning Point"
_Action="Turning Point"
_alttype="BARO"
elseif Type:lower()=="descent" then
_Type="Turning Point"
_Action="Fly Over Point"
_alttype="RADIO"
elseif Type:lower()=="holding" then
_Type="Turning Point"
_Action="Fly Over Point"
_alttype="RADIO"
elseif Type:lower()=="landing" or Type:lower()=="land" then
_Type="Land"
_Action="Landing"
_Altitude = 2
_alttype="RADIO"
_AID = Airport:GetID()
else
error("Unknown waypoint type in RAT:Waypoint function!")
_Type="Turning Point"
_Action="Turning Point"
_alttype="RADIO"
end
-- some debug info about input parameters
if self.debug then
local at="unknown (oops!)"
if _alttype=="BARO" then
at="ASL"
elseif _alttype=="RADIO" then
at="AGL"
end
local text=string.format("\nType: %s.\n", Type)
if _Action then
text=text..string.format("Action: %s.\n", tostring(_Action))
end
text=text..string.format("Coord: x = %6.1f km, y = %6.1f km, alt = %6.1f m.\n", Coord.x/1000, Coord.z/1000, Coord.y)
text=text..string.format("Speed = %6.1f m/s = %6.1f km/h = %6.1f knots.\n", Speed, Speed*3.6, Speed*1.94384)
text=text..string.format("Altitude = %6.1f m "..at..".\n", _Altitude)
if Airport then
if Type:lower() == "air" then
text=text..string.format("Zone = %s.", Airport:GetName())
else
text=text..string.format("Airport = %s with ID %i.", Airport:GetName(), Airport:GetID())
end
else
text=text..string.format("No (valid) airport specified.")
end
local debugmessage=false
if debugmessage then
MESSAGE:New(text, 30, "RAT Waypoint Debug"):ToAll()
end
env.info(myid..text)
end
-- define waypoint
local RoutePoint = {}
-- coordinates and altitude
RoutePoint.x = Coord.x
RoutePoint.y = Coord.z
RoutePoint.alt = _Altitude
-- altitude type: BARO=ASL or RADIO=AGL
RoutePoint.alt_type = _alttype
-- type
RoutePoint.type = _Type or nil
RoutePoint.action = _Action or nil
-- speed in m/s
RoutePoint.speed = Speed
RoutePoint.speed_locked = true
-- ETA (not used)
--TODO: ETA check if this makes the DCS bug go away
--RoutePoint.ETA=nil
RoutePoint.ETA_locked=true
-- waypoint name (only for the mission editor)
RoutePoint.name="RAT waypoint"
if _AID then
RoutePoint.airdromeId=_AID
end
-- properties
RoutePoint.properties = {
["vnav"] = 1,
["scale"] = 0,
["angle"] = 0,
["vangle"] = 0,
["steer"] = 2,
}
-- task
if Type:lower()=="holding" then
RoutePoint.task=self:_TaskHolding({x=Coord.x, y=Coord.z}, Altitude, Speed)
else
RoutePoint.task = {}
RoutePoint.task.id = "ComboTask"
RoutePoint.task.params = {}
RoutePoint.task.params.tasks = {}
end
-- return the waypoint
return RoutePoint
end
--- Set takeoff type. Starting cold at airport, starting hot at airport, starting at runway, starting in the air or randomly select one of the previous.
-- Default is "takeoff-hot" for a start at airport with engines already running.
-- @param #RAT self
-- @param #string type Type can be "takeoff-cold" or "cold", "takeoff-hot" or "hot", "takeoff-runway" or "runway", "air", "random".
function RAT:SetTakeoff(type)
-- All possible types for random selection.
local types={"takeoff-cold", "takeoff-hot", "takeoff-runway"}
local _Type
if type:lower()=="takeoff-cold" or type:lower()=="cold" then
_Type="takeoff-cold"
elseif type:lower()=="takeoff-hot" or type:lower()=="hot" then
_Type="takeoff-hot"
elseif type:lower()=="takeoff-runway" or type:lower()=="runway" then
_Type="takeoff-runway"
elseif type:lower()=="air" then
--TODO: not implemented yet
_Type="air"
elseif type:lower()=="random" then
_Type=types[math.random(1, #types)]
else
_Type="takeoff-hot"
end
self.takeoff=_Type
end
--- Orbit at a specified position at a specified alititude with a specified speed.
-- @param #RAT self
-- @param Dcs.DCSTypes#Vec2 P1 The point to hold the position.
-- @param #number Altitude The altitude AGL to hold the position.
-- @param #number Speed The speed flying when holding the position in m/s.
-- @return Dcs.DCSTasking.Task#Task
function RAT:_TaskHolding(P1, Altitude, Speed)
local LandHeight = land.getHeight(P1)
--TODO: Add duration of holding. Otherwise it will hold until fuel is emtpy.
-- second point is 10 km north of P1
--TODO: randomize P1
local P2={}
P2.x=P1.x
P2.y=P1.y+10000
local DCSTask = {
id = 'Orbit',
params = {
pattern = AI.Task.OrbitPattern.RACE_TRACK,
point = P1,
point2 = P2,
speed = Speed,
altitude = Altitude + LandHeight
}
}
return DCSTask
end
--- Provide information about the assigned flightplan.
-- @param #RAT self
-- @param #list waypoints Waypoints of the flight plan.
-- @param #string comment Some comment to identify the provided information.
-- @return #number total Total route length in meters.
function RAT:_Routeinfo(waypoints, comment)
local text=""
if comment then
text=comment.."\n"
end
text=text..string.format("Number of waypoints = %i\n", #waypoints)
-- info on coordinate and altitude
for i=1,#waypoints do
local p=waypoints[i]
text=text..string.format("WP #%i: x = %6.1f km, y = %6.1f km, alt = %6.1f m\n", i-1, p.x/1000, p.y/1000, p.alt)
end
-- info on distance between waypoints
local total=0.0
for i=1,#waypoints-1 do
local point1=waypoints[i]
local point2=waypoints[i+1]
local x1=point1.x
local y1=point1.y
local x2=point2.x
local y2=point2.y
local d=math.sqrt((x1-x2)^2 + (y1-y2)^2)
local heading=self:_Course(point1, point2)
total=total+d
text=text..string.format("Distance from WP %i-->%i = %6.1f km. Heading = %i.\n", i-1, i, d/1000, heading)
end
text=text..string.format("Total distance = %6.1f km", total/1000)
-- send message
env.info(text)
if self.debug then
MESSAGE:New(text, 60):ToAll()
end
-- return total route length in meters
return total
end
--- Set the route of the AI plane. Due to DCS landing bug, this has to be done before the unit is spawned.
-- @param #RAT self
-- @return Wrapper.Airport#AIRBASE Departure airbase.
-- @return Wrapper.Airport#AIRBASE Destination airbase.
-- @return #table Table of flight plan waypoints.
function RAT:_SetRoute()
-- unit conversions
local ft2meter=0.305
local kmh2ms=0.278
local FL2m=30.48
local nm2km=1.852
--TODO: Add case where we don't have a departure airport but rather a zone!
-- DEPARTURE AIRPORT
-- Departure airport or zone.
local departure=self:_SetDeparture()
-- coordinates
local Pdeparture
if self.takeoff=="air" then
-- For an air start, we take a random point within the spawn zone.
local vec2=departure:GetRandomVec2()
Pdeparture=COORDINATE:New(vec2.x, self.aircraft.FLcruise, vec2.y)
else
Pdeparture=departure:GetCoordinate()
end
-- height ASL if departure
local H_departure=Pdeparture.y
-- DESTINATION AIRPORT
-- get all destination airports within reach and at least 10 km away from departure
self:_GetDestinations(Pdeparture, self.mindist, self.aircraft.Reff)
local destination=self:_SetDestination()
if destination:GetName()==departure:GetName() then
local text="Destination and departure airport are identical: "..destination:GetName().." with ID "..destination:GetID()
MESSAGE:New(text, 120):ToAll()
error(myid..text)
end
-- coordinates of destination
local Pdestination=destination:GetCoordinate()
-- height ASL of destination
local H_destination=Pdestination.y
-- DESCENT/HOLDING POINT
-- get a random point between 10 and 20 km away from the destination
local Vholding
if self.category=="plane" then
Vholding=destination:GetCoordinate():GetRandomVec2InRadius(20000, 10000)
else
-- for helos we set 500 to 1000 m
Vholding=destination:GetCoordinate():GetRandomVec2InRadius(1000, 500)
end
local Pholding=COORDINATE:NewFromVec2(Vholding)
-- holding point altitude
local h_holding
if self.category=="plane" then
h_holding=1000
else
h_holding=100
end
h_holding=self:_Randomize(h_holding, 0.2)
-- distance from holding point to destination
local d_holding=Pholding:Get2DDistance(Pdestination)
-- GENERAL
-- heading from departure to holding point of destination
local heading=self:_Course(Pdeparture, Pholding) -- heading from departure to destination
-- total distance between departure and holding point (+last bit to destination)
local d_total=Pdeparture:Get2DDistance(Pholding)
-- CLIMB and DESCENT angles
local AlphaClimb=self.aircraft.AlphaClimb --=self:_Randomize(self.aircraft.AlphaClimb, 0.1)
local AlphaDescent=self.aircraft.AlphaDescent --self:_Randomize(self.aircraft.AlphaDescent, 0.1)
--CRUISE
-- set min/max cruise altitudes
local FLmax
local FLmin
local FLcruise=self.aircraft.FLcruise
if self.category=="plane" then
-- min cruise alt is above 100 meter above holding point
FLmin=math.max(H_departure, H_destination+h_holding)
-- Check if the distance between the two airports is large enough to reach the desired FL and descent again at the given climb/descent rates.
-- TODO: need to modify this for "air" start.
FLmax=self:_FLmax(AlphaClimb, AlphaDescent, d_total, H_departure)
if self.takeoff=="air" then
FLmax=FLmax*2
end
if FLmin>FLmax then
FLmin=FLmax*0.8
end
if FLcruise>FLmax then
FLcruise=FLmax*0.9
end
else
-- for helicopters we take cruise alt between 50 to 1000 meters above ground
FLmin=math.max(H_departure, H_destination)+50
FLmax=math.max(H_departure, H_destination)+1000
end
-- set randomized cruise altitude: default +-50% but limited
FLcruise=self:_Randomize(FLcruise, 0.5, FLmin, FLmax)
-- check that we are not above 90% of service ceiling
FLcruise=math.min(FLcruise, self.aircraft.ceiling*0.9)
--CLIMB
-- height of climb relative to ASL height of departure airport
local h_climb=FLcruise-H_departure
-- x-distance of climb part
local d_climb=h_climb/math.tan(AlphaClimb)
-- time of climb in seconds
local t_climb=h_climb/self.aircraft.Vclimb
-- DESCENT
-- height difference for descent form cruise alt to holding point
local h_descent=FLcruise-H_destination-h_holding
-- x-distance of descent part
local d_descent=math.abs(h_descent/math.tan(AlphaDescent))
-- CRUISE
local d_cruise=d_total-d_climb-d_descent
-- debug message
local text=string.format("Route distances:\n")
text=text..string.format("d_climb = %6.1f km\n", d_climb/1000)
text=text..string.format("d_cruise = %6.1f km\n", d_cruise/1000)
text=text..string.format("d_descent = %6.1f km\n", d_descent/1000)
text=text..string.format("d_holding = %6.1f km\n", d_holding/1000)
text=text..string.format("d_total = %6.1f km\n", d_total/1000)
text=text..string.format("Route heights:\n")
text=text..string.format("H_departure = %6.1f m ASL\n", H_departure)
text=text..string.format("H_destination = %6.1f m ASL\n", H_destination)
text=text..string.format("h_climb = %6.1f m AGL\n", h_climb)
text=text..string.format("h_descent = %6.1f m\n", h_descent)
text=text..string.format("h_holding = %6.1f m AGL\n", h_holding)
text=text..string.format("Alpha_climb = %6.1f Deg\n", math.deg(AlphaClimb))
text=text..string.format("Alpha_descent = %6.1f Deg\n", math.deg(AlphaDescent))
text=text..string.format("FLmin = %6.1f m ASL\n", FLmin)
text=text..string.format("FLmax = %6.1f m ASL\n", FLmax)
text=text..string.format("FLcruise = %6.1f m ASL\n", FLcruise)
text=text..string.format("Heading = %6.1f Degrees", heading)
env.info(myid..text)
if self.debug then
MESSAGE:New(text, 60):ToAll()
end
-- coordinates of route from depature (0) to cruise (1) to descent (2) to holing (3) to destination (4)
local c0=Pdeparture
local c1=c0:Translate(d_climb, heading)
local c2=c1:Translate(d_cruise, heading)
local c3=c2:Translate(d_descent, heading)
local c3=Pholding
local c4=Pdestination
-- convert coordinates into route waypoints
--TODO: modify this for air start
local wp0=self:Waypoint(self.takeoff, c0, self.aircraft.Vmin, 2, departure)
local wp1=self:Waypoint("climb", c1, self.aircraft.Vmax, FLcruise)
local wp2=self:Waypoint("cruise", c2, self.aircraft.Vcruise, FLcruise)
--TODO: add the possibility for a holing point, i.e. we circle a bit before final approach.
local wp3=self:Waypoint("descent", c3, self.aircraft.Vmin, h_holding)
--local wp3=self:Waypoint("holding", c3, self.aircraft.Vmin, h_holding)
local wp4=self:Waypoint("landing", c4, self.aircraft.Vmin, 2, destination)
-- set waypoints
local waypoints = {wp0, wp1, wp2, wp3, wp4}
-- some info on the route as message
self:_Routeinfo(waypoints, "Waypoint info in set_route:")
-- return departure, destination and waypoints
return departure, destination, waypoints
end
--- Calculate the max flight level for a given distance and fixed climb and descent rates.
-- In other words we have a distance between two airports and want to know how high we
-- can climb before we must descent again to arrive at the destination without any level/cruising part.
-- @param #RAT self
-- @param #number alpha Angle of climb [rad].
-- @param #number beta Angle of descent [rad].
-- @param #number d Distance between the two airports [m].
-- @param #number h0 Height [m] of departure airport. Note we implicitly assume that the height difference between departure and destination is negligible.
-- @return #number Maximal flight level in meters.
function RAT:_FLmax(alpha, beta, d, h0)
-- solve ASA triangle for one side (d) and two adjacent angles (alpha, beta) given
local gamma=math.rad(180)-alpha-beta
local a=d*math.sin(alpha)/math.sin(gamma)
local b=d*math.sin(beta)/math.sin(gamma)
local h1=b*math.sin(alpha)
local h2=a*math.sin(beta)
local FL2m=30.48
local text=string.format("FLmax = FL%3.0f = %6.1f m.\n", h1/FL2m, h1)
text=text..string.format("FLmax = FL%3.0f = %6.1f m.", h2/FL2m, h2)
env.info(myid..text)
return b*math.sin(alpha)+h0
end
--- Modifies the template of the group to be spawned.
-- In particular, the waypoints of the group's flight plan are copied into the spawn template.
-- This allows to spawn at airports and also land at other airports, i.e. circumventing the DCS "landing bug".
-- @param #RAT self
-- @param #table waypoints The waypoints of the AI flight plan.
function RAT:_ModifySpawnTemplate(waypoints)
-- point of Airbase
--local PointVec3 = Airbase:GetPointVec3()
local PointVec3 = {x=waypoints[1].x, y=waypoints[1].alt, z=waypoints[1].y}
local addheight
if self.takeoff=="air" then
addheight=5000
else
addheight=2
end
if self:_GetSpawnIndex(self.SpawnIndex+1) then
-- Get copy of spawn template.
local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate
if SpawnTemplate then
self:E(SpawnTemplate)
-- Translate the position of the Group Template to the Vec3.
for UnitID = 1, #SpawnTemplate.units do
self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y )
local UnitTemplate = SpawnTemplate.units[UnitID]
local SX = UnitTemplate.x
local SY = UnitTemplate.y
local BX = SpawnTemplate.route.points[1].x
local BY = SpawnTemplate.route.points[1].y
local TX = PointVec3.x + (SX-BX)
local TY = PointVec3.z + (SY-BY)
SpawnTemplate.units[UnitID].x = TX
SpawnTemplate.units[UnitID].y = TY
SpawnTemplate.units[UnitID].alt = PointVec3.y + addheight
self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y )
end
-- Copy waypoints into spawntemplate.
-- this way we avoid the "landing bug" DCS currently has :)
for i,wp in ipairs(waypoints) do
SpawnTemplate.route.points[i]=wp
end
--TODO: Is this really necessary. Should already be defined in _Waypoints() function
--SpawnTemplate.route.points[1].x = PointVec3.x
--SpawnTemplate.route.points[1].y = PointVec3.z
--SpawnTemplate.route.points[1].alt = PointVec3.y + addheight
--SpawnTemplate.route.points[1].airdromeId = Airbase:GetID()
--SpawnTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff]
SpawnTemplate.x = PointVec3.x
SpawnTemplate.y = PointVec3.z
-- Update modified template for spawn group.
self.SpawnGroups[self.SpawnIndex].SpawnTemplate=SpawnTemplate
self:E(SpawnTemplate)
end
end
end
--- Create a table with the valid coalitions for departure and destination airports.
-- @param #RAT self
function RAT:_SetCoalitionTable()
-- get all possible departures/destinations depending on coalition
if self.friendly=="all" then
self.ctable={coalition.side.BLUE, coalition.side.RED, coalition.side.NEUTRAL}
elseif self.friendly=="blue" then
self.ctable={coalition.side.BLUE, coalition.side.NEUTRAL}
elseif self.friendly=="blueonly" then
self.ctable={coalition.side.BLUE}
elseif self.friendly=="red" then
self.ctable={coalition.side.RED, coalition.side.NEUTRAL}
elseif self.friendly=="redonly" then
self.ctable={coalition.side.RED}
elseif self.friendly=="neutral" then
self.ctable={coalition.side.NEUTRAL}
elseif self.friendly=="same" then
self.ctable={self.coalition, coalition.side.NEUTRAL}
elseif self.friendly=="sameonly" then
self.ctable={self.coalition}
else
self.ctable={self.coalition, coalition.side.NEUTRAL}
end
-- debug info
self:T({"Coalition table: ", self.ctable})
end
--- Convert 3D waypoint to 3D coordinate. x==>x, alt==>y, y==>z
-- @param #RAT self
-- @param #table wp Containing .x, .y and .alt
-- @return Core.Point#COORDINATE Coordinates of the waypoint.
function RAT:_WP2COORD(wp)
local _coord = COORDINATE:New(wp.x, wp.alt, wp.y) -- Core.Point#COORDINATE
return _coord
end
---Determine the heading from point a to point b.
--@param #RAT self
--@param Core.Point#COORDINATE a Point from.
--@param Core.Point#COORDINATE b Point to.
--@return #number Heading/angle in degrees.
function RAT:_Course(a,b)
local dx = b.x-a.x
-- take the right value for y-coordinate (if we have "alt" then "y" if not "z")
local ay
if a.alt then
ay=a.y
else
ay=a.z
end
local by
if b.alt then
by=b.y
else
by=b.z
end
local dy = by-ay
local angle = math.deg(math.atan2(dy,dx))
if angle < 0 then
angle = 360 + angle
end
return angle
end
--- Randomize a value by a certain amount.
-- @param #RAT self
-- @param #number value The value which should be randomized
-- @param #number fac Randomization factor.
-- @param #number lower (Optional) Lower limit of the returned value.
-- @param #number upper (Optional) Upper limit of the returned value.
-- @return #number Randomized value.
-- @usage _Randomize(100, 0.1) returns a value between 90 and 110, i.e. a plus/minus ten percent variation.
-- @usage _Randomize(100, 0.5, nil, 120) returns a value between 50 and 120, i.e. a plus/minus fivty percent variation with upper bound 120.
function RAT:_Randomize(value, fac, lower, upper)
local r=math.random(value-value*fac,value+value*fac)
if upper and r>upper then
r=upper
end
if lower and r<lower then
r=lower
end
return r
end