From e094c8133a80ea26204983a16f9001202df14b5c Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 3 Apr 2018 16:49:34 +0200 Subject: [PATCH 01/31] Added PseudoATC and Suppression --- .../Moose/Functional/PseudoATC.lua | 766 +++++++ .../Moose/Functional/Suppression.lua | 1816 +++++++++++++++++ 2 files changed, 2582 insertions(+) create mode 100644 Moose Development/Moose/Functional/PseudoATC.lua create mode 100644 Moose Development/Moose/Functional/Suppression.lua diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua new file mode 100644 index 000000000..9e93073d6 --- /dev/null +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -0,0 +1,766 @@ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Functional** - Pseudo ATC. +-- +-- ![Banner Image](..\Presentations\PSEUDOATC\PSEUDOATC_Main.jpg) +-- +-- ==== +-- +-- The pseudo ATC enhances the standard DCS ATC functions. +-- +-- In particular, a menu entry "Pseudo ATC" is created in the special F10 menu. +-- +-- ## Features +-- +-- * Report QFE or QNH pressures at nearby airbases. +-- * Report wind direction and strength at airbases. +-- * Report temperature at airbases +-- * Report absolute bearing and range to nearest airports. +-- * Report current altitude AGL of own aircraft. +-- * Upon request, ATC reports altitude until touchdown. +-- * Pressure temperature, wind data and BR for mission waypoints. +-- * Works with static and dynamic weather. +-- * All maps supported (Caucasus, NTTR, Normandy, and all future maps). +-- * Multiplayer ready (?) (I suppose yes, but I don't have a server to test or debug. Jumping from client to client works.) +-- +-- Pressure units: hPa (european aircraft), mmHg (russian aircraft), inHg (american aircraft). +-- +-- ==== +-- +-- # Demo Missions +-- +-- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- +-- ==== +-- +-- # YouTube Channel +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) +-- +-- === +-- +-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** +-- +-- ### Contributions: **Sven van de Velde ([FlightControl](https://forums.eagle.ru/member.php?u=89536))** +-- +-- ==== +-- @module PeusoATC + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- PSEUDOATC class +-- @type PSEUDOATC +-- @field #string ClassName Name of the Class. +-- @field #boolean Debug If true, print debug info to dcs.log file. +-- @field #table player Table comprising the player info. +-- @field #number mdur Duration in seconds how low messages to the player are displayed. +-- @field #boolean eventsmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. +-- @extends Core.Base#BASE + +---# PSEUDOATC class, extends @{Base#BASE} +-- The PSEUDOATC class adds some rudimentary ATC functionality via the radio menu. +-- +-- ## Scripting: +-- +-- Scripting is almost trivial. Just add the following line to your script: +-- +-- PSEUDOATC:Start() +-- +-- +-- @field #PSEUDOATC +PSEUDOATC={ + ClassName = "PSEUDOATC", + Debug=true, + player={}, + maxairport=9, + mdur=30, + mrefresh=120, +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- PSEUDOATC unit conversions. +-- @list unit +PSEUDOATC.unit={ + hPa2inHg=0.0295299830714, + hPa2mmHg=0.7500615613030, + meter2feet=3.28084, + km2nm=0.539957, +} + +--- Some ID to identify who we are in output of the DCS.log file. +-- @field #string id +PSEUDOATC.id="PseudoATC | " + +--- PSEUDOATC version. +-- @field #list +PSEUDOATC.version={ + version = "0.5.0", + print = true, +} + + +--- PSEUDOATC contructor. Starts the PseudoATC. +-- @param #PSEUDOATC self +-- @return #PSEUDOATC Returns a PSEUDOATC object. +function PSEUDOATC:Start() + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #PSEUDOATC + + -- Debug info + env.info(PSEUDOATC.id..string.format("Creating PseudoATC object. PseudoATC version %s", PSEUDOATC.version.version)) + + -- Handle events. + if self.eventsmoose then + self:HandleEvent(EVENTS.Birth, self._OnBirth) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) + --self:HandleEvent(EVENTS.PilotDead, self._PlayerLeft) + self:HandleEvent(EVENTS.Land, self._PlayerLanded) + --self:HandleEvent(EVENTS.Takeoff, self._PlayerTakeoff) + else + -- Events are handled directly by DCS. + world.addEventHandler(self) + end + + -- Return object. + return self +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- Event Handling + +--- Event handler for suppressed groups. +--@param #PSEUDOATC self +--@param #table Event Event data table. Holds event.id, event.initiator and event.target etc. +function PSEUDOATC:onEvent(Event) + if Event == nil or Event.initiator == nil or Unit.getByName(Event.initiator:getName()) == nil then + return true + end + + local DCSiniunit = Event.initiator + local DCSplace = Event.place + local DCSsubplace = Event.subplace + + 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) + end + + if Event.place then + EventData.Place=Event.place + EventData.PlaceName=Event.place:getName() + end + if Event.subplace then + EventData.SubPlace=Event.subplace + EventData.SubPlaceName=Event.subplace:getName() + end + + -- Event info. + if self.Debug then + env.info(PSEUDOATC.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) + env.info(PSEUDOATC.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) + env.info(PSEUDOATC.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) + env.info(PSEUDOATC.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) + env.info(PSEUDOATC.id..string.format("EVENT: Place = %s" , tostring(EventData.PlaceName))) + env.info(PSEUDOATC.id..string.format("EVENT: SubPlace = %s" , tostring(EventData.SubPlaceName))) + end + + -- Event birth. + if Event.id == world.event.S_EVENT_BIRTH and _playername then + self:_OnBirth(EventData) + end + + -- Event land. + if Event.id == world.event.S_EVENT_LAND and _playername and EventData.Place then + self:_PlayerLanded(EventData) + end + + -- Event player left unit + if Event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT and _playername then + self:_PlayerLeft(EventData) + end + +end + +--- Function called my MOOSE event handler when a player enters a unit. +-- @param #PSEUDOATC self +-- @param Core.Event#EVENTDATA EventData +function PSEUDOATC:_OnBirth(EventData) + env.info(PSEUDOATC.id.."PlayerEntered event caught my MOOSE.") + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + if _unit and _playername then + self:PlayerEntered(_unit) + end + +end + +--- Function called by MOOSE event handler when a player leaves a unit or dies. +-- @param #PSEUDOATC self +-- @param Core.Event#EVENTDATA EventData +function PSEUDOATC:_PlayerLeft(EventData) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + if _unit and _playername then + self:PlayerLeft(_unit) + end +end + +--- Function called by MOOSE event handler when a player landed. +-- @param #PSEUDOATC self +-- @param Core.Event#EVENTDATA EventData +function PSEUDOATC:_PlayerLanded(EventData) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + local _base=EventData.Place + local _baseName=EventData.PlaceName + + if _unit and _playername and _base then + self:PlayerLanded(_unit, _baseName) + end +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- Menu Functions + +--- Function called when a player enters a unit. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT unit Unit the player entered. +function PSEUDOATC:PlayerEntered(unit) + self:F2({unit=unit}) + + local group=unit:GetGroup() --Wrapper.Group#GROUP + local GID=group:GetID() + local GroupName=group:GetName() + local PlayerName=unit:GetPlayerName() + local UnitName=unit:GetName() + local CallSign=unit:GetCallsign() + + -- Init player table. + self.player[GID]={} + self.player[GID].group=group + self.player[GID].unit=unit + self.player[GID].groupname=GroupName + self.player[GID].unitname=UnitName + self.player[GID].playername=PlayerName + self.player[GID].callsign=CallSign + self.player[GID].waypoints=group:GetTaskRoute() + + -- Info message. + local text=string.format("Player %s entered unit %s of group %s. ID = %d", PlayerName, UnitName, GroupName, GID) + if self.Debug then + MESSAGE:New(text, 30):ToGroup(group) + env.info(PSEUDOATC.id..text) + end + + -- Create main F10 menu, i.e. "F10/Pseudo ATC" + self.player[GID].menu_main=missionCommands.addSubMenuForGroup(GID, "Pseudo ATC") + + -- Create list of nearby airports. + self:LocalAirports(GID) + + -- Create submenu My Positon. + self:MenuAircraft(GID) + + -- Create submenu airports. + self:MenuAirports(GID) + + -- Start scheduler to refresh the F10 menues. + self.player[GID].scheduler, self.player[GID].schedulerid=SCHEDULER:New(nil, self.MenuRefresh, {self, GID}, self.mrefresh, self.mrefresh) + + self:T2(self.player[GID]) +end + +--- Function called when a player has landed. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT unit Unit of player which has landed. +-- @param #string place Name of the place the player landed at. +function PSEUDOATC:PlayerLanded(unit, place) + self:F2({unit=unit, place=place}) + + -- Gather some information. + local group=unit:GetGroup() + local id=group:GetID() + local PlayerName=self.player[id].playername + local UnitName=self.player[id].playername + local GroupName=self.player[id].groupname + local CallSign=self.player[id].callsign + + -- Debug message. + if self.Debug then + local text=string.format("Player %s (%s) from group %s with ID %d landed at %s", PlayerName, UnitName, GroupName, place) + MESSAGE:New(text,30):ToAll() + env.info(PSEUDOATC.id..text) + end + + -- Stop altitude reporting timer if its activated. + self:AltidudeStopTimer(id) + + -- Welcome message. + if place then + local text=string.format("Touchdown! Welcome to %s. Have a nice day!", place) + MESSAGE:New(text, self.mdur):ToGroup(group) + end + +end + +--- Function called when a player leaves a unit or dies. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT unit Player unit which was left. +function PSEUDOATC:PlayerLeft(unit) + self:F2({unit=unit}) + + -- Get id. + local group=unit:GetGroup() + local id=group:GetID() + + -- Debug message. + if self.Debug then + local text=string.format("Player %s (%s) callsign %s of group %s just left.", self.player[id].playername, self.player[id].unitname, self.player[id].callsign, self.player[id].groupname) + MESSAGE:New(text,30):ToAll() + env.info(PSEUDOATC.id..text) + end + + -- Stop scheduler for menu updates + if self.player[id].schedulerid then + self.player[id].scheduler:Stop(self.player[id].schedulerid) + self.player[id].scheduler=nil + self.player[id].schedulerid=nil + end + + -- Remove main menu + missionCommands.removeItem(self.player[id].menu_main) + + -- Remove player array. + self.player[id]=nil + +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- Menu Functions + +--- Refreshes all player menues. +-- @param #PSEUDOATC self. +-- @param #number id Group id of player unit. +function PSEUDOATC:MenuRefresh(id) + self:F(id) + + if self.Debug then + local text=string.format("Refreshing menues for player %s in group %s.", self.player[id].playername, self.player[id].groupname) + env.info(PSEUDOATC.id..text) + MESSAGE:New(text,30):ToAll() + end + + -- Clear menu. + self:MenuClear(id) + + -- Create list of nearby airports. + self:LocalAirports(id) + + -- Create submenu My Positon. + self:MenuAircraft(id) + + -- Create submenu airports. + self:MenuAirports(id) +end + + +--- Clear player menues. +-- @param #PSEUDOATC self. +-- @param #number id Group id of player unit. +function PSEUDOATC:MenuClear(id) + self:F(id) + + if self.Debug then + local text=string.format("Clearing menues for player %s in group %s.", self.player[id].playername, self.player[id].groupname) + env.info(PSEUDOATC.id..text) + MESSAGE:New(text,30):ToAll() + end + + BASE:E(self.player[id].menu_airports) + + if self.player[id].menu_airports then + for name,item in pairs(self.player[id].menu_airports) do + + if self.Debug then + env.info(PSEUDOATC.id..string.format("Deleting menu item %s for ID %d", name, id)) + BASE:E(item) + end + + missionCommands.removeItemForGroup(id, self.player[id].menu_airports[name]) + --missionCommands.removeItemForGroup(id, item) + end + + else + if self.Debug then + local text=string.format("no airports to clear menues") + env.info(PSEUDOATC.id..text) + end + end + + if self.player[id].menu_aircraft then + missionCommands.removeItemForGroup(id, self.player[id].menu_aircraft.main) + end + + self.player[id].menu_airports=nil + self.player[id].menu_aircraft=nil +end + +--- Create "F10/Pseudo ATC" menu items "Airport Data". +-- @param #PSEUDOATC self +-- @param #number id Group id of player unit for which menues are created. +function PSEUDOATC:MenuAirports(id) + self:F(id) + + -- Table for menu entries. + self.player[id].menu_airports={} + + local i=0 + for _,airport in pairs(self.player[id].airports) do + + i=i+1 + if i>self.maxairport then + break -- Max X<10 airports due to 10 menu items restriction. + end + + local name=airport.name + local d=airport.distance + local pos=AIRBASE:FindByName(name):GetCoordinate() + + --F10menu_ATC_airports[ID][name] = missionCommands.addSubMenuForGroup(ID, name, F10menu_ATC) + local submenu=missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_main) + self.player[id].menu_airports[name]=submenu + + -- Create menu reporting commands + missionCommands.addCommandForGroup(id, "Request QFE", submenu, self.ReportPressure, self, id, "QFE", pos, name) + missionCommands.addCommandForGroup(id, "Request QNH", submenu, self.ReportPressure, self, id, "QNH", pos, name) + missionCommands.addCommandForGroup(id, "Request Wind", submenu, self.ReportWind, self, id, pos, name) + missionCommands.addCommandForGroup(id, "Request Temperature", submenu, self.ReportTemperature, self, id, pos, name) + missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, name) + + if self.Debug then + env.info(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, id)) + end + end +end + +--- Create F10/Pseudo ATC menu item "My Plane". +-- @param #PSEUDOATC self +-- @param #number id Group id of player unit for which menues are created. +function PSEUDOATC:MenuAircraft(id) + self:F(id) + + -- Table for menu entries. + self.player[id].menu_aircraft={} + + local unit=self.player[id].unit --Wrapper.Unit#UNIT + local callsign=self.player[id].callsign + local name=string.format("My Aircraft (%s)", callsign) + + -- Debug info. + if self.Debug then + env.info(PSEUDOATC.id..string.format("Creating menu item %s for ID %d", name,id)) + end + + -- F10/PseudoATC/My Aircraft (callsign) + self.player[id].menu_aircraft.main = missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_main) + + -- F10/PseudoATC/My Aircraft (callsign)/Waypoints + if #self.player[id].waypoints>0 then + + --F10menu_ATC_waypoints[ID]={} + self.player[id].menu_aircraft_waypoints={} + self.player[id].menu_aircraft_waypoints.main=missionCommands.addSubMenuForGroup(id, "Waypoints", self.player[id].menu_aircraft.main) + + local j=0 + for i, wp in pairs(self.player[id].waypoints) do + -- Increase counter + j=j+1 + + if j>10 then + break -- max ten menu entries + end + + local pos=COORDINATE:New(wp.x,wp.alt,wp.z) + + local fname=string.format("Waypoint %d for %s", i-1, callsign) + local pname=string.format("Waypoint %d", i-1) + + -- "F10/PseudoATC/My Aircraft (callsign)/Waypoints/Waypoint X" + local submenu=missionCommands.addSubMenuForGroup(id, pname, self.player[id].menu_aircraft_waypoints.main) + self.player[id].menu_aircraft_waypoints.pname=submenu + + -- Menu commands for each waypoint "F10/PseudoATC/My Aircraft (callsign)/Waypoints/Waypoint X/" + missionCommands.addCommandForGroup(id, "Request QFE", submenu, self.ReportPressure, self, id, "QFE", pos, pname) + missionCommands.addCommandForGroup(id, "Request QNH", submenu, self.ReportPressure, self, id, "QNH", pos, pname) + missionCommands.addCommandForGroup(id, "Request Wind", submenu, self.ReportWind, self, id, pos, pname) + missionCommands.addCommandForGroup(id, "Request Temperature", submenu, self.ReportTemperature, self, id, pos, pname) + missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, pname) + end + end + missionCommands.addCommandForGroup(id, "Request current altitude AGL", self.player[id].menu_aircraft.main, self.ReportHeight, self, id) + missionCommands.addCommandForGroup(id, "Report altitude until touchdown", self.player[id].menu_aircraft.main, self.AltidudeStartTimer, self, id) + missionCommands.addCommandForGroup(id, "Quit reporting altitude", self.player[id].menu_aircraft.main, self.AltidudeStopTimer, self, id) +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- Reporting Functions + +--- Report pressure. +-- @param #PSEUDOATC self +-- @param #number id Group id to which the report is delivered. +-- @param #string Qcode Can be "QNH" for pressure at sea level or "QFE" for pressure at field elevation. Default is QFE or more precisely pressure at position. +-- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. +-- @param #string location Name of the location at which the pressure is measured. +function PSEUDOATC:ReportPressure(id, Qcode, position, location) + self:F({id=id, Qcode=Qcode, position=position, location=location}) + + -- Get pressure in hPa. + local P + if Qcode=="QNH" then + P=position:GetPressure(0) -- Get pressure at sea level. + else + P=position:GetPressure() -- Get pressure at (land) height of position. + end + + -- Unit conversion. + local P_inHg=P * PSEUDOATC.unit.hPa2inHg + local P_mmHg=P * PSEUDOATC.unit.hPa2mmHg + + -- Message text. + local text=string.format("%s at %s: P = %.1f hPa = %.2f inHg = %.1f mmHg.", Qcode, location, P, P_inHg, P_mmHg) + + -- Send message. + MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) +end + +--- Report temperature. +-- @param #PSEUDOATC self +-- @param #number id Group id to the report is delivered. +-- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. +-- @param #string location Name of the location at which the pressure is measured. +function PSEUDOATC:ReportTemperature(id, position, location) + self:F({id=id, position=position, location=location}) + + --- convert celsius to fahrenheit + local function celsius2fahrenheit(degC) + return degC*1.8+32 + end + + -- Get temperature at position in degrees Celsius. + local T=position:GetTemperature() + + -- Formatted temperature in Celsius and Fahrenheit. + local Tc=string.format('%d°C', T) + local Tf=string.format('%d°F', celsius2fahrenheit(T)) + + -- Message text. + local text=string.format("Temperature at %s is %s = %s", location, Tc, Tf) + + -- Send message to player group. + MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) +end + +--- Report wind direction and strength. +-- @param #PSEUDOATC self +-- @param #number id Group id to the report is delivered. +-- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. +-- @param #string location Name of the location at which the pressure is measured. +function PSEUDOATC:ReportWind(id, position, location) + self:F({id=id, position=position, location=location}) + + -- Get wind direction and speed. + local Dir,Vel=position:GetWind() + + -- Get Beaufort wind scale. + local Bn,Bd=UTILS.BeaufortScale(Vel) + + -- Formatted wind direction. + local Ds = string.format('%03d°', Dir) + + -- Message text. + local text=string.format("%s: Wind from %s at %.1f m/s (%s).", location, Ds, Vel, Bd) + + -- Send message to player group. + MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) +end + +--- Report absolute bearing and range form player unit to airport. +-- @param #PSEUDOATC self +-- @param #number id Group id to the report is delivered. +-- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. +-- @param #string location Name of the location at which the pressure is measured. +function PSEUDOATC:ReportBR(id, position, location) + self:F({id=id, position=position, location=location}) + + -- Current coordinates. + local unit=self.player[id].unit --Wrapper.Unit#UNIT + local coord=unit:GetCoordinate() + + -- Direction vector from current position (coord) to target (position). + local vec3=coord:GetDirectionVec3(position) + local angle=coord:GetAngleDegrees(vec3) + local range=coord:Get2DDistance(position) + + -- Bearing string. + local Bs=string.format('%03d°', angle) + + -- Message text. + local text=string.format("%s: Bearing %s, Range %.1f km = %.1f NM.", location, Bs, range/1000, range/1000 * PSEUDOATC.unit.km2nm) + + -- Send message to player group. + MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) +end + +--- Report altitude above ground level of player unit. +-- @param #PSEUDOATC self +-- @param #number id Group id to the report is delivered. +-- @param #number dt (Optional) Duration the message is displayed. +-- @return #number Altuitude above ground. +function PSEUDOATC:ReportHeight(id, dt) + self:F({id=id, dt=dt}) + + local dt = dt or self.mdur + + -- Return height [m] above ground level. + local function get_AGL(p) + local vec2={x=p.x,y=p.z} + local ground=land.getHeight(vec2) + local agl=p.y-ground + return agl + end + + -- Get height AGL. + local unit=self.player[id].unit --Wrapper.Unit#UNIT + local position=unit:GetCoordinate() + local height=get_AGL(position) + local callsign=unit:GetCallsign() + + -- Message text. + local text=string.format("%s: Your altitude is %d m = %d ft AGL.", callsign, height, height*PSEUDOATC.unit.meter2feet) + + -- Send message to player group. + MESSAGE:New(text, dt):ToGroup(self.player[id].group) + + -- Return height + return height +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start DCS scheduler function. +-- @param #PSEUDOATC self. +-- @param #number id Group id of player unit. +function PSEUDOATC:AltidudeStartTimer(id) + self:F(id) + + -- Debug info. + if self.Debug then + env.info(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.", id)) + end + + -- Start timer. + --self.player[id].altimer=timer.scheduleFunction(self.ReportAltTouchdown, self, id, Tnow+2) + self.player[id].altimer, self.player[id].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, id, 0.1}, 1, 5) +end + +--- Stop/destroy DCS scheduler function for reporting altitude. +-- @param #PSEUDOATC self. +-- @param #number id Group id of player unit. +function PSEUDOATC:AltidudeStopTimer(id) + + -- Debug info. + if self.Debug then + env.info(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", id)) + end + + -- Stop timer. + --timer.removeFunction(self.player[id].alttimer) + if self.player[id].altimerid then + self.player[id].altimer:Stop(self.player[id].altimerid) + end + + self.player[id].altimer=nil + self.player[id].altimerid=nil +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc + +--- Create list of nearby airports sorted by distance to player unit. +-- @param #PSEUDOATC self +-- @param #number id Group id of player unit. +function PSEUDOATC:LocalAirports(id) + self:F(id) + + -- Airports table. + self.player[id].airports=nil + self.player[id].airports={} + + -- Current player position. + local pos=self.player[id].unit:GetCoordinate() + + -- Loop over coalitions. + for i=0,2 do + + -- Get all airbases of coalition. + local airports=coalition.getAirbases(i) + + -- Loop over airbases + for _,airbase in pairs(airports) do + + local name=airbase:getName() + local q=AIRBASE:FindByName(name):GetCoordinate() + local d=q:Get2DDistance(pos) + + -- Add to table. + table.insert(self.player[id].airports, {distance=d, name=name}) + + end + end + + --- compare distance (for sorting airports) + local function compare(a,b) + return a.distance < b.distance + end + + -- Sort airports table w.r.t. distance to player. + table.sort(self.player[id].airports, compare) + +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #PSEUDOATC self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player. +-- @return #string Name of the player. +-- @return nil If player does not exist. +function PSEUDOATC:_GetPlayerUnitAndName(_unitName) + self:F(_unitName) + + if _unitName ~= nil then + local DCSunit=Unit.getByName(_unitName) + local playername=DCSunit:getPlayerName() + local unit=UNIT:Find(DCSunit) + + if DCSunit and unit and playername then + return unit, playername + end + end + + return nil,nil +end + + + diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua new file mode 100644 index 000000000..f3a4c4954 --- /dev/null +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -0,0 +1,1816 @@ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Functional** - Suppress fire of ground units when they get hit. +-- +-- ![Banner Image](..\Presentations\SUPPRESSION\Suppression_Main.png) +-- +-- ==== +-- +-- When ground units get hit by (suppressive) enemy fire, they will not be able to shoot back for a certain amount of time. +-- +-- The implementation is based on an idea and script by MBot. See the [DCS forum threat](https://forums.eagle.ru/showthread.php?t=107635) for details. +-- +-- In addition to suppressing the fire, conditions can be specified which let the group retreat to a defined zone, move away from the attacker +-- or hide at a nearby scenery object. +-- +-- ==== +-- +-- # Demo Missions +-- +-- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- +-- ==== +-- +-- # YouTube Channel +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) +-- +-- === +-- +-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** +-- +-- ### Contributions: **Sven van de Velde ([FlightControl](https://forums.eagle.ru/member.php?u=89536))** +-- +-- ==== +-- @module Suppression + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- SUPPRESSION class +-- @type SUPPRESSION +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. +-- @field #boolean flare Flare units when they get hit or die. +-- @field #boolean smoke Smoke places to which the group retreats, falls back or hides. +-- @field #list DCSdesc Table containing all DCS descriptors of the group. +-- @field #string Type Type of the group. +-- @field #number SpeedMax Maximum speed of group in km/h. +-- @field #boolean IsInfantry True if group has attribute Infantry. +-- @field Core.Controllable#CONTROLLABLE Controllable Controllable of the FSM. Must be a ground group. +-- @field #number Tsuppress_ave Average time in seconds a group gets suppressed. Actual value is sampled randomly from a Gaussian distribution. +-- @field #number Tsuppress_min Minimum time in seconds the group gets suppressed. +-- @field #number Tsuppress_max Maximum time in seconds the group gets suppressed. +-- @field #number TsuppressionOver Time at which the suppression will be over. +-- @field #number IniGroupStrength Number of units in a group at start. +-- @field #number Nhit Number of times the group was hit. +-- @field #string Formation Formation which will be used when falling back, taking cover or retreating. Default "Vee". +-- @field #number Speed Speed the unit will use when falling back, taking cover or retreating. Default 999. +-- @field #boolean MenuON If true creates a entry in the F10 menu. +-- @field #boolean FallbackON If true, group can fall back, i.e. move away from the attacking unit. +-- @field #number FallbackWait Time in seconds the unit will wait at the fall back point before it resumes its mission. +-- @field #number FallbackDist Distance in meters the unit will fall back. +-- @field #number FallbackHeading Heading in degrees to which the group should fall back. Default is directly away from the attacking unit. +-- @field #boolean TakecoverON If true, group can hide at a nearby scenery object. +-- @field #number TakecoverWait Time in seconds the group will hide before it will resume its mission. +-- @field #number TakecoverRange Range in which the group will search for scenery objects to hide at. +-- @field Core.Point#COORDINATE hideout Coordinate/place where the group will try to take cover. +-- @field #number PminFlee Minimum probability in percent that a group will flee (fall back or take cover) at each hit event. Default is 10 %. +-- @field #number PmaxFlee Maximum probability in percent that a group will flee (fall back or take cover) at each hit event. Default is 90 %. +-- @field Core.Zone#ZONE RetreatZone Zone to which a group retreats. +-- @field #number RetreatDamage Damage in percent at which the group will be ordered to retreat. +-- @field #number RetreatWait Time in seconds the group will wait in the retreat zone before it resumes its mission. Default two hours. +-- @field #string CurrentAlarmState Alam state the group is currently in. +-- @field #string CurrentROE ROE the group currently has. +-- @field #string DefaultAlarmState Alarm state the group will go to when it is changed back from another state. Default is "Auto". +-- @field #string DefaultROE ROE the group will get once suppression is over. Default is "Free". +-- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true. +-- @extends Core.Fsm#FSM_CONTROLLABLE +-- + +---# SUPPRESSION class, extends @{Core.Fsm#FSM_CONTROLLABLE} +-- Mimic suppressive enemy fire and let groups flee or retreat. +-- +-- ## Suppression Process +-- +-- ![Process](..\Presentations\SUPPRESSION\Suppression_Process.png) +-- +-- The suppression process can be described as follows. +-- +-- ### CombatReady +-- +-- A group starts in the state **CombatReady**. In this state the group is ready to fight. The ROE is set to either "Weapon Free" or "Return Fire". +-- The alarm state is set to either "Auto" or "Red". +-- +-- ### Event Hit +-- The most important event in this scenario is the **Hit** event. This is an event of the FSM and triggered by the DCS event hit. +-- +-- ### Suppressed +-- After the **Hit** event the group changes its state to **Suppressed**. Technically, the ROE of the group is changed to "Weapon Hold". +-- The suppression of the group will last a certain amount of time. It is randomized an will vary each time the group is hit. +-- The expected suppression time is set to 15 seconds by default. But the actual value is sampled from a Gaussian distribution. +-- +-- ![Process](..\Presentations\SUPPRESSION\Suppression_Gaussian.png) +-- +-- The graph shows the distribution of suppression times if a group would be hit 100,000 times. As can be seen, on most hits the group gets +-- suppressed for around 15 seconds. Other values are also possible but they become less likely the further away from the "expected" suppression time they are. +-- Minimal and maximal suppression times can also be specified. By default these are set to 5 and 25 seconds, respectively. This can also be seen in the graph +-- because the tails of the Gaussian distribution are cut off at these values. +-- +-- ### Event Recovered +-- After the suppression time is over, the event **Recovered** is initiated and the group becomes **CombatReady** again. +-- The ROE of the group will be set to "Weapon Free". +-- +-- Of course, it can also happen that a group is hit again while it is still suppressed. In that case a new random suppression time is calculated. +-- If the new suppression time is longer than the remaining suppression of the previous hit, then the group recovers when the suppression time of the last +-- hit has passed. +-- If the new suppression time is shorter than the remaining suppression, the group will recover after the longer time of the first suppression has passed. +-- +-- For example: +-- +-- * A group gets hit the first time and is suppressed for - let's say - 15 seconds. +-- * After 10 seconds, i.e. when 5 seconds of the old suppression are left, the group gets hit a again. +-- * A new suppression time is calculated which can be smaller or larger than the remaining 5 seconds. +-- * If the new suppression time is smaller, e.g. three seconds, than five seconds, the group will recover after the 5 remaining seconds of the first suppression have passed. +-- * If the new suppression time is longer than last suppression time, e.g. 10 seconds, then the group will recover after the 10 seconds of the new hit have passed. +-- +-- Generally speaking, the suppression times are not just added on top of each other. Because this could easily lead to the situation that a group +-- never becomes CombatReady again before it gets destroyed. +-- +-- The mission designer can capture the event **Recovered** by the function @{#SUPPRESSION.OnAfterRecovered}(). +-- +-- ## Flee Events and States +-- Apart from being suppressed the groups can also flee from the enemy under certain conditions. +-- +-- ### Event Retreat +-- The first option is a retreat. This can be enabled by setting a retreat zone, i.e. a trigger zone defined in the mission editor. +-- +-- If the group takes a certain amount of damage, the event **Retreat** will be called and the group will start to move to the retreat zone. +-- The group will be in the state **Retreating**, which means that its ROE is set to "Weapon Hold" and the alarm state is set to "Green". +-- Setting the alarm state to green is necessary to enable the group to move under fire. +-- +-- When the group has reached the retreat zone, the event **Retreated** is triggered and the state will change to **Retreated** (note that both the event and +-- the state of the same name in this case). ROE and alarm state are +-- set to "Return Fire" and "Auto", respectively. The group will stay in the retreat zone and not actively participate in the combat any more. +-- +-- If no option retreat zone has been specified, the option retreat is not available. +-- +-- The mission designer can capture the events **Retreat** and **Retreated** by the functions @{#SUPPRESSION.OnAfterRetreat}() and @{#SUPPRESSION.OnAfterRetreated}(). +-- +-- ### Fallback +-- +-- If a group is attacked by another ground group, it has the option to fall back, i.e. move away from the enemy. The probability of the event **FallBack** to +-- happen depends on the damage of the group that was hit. The more a group gets damaged, the more likely **FallBack** event becomes. +-- +-- If the group enters the state **FallingBack** it will move 100 meters in the opposite direction of the attacking unit. ROE and alarmstate are set to "Weapon Hold" +-- and "Green", respectively. +-- +-- At the fallback point the group will wait for 60 seconds before it resumes its normal mission. +-- +-- The mission designer can capture the event **FallBack** by the function @{#SUPPRESSION.OnAfterFallBack}(). +-- +-- ### TakeCover +-- +-- If a group is hit by either another ground or air unit, it has the option to "take cover" or "hide". This means that the group will move to a random +-- scenery object in it vicinity. +-- +-- Analogously to the fall back case, the probability of a **TakeCover** event to occur, depends on the damage of the group. The more a group is damaged, the more +-- likely it becomes that a group takes cover. +-- +-- When a **TakeCover** event occurs an area with a radius of 300 meters around the hit group is searched for an arbitrary scenery object. +-- If at least one scenery object is found, the group will move there. One it has reached its "hideout", it will wait there for two minutes before it resumes its +-- normal mission. +-- +-- If more than one scenery object is found, the group will move to a random one. +-- If no scenery object is near the group the **TakeCover** event is rejected and the group will not move. +-- +-- The mission designer can capture the event **TakeCover** by the function @{#SUPPRESSION.OnAfterTakeCover}(). +-- +-- ### Choice of FallBack or TakeCover if both are enabled? +-- +-- If both **FallBack** and **TakeCover** events are enabled by the functions @{#SUPPRESSION.Fallback}() and @{#SUPPRESSION.Takecover}() the algorithm does the following: +-- +-- * If the attacking unit is a ground unit, then the **FallBack** event is executed. +-- * Otherwise, i.e. if the attacker is *not* a ground unit, then the **TakeCover** event is triggered. +-- +-- ### FightBack +-- +-- When a group leaves the states **TakingCover** or **FallingBack** the event **FightBack** is triggered. This changes the ROE and the alarm state back to their default values. +-- +-- The mission designer can capture the event **FightBack** by the function @{#SUPPRESSION.OnAfterFightBack}() +-- +-- # Examples +-- +-- ## Simple Suppression +-- This example shows the basic steps to use suppressive fire for a group. +-- +-- ![Process](..\Presentations\SUPPRESSION\Suppression_Example_01.png) +-- +-- ## Suppression and Rescure +-- This example shows how the event **Retreat** can be captured. Here, a transport is started which picks up the wounded troups and drives them to a safe zone. +-- +-- ![Process](..\Presentations\SUPPRESSION\Suppression_Rescue.png) +-- +-- # Customization and Fine Tuning +-- The following user functions can be used to change the default values +-- +-- * @{#SUPPRESSION.SetSuppressionTime}() can be used to set the time a goup gets suppressed. +-- * @{#SUPPRESSION.SetRetreatZone}() sets the retreat zone and enables the possiblity for the group to retreat. +-- * @{#SUPPRESSION.SetFallbackDistance}() sets a value how far the unit moves away from the attacker after the fallback event. +-- * @{#SUPPRESSION.SetFallbackWait}() sets the time after which the group resumes its mission after a FallBack event. +-- * @{#SUPPRESSION.SetTakecoverWait}() sets the time after which the group resumes its mission after a TakeCover event. +-- * @{#SUPPRESSION.SetTakecoverRange}() sets the radius in which hideouts are searched. +-- * @{#SUPPRESSION.SetTakecoverPlace}() explicitly sets the place where the group will run at a TakeCover event. +-- * @{#SUPPRESSION.SetMinimumFleeProbability}() sets the minimum probability that a group flees (FallBack or TakeCover) after a hit. Note taht the probability increases with damage. +-- * @{#SUPPRESSION.SetMaximumFleeProbability}() sets the maximum probability that a group flees (FallBack or TakeCover) after a hit. Default is 90%. +-- * @{#SUPPRESSION.SetRetreatDamage}() sets the damage a group/unit can take before it is ordered to retreat. +-- * @{#SUPPRESSION.SetRetreatWait}() sets the time a group waits in the retreat zone after a retreat. +-- * @{#SUPPRESSION.SetDefaultAlarmState}() sets the alarm state a group gets after it becomes CombatReady again. +-- * @{#SUPPRESSION.SetDefaultROE}() set the rules of engagement a group gets after it becomes CombatReady again. +-- * @{#SUPPRESSION.FlareOn}() is mainly for debugging. A flare is fired when a unit is hit, gets suppressed, recovers, dies. +-- * @{#SUPPRESSION.SmokeOn}() is mainly for debugging. Puts smoke on retreat zone, hideouts etc. +-- * @{#SUPPRESSION.MenuON}() is mainly for debugging. Activates a radio menu item where certain functions like retreat etc. can be triggered manually. +-- +-- +-- @field #SUPPRESSION +SUPPRESSION={ + ClassName = "SUPPRESSION", + Debug = false, + flare = false, + smoke = false, + DCSdesc = nil, + Type = nil, + IsInfantry=nil, + SpeedMax = nil, + Tsuppress_ave = 15, + Tsuppress_min = 5, + Tsuppress_max = 25, + TsuppressOver = nil, + IniGroupStrength = nil, + Nhit = 0, + Formation = "Off road", + Speed = 4, + MenuON = false, + FallbackON = false, + FallbackWait = 60, + FallbackDist = 100, + FallbackHeading = nil, + TakecoverON = false, + TakecoverWait = 120, + TakecoverRange = 300, + hideout = nil, + PminFlee = 10, + PmaxFlee = 90, + RetreatZone = nil, + RetreatDamage = nil, + RetreatWait = 7200, + CurrentAlarmState = "unknown", + CurrentROE = "unknown", + DefaultAlarmState = "Auto", + DefaultROE = "Weapon Free", + eventmoose = true, +} + +--- Enumerator of possible rules of engagement. +-- @field #list ROE +SUPPRESSION.ROE={ + Hold="Weapon Hold", + Free="Weapon Free", + Return="Return Fire", +} + +--- Enumerator of possible alarm states. +-- @field #list AlarmState +SUPPRESSION.AlarmState={ + Auto="Auto", + Green="Green", + Red="Red", +} + +--- Main F10 menu for suppresion, i.e. F10/Suppression. +-- @field #string MenuF10 +SUPPRESSION.MenuF10=nil + +--- Some ID to identify who we are in output of the DCS.log file. +-- @field #string id +SUPPRESSION.id="SFX | " + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--TODO: Figure out who was shooting and move away from him. +--TODO: Move behind a scenery building if there is one nearby. +--TODO: Retreat to a given zone or point. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Creates a new AI_suppression object. +-- @param #SUPPRESSION self +-- @param Wrapper.Group#GROUP group The GROUP object for which suppression should be applied. +-- @return #SUPPRESSION SUPPRESSION object. +-- @return nil If group does not exist or is not a ground group. +function SUPPRESSION:New(group) + BASE:F2(group) + + -- Inherits from FSM_CONTROLLABLE + local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #SUPPRESSION + + -- Check that group is present. + if group then + self:T(SUPPRESSION.id.."Suppressive fire for group "..group:GetName()) + else + self:E(SUPPRESSION.id.."Suppressive fire: Requested group does not exist! (Has to be a MOOSE group.)") + return nil + end + + -- Check that we actually have a GROUND group. + if group:IsGround()==false then + self:E(SUPPRESSION.id.."SUPPRESSION fire group "..group:GetName().." has to be a GROUND group!") + return nil + end + + -- Set the controllable for the FSM. + self:SetControllable(group) + + -- Get DCS descriptors of group. + local DCSgroup=Group.getByName(group:GetName()) + local DCSunit=DCSgroup:getUnit(1) + self.DCSdesc=DCSunit:getDesc() + + -- Get max speed the group can do and convert to km/h. + self.SpeedMax=self.DCSdesc.speedMaxOffRoad*3.6 + --self.SpeedMaxOffRoad=DCSdesc.speedMaxOffRoad + + -- Set speed to maximum. + self.Speed=self.SpeedMax + + -- Is this infantry or not. + self.IsInfantry=DCSunit:hasAttribute("Infantry") + + -- Type of group. + self.Type=group:GetTypeName() + + -- Initial group strength. + self.IniGroupStrength=#group:GetUnits() + + -- Set ROE and Alarm State. + self:SetDefaultROE("Free") + self:SetDefaultAlarmState("Auto") + + -- Transitions + self:AddTransition("*", "Start", "CombatReady") + self:AddTransition("CombatReady", "Hit", "Suppressed") + self:AddTransition("Suppressed", "Hit", "Suppressed") + self:AddTransition("Suppressed", "Recovered", "CombatReady") + self:AddTransition("Suppressed", "TakeCover", "TakingCover") + self:AddTransition("Suppressed", "FallBack", "FallingBack") + self:AddTransition("*", "Retreat", "Retreating") + self:AddTransition("TakingCover", "FightBack", "CombatReady") + self:AddTransition("FallingBack", "FightBack", "CombatReady") + self:AddTransition("Retreating", "Retreated", "Retreated") + self:AddTransition("*", "Dead", "*") + + + --- User function for OnBefore "Hit" event. + -- @function [parent=#SUPPRESSION] OnBeforeHit + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT Unit Unit that was hit. + -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + -- @return #boolean + + --- User function for OnAfer "Hit" event. + -- @function [parent=#SUPPRESSION] OnAfterHit + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT Unit Unit that was hit. + -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + + + --- User function for OnBefore "Recovered" event. + -- @function [parent=#SUPPRESSION] OnBeforeRecovered + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean + + --- User function for OnAfter "Recovered" event. + -- @function [parent=#SUPPRESSION] OnAfterRecovered + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- User function for OnBefore "TakeCover" event. + -- @function [parent=#SUPPRESSION] OnBeforeTakeCover + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + -- @return #boolean + + --- User function for OnAfter "TakeCover" event. + -- @function [parent=#SUPPRESSION] OnAfterTakeCover + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + + + --- User function for OnBefore "FallBack" event. + -- @function [parent=#SUPPRESSION] OnBeforeFallBack + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + -- @return #boolean + + --- User function for OnAfter "FallBack" event. + -- @function [parent=#SUPPRESSION] OnAfterFallBack + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + + + --- User function for OnBefore "Retreat" event. + -- @function [parent=#SUPPRESSION] OnBeforeRetreat + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean + + --- User function for OnAfter "Retreat" event. + -- @function [parent=#SUPPRESSION] OnAfterRetreat + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- User function for OnBefore "Retreated" event. + -- @function [parent=#SUPPRESSION] OnBeforeRetreated + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean + + --- User function for OnAfter "Retreated" event. + -- @function [parent=#SUPPRESSION] OnAfterRetreated + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- User function for OnBefore "FlightBack" event. + -- @function [parent=#SUPPRESSION] OnBeforeFightBack + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean + + --- User function for OnAfter "FlightBack" event. + -- @function [parent=#SUPPRESSION] OnAfterFightBack + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set average, minimum and maximum time a unit is suppressed each time it gets hit. +-- @param #SUPPRESSION self +-- @param #number Tave Average time [seconds] a group will be suppressed. Default is 15 seconds. +-- @param #number Tmin (Optional) Minimum time [seconds] a group will be suppressed. Default is 5 seconds. +-- @param #number Tmax (Optional) Maximum time a group will be suppressed. Default is 25 seconds. +function SUPPRESSION:SetSuppressionTime(Tave, Tmin, Tmax) + + -- Minimum suppression time is input or default but at least 1 second. + self.Tsuppress_min=Tmin or self.Tsuppress_min + self.Tsuppress_min=math.max(self.Tsuppress_min, 1) + + -- Maximum suppression time is input or dault but at least Tmin. + self.Tsuppress_max=Tmax or self.Tsuppress_max + self.Tsuppress_max=math.max(self.Tsuppress_max, self.Tsuppress_min) + + -- Expected suppression time is input or default but at leat Tmin and at most Tmax. + self.Tsuppress_ave=Tave or self.Tsuppress_ave + self.Tsuppress_ave=math.max(self.Tsuppress_min) + self.Tsuppress_ave=math.min(self.Tsuppress_max) + + self:T(SUPPRESSION.id..string.format("Set ave suppression time to %d seconds.", self.Tsuppress_ave)) + self:T(SUPPRESSION.id..string.format("Set min suppression time to %d seconds.", self.Tsuppress_min)) + self:T(SUPPRESSION.id..string.format("Set max suppression time to %d seconds.", self.Tsuppress_max)) +end + +--- Set the zone to which a group retreats after being damaged too much. +-- @param #SUPPRESSION self +-- @param Core.Zone#ZONE zone MOOSE zone object. +function SUPPRESSION:SetRetreatZone(zone) + self.RetreatZone=zone +end + +--- Turn Debug mode on. Enables messages and more output to DCS log file. +-- @param #SUPPRESSION self +function SUPPRESSION:DebugOn() + self.Debug=true +end + +--- Flare units when they are hit, die or recover from suppression. +-- @param #SUPPRESSION self +function SUPPRESSION:FlareOn() + self.flare=true +end + +--- Smoke positions where units fall back to, hide or retreat. +-- @param #SUPPRESSION self +function SUPPRESSION:SmokeOn() + self.smoke=true +end + +--- Set the formation a group uses for fall back, hide or retreat. +-- @param #SUPPRESSION self +-- @param #string formation Formation of the group. Default "Vee". +function SUPPRESSION:SetFormation(formation) + self.Formation=formation or "Vee" +end + +--- Set speed a group moves at for fall back, hide or retreat. +-- @param #SUPPRESSION self +-- @param #number speed Speed in km/h of group. Default max speed the group can do. +function SUPPRESSION:SetSpeed(speed) + self.Speed=speed or self.SpeedMax + self.Speed=math.min(self.Speed, self.SpeedMax) +end + +--- Enable fall back if a group is hit. +-- @param #SUPPRESSION self +-- @param #boolean switch Enable=true or disable=false fall back of group. +function SUPPRESSION:Fallback(switch) + if switch==nil then + switch=true + end + self.FallbackON=switch +end + +--- Set distance a group will fall back when it gets hit. +-- @param #SUPPRESSION self +-- @param #number distance Distance in meters. +function SUPPRESSION:SetFallbackDistance(distance) + self.FallbackDist=distance +end + +--- Set time a group waits at its fall back position before it resumes its normal mission. +-- @param #SUPPRESSION self +-- @param #number time Time in seconds. +function SUPPRESSION:SetFallbackWait(time) + self.FallbackWait=time +end + +--- Enable take cover option if a unit is hit. +-- @param #SUPPRESSION self +-- @param #boolean switch Enable=true or disable=false fall back of group. +function SUPPRESSION:Takecover(switch) + if switch==nil then + switch=true + end + self.TakecoverON=switch +end + +--- Set time a group waits at its hideout position before it resumes its normal mission. +-- @param #SUPPRESSION self +-- @param #number time Time in seconds. +function SUPPRESSION:SetTakecoverWait(time) + self.TakecoverWait=time +end + +--- Set distance a group searches for hideout places. +-- @param #SUPPRESSION self +-- @param #number range Search range in meters. +function SUPPRESSION:SetTakecoverRange(range) + self.TakecoverRange=range +end + +--- Set hideout place explicitly. +-- @param #SUPPRESSION self +-- @param Core.Point#COORDINATE Hideout Place where the group will hide after the TakeCover event. +function SUPPRESSION:SetTakecoverPlace(Hideout) + self.hideout=Hideout +end + +--- Set minimum probability that a group flees (falls back or takes cover) after a hit event. Default is 10%. +-- @param #SUPPRESSION self +-- @param #number probability Probability in percent. +function SUPPRESSION:SetMinimumFleeProbability(probability) + self.PminFlee=probability or 10 +end + +--- Set maximum probability that a group flees (falls back or takes cover) after a hit event. Default is 90%. +-- @param #SUPPRESSION self +-- @param #number probability Probability in percent. +function SUPPRESSION:SetMaximumFleeProbability(probability) + self.PmaxFlee=probability or 90 +end + +--- Set damage threshold before a group is ordered to retreat if a retreat zone was defined. +-- If the group consists of only a singe unit, this referrs to the life of the unit. +-- If the group consists of more than one unit, this referrs to the group strength relative to its initial strength. +-- @param #SUPPRESSION self +-- @param #number damage Damage in percent. If group gets damaged above this value, the group will retreat. Default 50 %. +function SUPPRESSION:SetRetreatDamage(damage) + self.RetreatDamage=damage or 50 +end + +--- Set time a group waits in the retreat zone before it resumes its mission. Default is two hours. +-- @param #SUPPRESSION self +-- @param #number time Time in seconds. Default 7200 seconds = 2 hours. +function SUPPRESSION:SetRetreatWait(time) + self.RetreatWait=time or 7200 +end + +--- Set alarm state a group will get after it returns from a fall back or take cover. +-- @param #SUPPRESSION self +-- @param #string alarmstate Alarm state. Possible "Auto", "Green", "Red". Default is "Auto". +function SUPPRESSION:SetDefaultAlarmState(alarmstate) + if alarmstate:lower()=="auto" then + self.DefaultAlarmState=SUPPRESSION.AlarmState.Auto + elseif alarmstate:lower()=="green" then + self.DefaultAlarmState=SUPPRESSION.AlarmState.Green + elseif alarmstate:lower()=="red" then + self.DefaultAlarmState=SUPPRESSION.AlarmState.Red + else + self.DefaultAlarmState=SUPPRESSION.AlarmState.Auto + end +end + +--- Set Rules of Engagement (ROE) a group will get when it recovers from suppression. +-- @param #SUPPRESSION self +-- @param #string roe ROE after suppression. Possible "Free", "Hold" or "Return". Default "Free". +function SUPPRESSION:SetDefaultROE(roe) + if roe:lower()=="free" then + self.DefaultROE=SUPPRESSION.ROE.Free + elseif roe:lower()=="hold" then + self.DefaultROE=SUPPRESSION.ROE.Hold + elseif roe:lower()=="return" then + self.DefaultROE=SUPPRESSION.ROE.Return + else + self.DefaultROE=SUPPRESSION.ROE.Free + end +end + +--- Create an F10 menu entry for the suppressed group. The menu is mainly for Debugging purposes. +-- @param #SUPPRESSION self +-- @param #boolean switch Enable=true or disable=false menu group. Default is true. +function SUPPRESSION:MenuOn(switch) + if switch==nil then + switch=true + end + self.MenuON=switch +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create F10 main menu, i.e. F10/Suppression. The menu is mainly for Debugging purposes. +-- @param #SUPPRESSION self +function SUPPRESSION:_CreateMenuGroup() + local SubMenuName=self.Controllable:GetName() + local MenuGroup=MENU_MISSION:New(SubMenuName, SUPPRESSION.MenuF10) + MENU_MISSION_COMMAND:New("Fallback!", MenuGroup, self.OrderFallBack, self) + MENU_MISSION_COMMAND:New("Take Cover!", MenuGroup, self.OrderTakeCover, self) + MENU_MISSION_COMMAND:New("Retreat!", MenuGroup, self.OrderRetreat, self) + MENU_MISSION_COMMAND:New("Report Status", MenuGroup, self.Status, self, true) +end + +--- Order group to fall back between 100 and 150 meters in a random direction. +-- @param #SUPPRESSION self +function SUPPRESSION:OrderFallBack() + local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + local vicinity=group:GetCoordinate():GetRandomVec2InRadius(150, 100) + local coord=COORDINATE:NewFromVec2(vicinity) + self:FallBack(self.Controllable) +end + +--- Order group to take cover at a nearby scenery object. +-- @param #SUPPRESSION self +function SUPPRESSION:OrderTakeCover() + -- Search place to hide or take specified one. + local Hideout=self.hideout + if self.hideout==nil then + Hideout=self:_SearchHideout() + end + -- Trigger TakeCover event. + self:TakeCover(Hideout) +end + +--- Order group to retreat to a pre-defined zone. +-- @param #SUPPRESSION self +function SUPPRESSION:OrderRetreat() + self:Retreat() +end + +--- Status of group. Current ROE, alarm state, life. +-- @param #SUPPRESSION self +-- @param #boolean message Send message to all players. +function SUPPRESSION:Status(message) + + local name=self.Controllable:GetName() + local nunits=#self.Controllable:GetUnits() + local roe=self.CurrentROE + local state=self.CurrentAlarmState + local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife() + + local text=string.format("Status of group %s\n", name) + text=text..string.format("Number of units: %d of %d\n", nunits, self.IniGroupStrength) + text=text..string.format("Current state: %s\n", self:GetState()) + text=text..string.format("ROE: %s\n", roe) + text=text..string.format("Alarm state: %s\n", state) + text=text..string.format("Hits taken: %d\n", self.Nhit) + text=text..string.format("Life min: %3.0f\n", life_min) + text=text..string.format("Life max: %3.0f\n", life_max) + text=text..string.format("Life ave: %3.0f\n", life_ave) + text=text..string.format("Life ave0: %3.0f\n", life_ave0) + text=text..string.format("Group strength: %3.0f", groupstrength) + + MESSAGE:New(text, 10):ToAllIf(message or self.Debug) + self:T(SUPPRESSION.id.."\n"..text) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Start" event. Initialized ROE and alarm state. Starts the event handler. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterStart(Controllable, From, Event, To) + self:_EventFromTo("onafterStart", Event, From, To) + + local text=string.format("Started SUPPRESSION for group %s.", Controllable:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + + local rzone="not defined" + if self.RetreatZone then + rzone=self.RetreatZone:GetName() + end + + -- Set retreat damage value if it was not set by user input. + if self.RetreatDamage==nil then + if self.RetreatZone then + if self.IniGroupStrength==1 then + self.RetreatDamage=60.0 -- 40% of life is left. + elseif self.IniGroupStrength==2 then + self.RetreatDamage=50.0 -- 50% of group left, i.e. 1 of 2. We already order a retreat, because if for a group 2 two a zone is defined it would not be used at all. + else + self.RetreatDamage=66.5 -- 34% of the group is left, e.g. 1 of 3,4 or 5, 2 of 6,7 or 8, 3 of 9,10 or 11, 4/12, 4/13, 4/14, 5/15, ... + end + else + self.RetreatDamage=100 -- If no retreat then this should be set to 100%. + end + end + + -- Create main F10 menu if it is not there yet. + if self.MenuON then + if not SUPPRESSION.MenuF10 then + SUPPRESSION.MenuF10 = MENU_MISSION:New("Suppression") + end + self:_CreateMenuGroup() + end + + -- Set the current ROE and alam state. + self:_SetAlarmState(self.DefaultAlarmState) + self:_SetROE(self.DefaultROE) + + local text=string.format("\n******************************************************\n") + text=text..string.format("Suppressed group = %s\n", Controllable:GetName()) + text=text..string.format("Type = %s\n", self.Type) + text=text..string.format("IsInfantry = %s\n", tostring(self.IsInfantry)) + text=text..string.format("Group strength = %d\n", self.IniGroupStrength) + text=text..string.format("Average time = %5.1f seconds\n", self.Tsuppress_ave) + text=text..string.format("Minimum time = %5.1f seconds\n", self.Tsuppress_min) + text=text..string.format("Maximum time = %5.1f seconds\n", self.Tsuppress_max) + text=text..string.format("Default ROE = %s\n", self.DefaultROE) + text=text..string.format("Default AlarmState = %s\n", self.DefaultAlarmState) + text=text..string.format("Fall back ON = %s\n", tostring(self.FallbackON)) + text=text..string.format("Fall back distance = %5.1f m\n", self.FallbackDist) + text=text..string.format("Fall back wait = %5.1f seconds\n", self.FallbackWait) + text=text..string.format("Fall back heading = %s degrees\n", tostring(self.FallbackHeading)) + text=text..string.format("Take cover ON = %s\n", tostring(self.TakecoverON)) + text=text..string.format("Take cover search = %5.1f m\n", self.TakecoverRange) + text=text..string.format("Take cover wait = %5.1f seconds\n", self.TakecoverWait) + text=text..string.format("Min flee probability = %5.1f\n", self.PminFlee) + text=text..string.format("Max flee probability = %5.1f\n", self.PmaxFlee) + text=text..string.format("Retreat zone = %s\n", rzone) + text=text..string.format("Retreat damage = %5.1f %%\n", self.RetreatDamage) + text=text..string.format("Retreat wait = %5.1f seconds\n", self.RetreatWait) + text=text..string.format("Speed = %5.1f km/h\n", self.Speed) + text=text..string.format("Speed max = %5.1f km/h\n", self.SpeedMax) + text=text..string.format("Formation = %s\n", self.Formation) + text=text..string.format("******************************************************\n") + self:T(SUPPRESSION.id..text) + + -- Add event handler. + if self.eventmoose then + self:HandleEvent(EVENTS.Hit, self._OnEventHit) + self:HandleEvent(EVENTS.Dead, self._OnEventDead) + else + world.addEventHandler(self) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Hit" event. (Of course, this is not really before the group got hit.) +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Unit Unit that was hit. +-- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. +-- @return boolean +function SUPPRESSION:onbeforeHit(Controllable, From, Event, To, Unit, AttackUnit) + self:_EventFromTo("onbeforeHit", Event, From, To) + + --local Tnow=timer.getTime() + --env.info(SUPPRESSION.id..string.format("Last hit = %s %s", tostring(self.LastHit), tostring(Tnow))) + + return true +end + +--- After "Hit" event. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Unit Unit that was hit. +-- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. +function SUPPRESSION:onafterHit(Controllable, From, Event, To, Unit, AttackUnit) + self:_EventFromTo("onafterHit", Event, From, To) + + -- Suppress unit. + if From=="CombatReady" or From=="Suppressed" then + self:_Suppress() + end + + -- Get life of group in %. + local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife() + + -- Damage in %. If group consists only of one unit, we take its life value. + local Damage=100-life_ave0 + + -- Condition for retreat. + local RetreatCondition = Damage >= self.RetreatDamage-0.01 and self.RetreatZone + + -- Probability that a unit flees. The probability increases linearly with the damage of the group/unit. + -- If Damage=0 ==> P=Pmin + -- if Damage=RetreatDamage ==> P=Pmax + -- If no retreat zone has been specified, RetreatDamage is 100. + local Pflee=(self.PmaxFlee-self.PminFlee)/self.RetreatDamage * math.min(Damage, self.RetreatDamage) + self.PminFlee + + -- Evaluate flee condition. + local P=math.random(0,100) + local FleeCondition = P < Pflee + + local text + text=string.format("\nGroup %s: Life min=%5.1f, max=%5.1f, ave=%5.1f, ave0=%5.1f group=%5.1f\n", Controllable:GetName(), life_min, life_max, life_ave, life_ave0, groupstrength) + text=string.format("Group %s: Damage = %8.4f (%8.4f retreat threshold).\n", Controllable:GetName(), Damage, self.RetreatDamage) + text=string.format("Group %s: P_Flee = %5.1f %5.1f=P_rand (P_Flee > Prand ==> Flee)\n", Controllable:GetName(), Pflee, P) + self:T(SUPPRESSION.id..text) + + -- Group is obviously destroyed. + if Damage >= 99.9 then + return + end + + if RetreatCondition then + + -- Trigger Retreat event. + self:Retreat() + + elseif FleeCondition then + + if self.FallbackON and AttackUnit:IsGround() then + + -- Trigger FallBack event. + self:FallBack(AttackUnit) + + elseif self.TakecoverON then + + -- Search place to hide or take specified one. + local Hideout=self.hideout + if self.hideout==nil then + Hideout=self:_SearchHideout() + end + + -- Trigger TakeCover event. + self:TakeCover(Hideout) + end + end + + -- Give info on current status. + if self.Debug then + self:Status() + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Recovered" event. Check if suppression time is over. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean +function SUPPRESSION:onbeforeRecovered(Controllable, From, Event, To) + self:_EventFromTo("onbeforeRecovered", Event, From, To) + + -- Current time. + local Tnow=timer.getTime() + + -- Debug info + self:T(SUPPRESSION.id..string.format("onbeforeRecovered: Time now: %d - Time over: %d", Tnow, self.TsuppressionOver)) + + -- Recovery is only possible if enough time since the last hit has passed. + if Tnow >= self.TsuppressionOver then + return true + else + return false + end + +end + +--- After "Recovered" event. Group has recovered and its ROE is set back to the "normal" unsuppressed state. Optionally the group is flared green. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterRecovered(Controllable, From, Event, To) + self:_EventFromTo("onafterRecovered", Event, From, To) + + if Controllable and Controllable:IsAlive() then + + -- Debug message. + local text=string.format("Group %s has recovered!", Controllable:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(SUPPRESSION.id..text) + + -- Set ROE back to default. + self:_SetROE() + + -- Flare unit green. + if self.flare or self.Debug then + Controllable:FlareGreen() + end + + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "FightBack" event. ROE and Alarm state are set back to default. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterFightBack(Controllable, From, Event, To) + self:_EventFromTo("onafterFightBack", Event, From, To) + + -- Set ROE and alarm state back to default. + self:_SetROE() + self:_SetAlarmState() +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "FallBack" event. We check that group is not already falling back. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. +-- @return #boolean +function SUPPRESSION:onbeforeFallBack(Controllable, From, Event, To, AttackUnit) + self:_EventFromTo("onbeforeFallBack", Event, From, To) + + --TODO: Add retreat? Only allowd transition is Suppressed-->Fallback. So in principle no need. + if From == "FallingBack" then + return false + else + return true + end +end + +--- After "FallBack" event. We get the heading away from the attacker and route the group a certain distance in that direction. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. +function SUPPRESSION:onafterFallBack(Controllable, From, Event, To, AttackUnit) + self:_EventFromTo("onafterFallback", Event, From, To) + + -- Debug info + self:T(SUPPRESSION.id..string.format("Group %s is falling back after %d hits.", Controllable:GetName(), self.Nhit)) + + -- Coordinate of the attacker and attacked unit. + local ACoord=AttackUnit:GetCoordinate() + local DCoord=Controllable:GetCoordinate() + + -- Heading from attacker to attacked unit. + local heading=self:_Heading(ACoord, DCoord) + + -- Overwrite heading with user specified heading. + if self.FallbackHeading then + heading=self.FallbackHeading + end + + -- Create a coordinate ~ 100 m in opposite direction of the attacking unit. + local Coord=DCoord:Translate(self.FallbackDist, heading) + + -- Place marker + local MarkerID=Coord:MarkToAll("Fall back position for group "..Controllable:GetName()) + + -- Smoke the coordinate. + if self.smoke or self.Debug then + Coord:SmokeBlue() + end + + -- Set ROE to weapon hold. + self:_SetROE(SUPPRESSION.ROE.Hold) + + -- Set alarm state to GREEN and let the unit run away. + self:_SetAlarmState(SUPPRESSION.AlarmState.Green) + + -- Make the group run away. + self:_Run(Coord, self.Speed, self.Formation, self.FallbackWait) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "TakeCover" event. Search an area around the group for possible scenery objects where the group can hide. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Hideout Place where the group will hide. +-- @return #boolean +function SUPPRESSION:onbeforeTakeCover(Controllable, From, Event, To, Hideout) + self:_EventFromTo("onbeforeTakeCover", Event, From, To) + + --TODO: Need to test this! + if From=="TakingCover" then + return false + end + + -- Block transition if no hideout place is given. + if Hideout ~= nil then + return true + else + return false + end + +end + +--- After "TakeCover" event. Group will run to a nearby scenery object and "hide" there for a certain time. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Hideout Place where the group will hide. +function SUPPRESSION:onafterTakeCover(Controllable, From, Event, To, Hideout) + self:_EventFromTo("onafterTakeCover", Event, From, To) + + if self.Debug then + local MarkerID=Hideout:MarkToAll(string.format("Hideout for group %s", Controllable:GetName())) + end + + -- Smoke place of hideout. + if self.smoke or self.Debug then + Hideout:SmokeBlue() + end + + -- Set ROE to weapon hold. + self:_SetROE(SUPPRESSION.ROE.Hold) + + -- Set the ALARM STATE to GREEN. Then the unit will move even if it is under fire. + self:_SetAlarmState(SUPPRESSION.AlarmState.Green) + + -- Make the group run away. + self:_Run(Hideout, self.Speed, self.Formation, self.TakecoverWait) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Retreat" event. We check that the group is not already retreating. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean True if transition is allowed, False if transition is forbidden. +function SUPPRESSION:onbeforeRetreat(Controllable, From, Event, To) + self:_EventFromTo("onbeforeRetreat", Event, From, To) + + if From=="Retreating" then + local text=string.format("Group %s is already retreating.") + self:T2(SUPPRESSION.id..text) + return false + else + return true + end + +end + +--- After "Retreat" event. Find a random point in the retreat zone and route the group there. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterRetreat(Controllable, From, Event, To) + self:_EventFromTo("onafterRetreat", Event, From, To) + + -- Route the group to a zone. + local text=string.format("Group %s is retreating! Alarm state green.", Controllable:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(SUPPRESSION.id..text) + + -- Get a random point in the retreat zone. + local ZoneCoord=self.RetreatZone:GetRandomCoordinate() -- Core.Point#COORDINATE + local ZoneVec2=ZoneCoord:GetVec2() + + -- Debug smoke zone and point. + if self.smoke or self.Debug then + ZoneCoord:SmokeBlue() + end + if self.Debug then + self.RetreatZone:SmokeZone(SMOKECOLOR.Red, 12) + end + + -- Set ROE to weapon hold. + self:_SetROE(SUPPRESSION.ROE.Hold) + + -- Set the ALARM STATE to GREEN. Then the unit will move even if it is under fire. + self:_SetAlarmState(SUPPRESSION.AlarmState.Green) + + -- Make unit run to retreat zone and wait there for ~two hours. + self:_Run(ZoneCoord, self.Speed, self.Formation, self.RetreatWait) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Retreateded" event. Check that the group is really in the retreat zone. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onbeforeRetreated(Controllable, From, Event, To) + self:_EventFromTo("onbeforeRetreated", Event, From, To) + + -- Check that the group is inside the zone. + local inzone=self.RetreatZone:IsVec3InZone(Controllable:GetVec3()) + + return inzone +end + +--- After "Retreateded" event. Group has reached the retreat zone. Set ROE to return fire and alarm state to auto. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterRetreated(Controllable, From, Event, To) + self:_EventFromTo("onafterRetreated", Event, From, To) + + -- Set ROE to weapon return fire. + self:_SetROE(SUPPRESSION.ROE.Return) + + -- Set the ALARM STATE to GREEN. Then the unit will move even if it is under fire. + self:_SetAlarmState(SUPPRESSION.AlarmState.Auto) + + -- TODO: Add hold task? Move from _Run() +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Dead" event, when a unit has died. When all units of a group are dead, FSM is stopped and eventhandler removed. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterDead(Controllable, From, Event, To) + self:_EventFromTo("onafterDead", Event, From, To) + + -- Number of units left in the group. + local nunits=#self.Controllable:GetUnits() + + local text=string.format("Group %s: One of our units just died! %d units left.", self.Controllable:GetName(), nunits) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(SUPPRESSION.id..text) + + -- Go to stop state. + if nunits==0 then + self:T(SUPPRESSION.id..string.format("Stopping SUPPRESSION for group %s.", Controllable:GetName())) + self:Stop() + if self.mooseevents then + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.Hit) + else + world.removeEventHandler(self) + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Event Handler +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event handler for suppressed groups. +--@param #SUPPRESSION self +function SUPPRESSION:onEvent(Event) + --self:E(event) + + if Event == nil or Event.initiator == nil or Unit.getByName(Event.initiator:getName()) == nil then + return true + end + + local EventData={} + if Event.initiator then + EventData.IniDCSUnit = Event.initiator + EventData.IniUnitName = Event.initiator:getName() + EventData.IniDCSGroup = Event.initiator:getGroup() + EventData.IniGroupName = Event.initiator:getGroup():getName() + EventData.IniGroup = GROUP:FindByName(EventData.IniGroupName) + EventData.IniUnit = UNIT:FindByName(EventData.IniUnitName) + end + + if Event.target then + EventData.TgtDCSUnit = Event.target + EventData.TgtUnitName = Event.target:getName() + EventData.TgtDCSGroup = Event.target:getGroup() + EventData.TgtGroupName = Event.target:getGroup():getName() + EventData.TgtGroup = GROUP:FindByName(EventData.TgtGroupName) + EventData.TgtUnit = UNIT:FindByName(EventData.TgtUnitName) + end + + + -- Event HIT + if Event.id == world.event.S_EVENT_HIT then + self:_OnEventHit(EventData) + end + + -- Event HIT + if Event.id == world.event.S_EVENT_DEAD then + self:_OnEventDead(EventData) + end + +end + +--- Event handler for Dead event of suppressed groups. +-- @param #SUPPRESSION self +-- @param Core.Event#EVENTDATA EventData +function SUPPRESSION:_OnEventHit(EventData) + + local GroupNameSelf=self.Controllable:GetName() + local GroupNameTgt=EventData.TgtGroupName + local TgtUnit=EventData.TgtUnit + local tgt=EventData.TgtDCSUnit + local IniUnit=EventData.IniUnit + + -- Check that correct group was hit. + if GroupNameTgt == GroupNameSelf then + + self:T2(SUPPRESSION.id..string.format("Hit event at t = %5.1f", timer.getTime())) + + -- Flare unit that was hit. + if self.flare or self.Debug then + TgtUnit:FlareRed() + end + + -- Increase Hit counter. + self.Nhit=self.Nhit+1 + + -- Info on hit times. + self:T(SUPPRESSION.id..string.format("Group %s has just been hit %d times.", self.Controllable:GetName(), self.Nhit)) + + --self:Status() + local life=tgt:getLife()/(tgt:getLife0()+1)*100 + self:T2(SUPPRESSION.id..string.format("Target unit life = %5.1f", life)) + + -- FSM Hit event. + self:__Hit(3, TgtUnit, IniUnit) + end + +end + +--- Event handler for Dead event of suppressed groups. +-- @param #SUPPRESSION self +-- @param Core.Event#EVENTDATA EventData +function SUPPRESSION:_OnEventDead(EventData) + + local GroupNameSelf=self.Controllable:GetName() + local GroupNameIni=EventData.IniGroupName + + -- Check for correct group. + if GroupNameIni== GroupNameSelf then + + -- Dead Unit. + local IniUnit=EventData.IniUnit --Wrapper.Unit#UNIT + local IniUnitName=EventData.IniUnitName + + if EventData.IniUnit then + self:T2(SUPPRESSION.id..string.format("Group %s: Dead MOOSE unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName)) + else + self:T2(SUPPRESSION.id..string.format("Group %s: Dead MOOSE unit DOES NOT not exist! Unit name %s.", GroupNameIni, IniUnitName)) + end + + if EventData.IniDCSUnit then + self:T2(SUPPRESSION.id..string.format("Group %s: Dead DCS unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName)) + else + self:T2(SUPPRESSION.id..string.format("Group %s: Dead DCS unit DOES NOT exist! Unit name %s.", GroupNameIni, IniUnitName)) + end + + -- Flare unit that died. + if IniUnit and (self.flare or self.Debug) then + IniUnit:FlareWhite() + self:T(SUPPRESSION.id..string.format("Flare Dead MOOSE unit.")) + end + + -- Flare unit that died. + if EventData.IniDCSUnit and (self.flare or self.Debug) then + local p=EventData.IniDCSUnit:getPosition().p + trigger.action.signalFlare(p, trigger.flareColor.Yellow , 0) + self:T(SUPPRESSION.id..string.format("Flare Dead DCS unit.")) + end + + -- Get status. + self:Status() + + -- FSM Dead event. + self:__Dead(0.1) + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Suppress fire of a unit by setting its ROE to "Weapon Hold". +-- @param #SUPPRESSION self +function SUPPRESSION:_Suppress() + + -- Current time. + local Tnow=timer.getTime() + + -- Controllable + local Controllable=self.Controllable --Wrapper.Controllable#CONTROLLABLE + + -- Group will hold their weapons. + self:_SetROE(SUPPRESSION.ROE.Hold) + + -- Get randomized time the unit is suppressed. + local sigma=(self.Tsuppress_max-self.Tsuppress_min)/4 + local Tsuppress=self:_Random_Gaussian(self.Tsuppress_ave,sigma,self.Tsuppress_min, self.Tsuppress_max) + + -- Time at which the suppression is over. + local renew=true + if self.TsuppressionOver ~= nil then + if Tsuppress+Tnow > self.TsuppressionOver then + self.TsuppressionOver=Tnow+Tsuppress + else + renew=false + end + else + self.TsuppressionOver=Tnow+Tsuppress + end + + -- Recovery event will be called in Tsuppress seconds. + if renew then + self:__Recovered(self.TsuppressionOver-Tnow) + end + + -- Debug message. + local text=string.format("Group %s is suppressed for %d seconds. Suppression ends at %d:%02d.", Controllable:GetName(), Tsuppress, self.TsuppressionOver/60, self.TsuppressionOver%60) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(SUPPRESSION.id..text) + +end + + +--- Make group run/drive to a certain point. We put in several intermediate waypoints because sometimes the group stops before it arrived at the desired point. +--@param #SUPPRESSION self +--@param Core.Point#COORDINATE fin Coordinate where we want to go. +--@param #number speed Speed of group. Default is 999. +--@param #string formation Formation of group. Default is "Vee". +--@param #number wait Time the group will wait/hold at final waypoint. Default is 30 seconds. +function SUPPRESSION:_Run(fin, speed, formation, wait) + + speed=speed or 999 + formation=formation or "Vee" + wait=wait or 30 + + local group=self.Controllable -- Wrapper.Controllable#CONTROLLABLE + + -- Clear all tasks. + group:ClearTasks() + + -- Current coordinates of group. + local ini=group:GetCoordinate() + + -- Distance between current and final point. + local dist=ini:Get2DDistance(fin) + + -- Heading from ini to fin. + local heading=self:_Heading(ini, fin) + + -- Number of waypoints. + local nx + if dist <= 50 then + nx=2 + elseif dist <= 100 then + nx=3 + elseif dist <= 500 then + nx=4 + else + nx=5 + end + + -- Number of intermediate waypoints. + local dx=dist/(nx-1) + + -- Waypoint and task arrays. + local wp={} + local tasks={} + + -- First waypoint is the current position of the group. + wp[1]=ini:WaypointGround(speed, formation) + local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName())) + tasks[1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, 1, false) + + self:T2(SUPPRESSION.id..string.format("Number of waypoints %d", nx)) + for i=1,nx-2 do + + local x=dx*i + local coord=ini:Translate(x, heading) + + wp[#wp+1]=coord:WaypointGround(speed, formation) + tasks[#tasks+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, false) + + self:T2(SUPPRESSION.id..string.format("%d x = %4.1f", i, x)) + if self.Debug then + local MarkerID=coord:MarkToAll(string.format("Waypoing %d of group %s", #wp, self.Controllable:GetName())) + end + + end + self:T2(SUPPRESSION.id..string.format("Total distance: %4.1f", dist)) + + -- Final waypoint. + wp[#wp+1]=fin:WaypointGround(speed, formation) + if self.Debug then + local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)", #wp, self.Controllable:GetName())) + end + + -- Task to hold. + local ConditionWait=group:TaskCondition(nil, nil, nil, nil, wait, nil) + local TaskHold = group:TaskHold() + + -- Task combo to make group hold at final waypoint. + local TaskComboFin = {} + TaskComboFin[#TaskComboFin+1] = group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, true) + TaskComboFin[#TaskComboFin+1] = group:TaskControlled(TaskHold, ConditionWait) + + -- Add final task. + tasks[#tasks+1]=group:TaskCombo(TaskComboFin) + + -- Original waypoints of the group. + local Waypoints = group:GetTemplateRoutePoints() + + -- New points are added to the default route. + for i,p in ipairs(wp) do + table.insert(Waypoints, i, wp[i]) + end + + -- Set task for all waypoints. + for i,wp in ipairs(Waypoints) do + group:SetTaskWaypoint(Waypoints[i], tasks[i]) + end + + -- Submit task and route group along waypoints. + group:Route(Waypoints) + +end + +--- Function called when group is passing a waypoint. At the last waypoint we set the group back to CombatReady. +--@param Wrapper.Group#GROUP group Group which is passing a waypoint. +--@param #SUPPRESSION Fsm The suppression object. +--@param #number i Waypoint number that has been reached. +--@param #boolean final True if it is the final waypoint. Start Fightback. +function SUPPRESSION._Passing_Waypoint(group, Fsm, i, final) + + -- Debug message. + local text=string.format("Group %s passing waypoint %d (final=%s)", group:GetName(), i, tostring(final)) + MESSAGE:New(text,10):ToAllIf(Fsm.Debug) + if Fsm.Debug then + env.info(SUPPRESSION.id..text) + end + + if final then + if Fsm:is("Retreating") then + -- Retreated-->Retreated. + Fsm:Retreated() + else + -- FightBack-->Combatready: Change alarm state back to default. + Fsm:FightBack() + end + end +end + + +--- Search a place to hide. This is any scenery object in the vicinity. +--@param #SUPPRESSION self +--@return Core.Point#COORDINATE Coordinate of the hideout place. +--@return nil If no scenery object is within search radius. +function SUPPRESSION:_SearchHideout() + -- We search objects in a zone with radius ~300 m around the group. + local Zone = ZONE_GROUP:New("Zone_Hiding", self.Controllable, self.TakecoverRange) + local gpos = self.Controllable:GetCoordinate() + + -- Scan for Scenery objects to run/drive to. + Zone:Scan(Object.Category.SCENERY) + + -- Array with all possible hideouts, i.e. scenery objects in the vicinity of the group. + local hideouts={} + + for SceneryTypeName, SceneryData in pairs(Zone:GetScannedScenery()) do + for SceneryName, SceneryObject in pairs(SceneryData) do + + local SceneryObject = SceneryObject -- Wrapper.Scenery#SCENERY + + -- Position of the scenery object. + local spos=SceneryObject:GetCoordinate() + + -- Distance from group to hideout. + local distance= spos:Get2DDistance(gpos) + + if self.Debug then + -- Place markers on every possible scenery object. + local MarkerID=SceneryObject:GetCoordinate():MarkToAll(string.format("%s scenery object %s", self.Controllable:GetName(),SceneryObject:GetTypeName())) + local text=string.format("%s scenery: %s, Coord %s", self.Controllable:GetName(), SceneryObject:GetTypeName(), SceneryObject:GetCoordinate():ToStringLLDMS()) + self:T2(SUPPRESSION.id..text) + end + + -- Add to table. + table.insert(hideouts, {object=SceneryObject, distance=distance}) + end + end + + -- Get random hideout place. + local Hideout=nil + if #hideouts>0 then + + -- Debug info. + self:T(SUPPRESSION.id.."Number of hideouts "..#hideouts) + + -- Sort results table wrt number of hits. + local _sort = function(a,b) return a.distance < b.distance end + table.sort(hideouts,_sort) + + -- Pick a random location. + --Hideout=hideouts[math.random(#hideouts)].object + + -- Pick closest location. + Hideout=hideouts[1].object:GetCoordinate() + + else + self:E(SUPPRESSION.id.."No hideouts found!") + end + + return Hideout + +end + +--- Get (relative) life in percent of a group. Function returns the value of the units with the smallest and largest life. Also the average value of all groups is returned. +-- @param #SUPPRESSION self +-- @return #number Smallest life value of all units. +-- @return #number Largest life value of all units. +-- @return #number Average life value of all alife groups +-- @return #number Average life value of all groups including already dead ones. +-- @return #number Relative group strength. +function SUPPRESSION:_GetLife() + + local group=self.Controllable --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + local units=group:GetUnits() + + local life_min=nil + local life_max=nil + local life_ave=0 + local life_ave0=0 + local n=0 + + local groupstrength=#units/self.IniGroupStrength*100 + + self.T2(SUPPRESSION.id..string.format("Group %s _GetLife nunits = %d", self.Controllable:GetName(), #units)) + + for _,unit in pairs(units) do + + local unit=unit -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + n=n+1 + local life=unit:GetLife()/(unit:GetLife0()+1)*100 + if life_min==nil or life < life_min then + life_min=life + end + if life_max== nil or life > life_max then + life_max=life + end + life_ave=life_ave+life + if self.Debug then + local text=string.format("n=%02d: Life = %3.1f, Life0 = %3.1f, min=%3.1f, max=%3.1f, ave=%3.1f, group=%3.1f", n, unit:GetLife(), unit:GetLife0(), life_min, life_max, life_ave/n,groupstrength) + self:T2(SUPPRESSION.id..text) + end + end + + end + + -- If the counter did not increase (can happen!) return 0 + if n==0 then + return 0,0,0,0,0 + end + + -- Average life relative to initial group strength including the dead ones. + life_ave0=life_ave/self.IniGroupStrength + + -- Average life of all alive units. + life_ave=life_ave/n + + return life_min, life_max, life_ave, life_ave0, groupstrength + else + return 0, 0, 0, 0, 0 + end +end + + +--- Heading from point a to point b in degrees. +--@param #SUPPRESSION self +--@param Core.Point#COORDINATE a Coordinate. +--@param Core.Point#COORDINATE b Coordinate. +--@return #number angle Angle from a to b in degrees. +function SUPPRESSION:_Heading(a, b, distance) + local dx = b.x-a.x + local dy = b.z-a.z + local angle = math.deg(math.atan2(dy,dx)) + if angle < 0 then + angle = 360 + angle + end + return angle +end + +--- Generate Gaussian pseudo-random numbers. +-- @param #SUPPRESSION self +-- @param #number x0 Expectation value of distribution. +-- @param #number sigma (Optional) Standard deviation. Default 10. +-- @param #number xmin (Optional) Lower cut-off value. +-- @param #number xmax (Optional) Upper cut-off value. +-- @return #number Gaussian random number. +function SUPPRESSION:_Random_Gaussian(x0, sigma, xmin, xmax) + + -- Standard deviation. Default 5 if not given. + sigma=sigma or 5 + + local r + local gotit=false + local i=0 + while not gotit do + + -- Uniform numbers in [0,1). We need two. + local x1=math.random() + local x2=math.random() + + -- Transform to Gaussian exp(-(x-x0)²/(2*sigma²). + r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0 + + i=i+1 + if (r>=xmin and r<=xmax) or i>100 then + gotit=true + end + end + + return r + +end + +--- Sets the ROE for the group and updates the current ROE variable. +-- @param #SUPPRESSION self +-- @param #string roe ROE the group will get. Possible "Free", "Hold", "Return". Default is self.DefaultROE. +function SUPPRESSION:_SetROE(roe) + local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + + -- If no argument is given, we take the default ROE. + roe=roe or self.DefaultROE + + -- Update the current ROE. + self.CurrentROE=roe + + -- Set the ROE. + if roe==SUPPRESSION.ROE.Free then + group:OptionROEOpenFire() + elseif roe==SUPPRESSION.ROE.Hold then + group:OptionROEHoldFire() + elseif roe==SUPPRESSION.ROE.Return then + group:OptionROEReturnFire() + else + self:E(SUPPRESSION.id.."Unknown ROE requested: "..tostring(roe)) + group:OptionROEOpenFire() + self.CurrentROE=SUPPRESSION.ROE.Free + end + + local text=string.format("Group %s now has ROE %s.", self.Controllable:GetName(), self.CurrentROE) + self:T(SUPPRESSION.id..text) +end + +--- Sets the alarm state of the group and updates the current alarm state variable. +-- @param #SUPPRESSION self +-- @param #string state Alarm state the group will get. Possible "Auto", "Green", "Red". Default is self.DefaultAlarmState. +function SUPPRESSION:_SetAlarmState(state) + local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + + -- Input or back to default alarm state. + state=state or self.DefaultAlarmState + + -- Update the current alam state of the group. + self.CurrentAlarmState=state + + -- Set the alarm state. + if state==SUPPRESSION.AlarmState.Auto then + group:OptionAlarmStateAuto() + elseif state==SUPPRESSION.AlarmState.Green then + group:OptionAlarmStateGreen() + elseif state==SUPPRESSION.AlarmState.Red then + group:OptionAlarmStateRed() + else + self:E(SUPPRESSION.id.."Unknown alarm state requested: "..tostring(state)) + group:OptionAlarmStateAuto() + self.CurrentAlarmState=SUPPRESSION.AlarmState.Auto + end + + local text=string.format("Group %s now has Alarm State %s.", self.Controllable:GetName(), self.CurrentAlarmState) + self:T(SUPPRESSION.id..text) +end + +--- Print event-from-to string to DCS log file. +-- @param #SUPPRESSION self +-- @param #string BA Before/after info. +-- @param #string Event Event. +-- @param #string From From state. +-- @param #string To To state. +function SUPPRESSION:_EventFromTo(BA, Event, From, To) + local text=string.format("\n%s: %s EVENT %s: %s --> %s", BA, self.Controllable:GetName(), Event, From, To) + self:T(SUPPRESSION.id..text) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + From ed0c5b2264719c8b1a1833c67b2f5c83c50e01c4 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 5 Apr 2018 22:44:33 +0200 Subject: [PATCH 02/31] Added Range 1.0.3 Just copied from other branch. --- Moose Development/Moose/Functional/Range.lua | 66 +++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 3c946ea79..e3e3590cb 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -227,7 +227,7 @@ RANGE.id="RANGE | " --- Range script version. -- @field #number version -RANGE.version="1.0.1" +RANGE.version="1.0.3" --TODO list --TODO: Add statics for strafe pits. @@ -1390,6 +1390,9 @@ function RANGE:_CheckInZone(_unitName) else + -- Get current ammo. + local _ammo=self:_GetAmmo(_unitName) + -- Result. local _result = self.strafeStatus[_unitID] @@ -1403,9 +1406,19 @@ function RANGE:_CheckInZone(_unitName) else _result.text = "POOR PASS" end + + local shots=_result.ammo-_ammo + local accur=0 + if shots>0 then + accur=_result.hits/shots*100 + end + -- Message text. local _text=string.format("%s, %s with %d hits on target %s.", self:_myname(_unitName), _result.text, _result.hits, _result.zone.name) + if shots and accur then + _text=_text..string.format("\nTotal rounds fired %d. Accuracy %.1f %%.", shots, accur) + end -- Send message. self:_DisplayMessageToGroup(_unit, _text) @@ -1446,9 +1459,12 @@ function RANGE:_CheckInZone(_unitName) -- 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, pastfoulline=false } + 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) @@ -1550,6 +1566,52 @@ end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Helper Functions +--- Get the number of shells a unit currently has. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +-- @return Number of shells left +function RANGE:_GetAmmo(unitname) + self:F(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) + end + end + end + end + + return ammo +end + --- Mark targets on F10 map. -- @param #RANGE self -- @param #string _unitName Name of the player unit. From 303f5cb571fc3d6193402553efb699df49f39f54 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Sat, 7 Apr 2018 23:36:09 +0200 Subject: [PATCH 03/31] RANGE v1.1.0 - Missiles are now tracked. - Statics can now be used for strafe pits. - Statics are automatically recognized. - More user functions to add bomb or strafe targets. - Bomb targets are allowed to move. - Bomb targets can automatically move randomly inside the range zone. - Improved trace output. - Documentation updated. --- Moose Development/Moose/Functional/Range.lua | 783 ++++++++++++++----- 1 file changed, 569 insertions(+), 214 deletions(-) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index e3e3590cb..9748e89d6 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -16,12 +16,12 @@ -- -- ## Features -- --- * Bomb and rocket impact point from closest range target is measured and distance reported to the player. --- * Number of hits on strafing passes are counted. +-- * 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. -- * Range targets can be marked by smoke. -- * Range can be illuminated by illumination bombs for night practices. --- * Rocket or bomb impact points can be marked by smoke. +-- * 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. @@ -55,8 +55,9 @@ -- @field #string ClassName Name of the Class. -- @field #boolean Debug If true, debug info is send as messages on the screen. -- @field #string rangename Name of the range. --- @field Core.Point#COORDINATE location Coordinate of the range. --- @field #number rangeradius Radius of range defining its total size for e.g. smoking bomb impact points and sending radio messages. Default 10 km. +-- @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 #table strafeTargets Table of strafing targets. -- @field #table bombingTargets Table of targets to bomb. -- @field #number nbombtargets Number of bombing targets. @@ -79,14 +80,17 @@ -- @field #number scorebombdistance Distance from closest target up to which bomb hits are counted. Default 1000 m. -- @field #number TdelaySmoke Time delay in seconds between impact of bomb and starting the smoke. Default 3 seconds. -- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true. +-- @field #boolean trackbombs If true (default), all bomb types are tracked and impact point to closest bombing target is evaluated. +-- @field #boolean trackrockets If true (default), all rocket types are tracked and impact point to closest bombing target is evaluated. +-- @field #boolean trackmissiles If true (default), all missile types are tracked and impact point to closest bombing target is evaluated. -- @extends Core.Base#BASE ---# RANGE class, extends @{Base#BASE} -- The RANGE class enables a mission designer to easily set up practice ranges in DCS. A new RANGE object can be created with the @{#RANGE.New}(rangename) contructor. -- The parameter "rangename" 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 consits 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 or rocket to the closest range target is measured and tabulated. +-- 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. @@ -101,31 +105,34 @@ -- ## 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}(unitnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) function. +-- A strafe pit can be added to the range by the @{#RANGE.AddStrafepit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function. -- --- The first parameter defines the target. This has to be given as a lua table which contains the unit names of the targets as 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 @{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. +-- The parameter *inverseheading* turns the heading around by 180 degrees. This is sometimes useful, since the default heading of strafe target units point in the +-- wrong/opposite direction. +-- * The parameter *goodpass* defines the number of hits a pilot has to achive during a run to be judged as a "good" pass. +-- * The last parameter *foulline* sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted! -- --- 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. --- The parameter "inverseheading" turns the heading around by 180 degrees. This is sometimes useful, since the default heading of strafe target units point in the --- wrong/opposite direction. --- --- The parameter "goodpass" defines the number of hits a pilot has to achive during a run to be judges 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 @{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}(unitnames goodhitrange,static) function. +-- One ore multiple bombing targets can be added to the range by the @{#RANGE.AddBombingTargets}(targetnames, goodhitrange, randommove) function. -- --- The first parameter "unitnames" has to be a lua table, which contains the names of the units as defined in the mission editor. --- --- The 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". --- --- The final (optional) parameter "static" can be enabled (set to true) if static bomb targets are used rather than alive units. +-- * The first parameter *targetnames* has to be a lua table, which contains the names of @{Unit} and/or @{Static} objects defined in the mission editor. +-- Note that the @{Range} logic **automatically** determines, if a name belongs to a @{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. +-- +-- Another possibility to add bombing targets is the @{#RANGE.AddBombingTargetGroup}(*group, goodhitrange, randommove*) function. Here the parameter *group* is a MOOSE @{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: @@ -138,6 +145,9 @@ -- * @{#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. @@ -155,11 +165,11 @@ -- ## Examples -- -- ### Goldwater Range --- This example shows hot to set up the Barry M. Goldwater range. It consists of two strafe pits each has two targets plus three bombing targets. +-- 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. -- --- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is used in this example. --- --- -- 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. +-- -- 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"} -- @@ -167,16 +177,15 @@ -- local bombtargets={"GWR Bomb Target Circle Left", "GWR Bomb Target Circle Right", "GWR Bomb Target Hard"} -- -- -- Create a range object. --- local GoldwaterRange=RANGE:New("Goldwater Range") +-- GoldwaterRange=RANGE:New("Goldwater Range") -- --- -- Distance between foul line and strafe target. Note that this could also be done manually by simply measuring the distance between the target and the foul line in the ME. --- local strafe=UNIT:FindByName("GWR Strafe Pit Left 1") --- local foul=UNIT:FindByName("GWR Foul Line Left") --- local fouldist=strafe:GetCoordinate():Get2DDistance(foul:GetCoordinate()) +-- -- 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, 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) @@ -184,6 +193,24 @@ -- -- 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. +-- +-- ## 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. +-- -- -- -- @field #RANGE @@ -192,7 +219,8 @@ RANGE={ Debug=false, rangename=nil, location=nil, - rangeradius=10000, + rangeradius=5000, + rangezone=nil, strafeTargets={}, bombingTargets={}, nbombtargets=0, @@ -215,8 +243,32 @@ RANGE={ scorebombdistance=1000, TdelaySmoke=3.0, eventmoose=true, + trackbombs=true, + trackrockets=true, + trackmissiles=true, } +--- Default range parameters. +-- @list Defaults +RANGE.Defaults={ + goodhitrange=25, + strafemaxalt=914, + dtBombtrack=0.005, + Tmsg=30, + ndisplayresult=10, + rangeradius=5000, + TdelaySmoke=3.0, + boxlength=3000, + boxwidth=300, + goodpass=20, + goodhitrange=25, + foulline=610, +} + +--- Global list of all defined range names. +-- @field #table Names +RANGE.Names={} + --- Main radio menu. -- @field #table MenuF10 RANGE.MenuF10={} @@ -227,10 +279,13 @@ RANGE.id="RANGE | " --- Range script version. -- @field #number version -RANGE.version="1.0.3" +RANGE.version="1.1.0" ---TODO list ---TODO: Add statics for strafe pits. +--TODO list: +--TODO: Add custom weapons, which can be specified by the user. +--TODO: Check if units are still alive. +--DONE: Add statics for strafe pits. +--DONE: Add missiles. --DONE: Convert env.info() to self:T() --DONE: Add user functions. --DONE: Rename private functions, i.e. start with _functionname. @@ -251,6 +306,7 @@ function RANGE:New(rangename) 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. @@ -274,9 +330,10 @@ function RANGE:Start() local _count=0 for _,_target in pairs(self.bombingTargets) do _count=_count+1 - --_target.name + + -- Get range location. if _location==nil then - _location=_target.point --Core.Point#COORDINATE + _location=_target.target:GetCoordinate() --Core.Point#COORDINATE end end self.nbombtargets=_count @@ -285,6 +342,7 @@ function RANGE:Start() _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() @@ -293,13 +351,20 @@ function RANGE:Start() end self.nstrafetargets=_count - -- Location of the range. We simply take the first unit/target we find. - self.location=_location + -- 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 nil + 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. @@ -311,9 +376,6 @@ function RANGE:Start() if self.eventmoose then -- Events are handled my MOOSE. self:T(RANGE.id.."Events are handled by MOOSE.") - --self:HandleEvent(EVENTS.Birth, self._OnBirth) - --self:HandleEvent(EVENTS.Hit, self._OnHit) - --self:HandleEvent(EVENTS.Shot, self._OnShot) self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Hit) self:HandleEvent(EVENTS.Shot) @@ -323,6 +385,28 @@ function RANGE:Start() world.addEventHandler(self) end + -- Make bomb target move randomly within the range zone. + for _,_target in pairs(self.bombingTargets) do + + -- Check if it is a static object. + local _static=self:_CheckStatic(_target.target:GetName()) + + if _target.move and _static==false and _target.speed>1 then + local unit=_target.target --Wrapper.Unit#UNIT + _target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road") + end + + end + + -- Debug mode: smoke all targets and range zone. + if self.Debug then + self:_MarkTargetsOnMap() + self:_SmokeBombTargets() + self:_SmokeStrafeTargets() + self:_SmokeStrafeTargetBoxes() + self.rangezone:SmokeZone(SMOKECOLOR.White) + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -332,35 +416,50 @@ end -- @param #RANGE self -- @param #number maxalt Maximum altitude AGL in meters. Default is 914 m= 3000 ft. function RANGE:SetMaxStrafeAlt(maxalt) - self.strafemaxalt=maxalt or 914 + self.strafemaxalt=maxalt or RANGE.Defaults.strafemaxalt end --- Set time interval for tracking bombs. A smaller time step increases accuracy but needs more CPU time. -- @param #RANGE self -- @param #number dt Time interval in seconds. Default is 0.005 s. function RANGE:SetBombtrackTimestep(dt) - self.dtBombtrack=dt or 0.005 + self.dtBombtrack=dt or RANGE.Defaults.dtBombtrack end --- Set time how long (most) messages are displayed. -- @param #RANGE self -- @param #number time Time in seconds. Default is 30 s. function RANGE:SetMessageTimeDuration(time) - self.Tmsg=time or 30 + self.Tmsg=time or RANGE.Defaults.Tmsg end --- Set max number of player results that are displayed. -- @param #RANGE self -- @param #number nmax Number of results. Default is 10. function RANGE:SetDisplayedMaxPlayerResults(nmax) - self.ndisplayresult=nmax or 10 + self.ndisplayresult=nmax or RANGE.Defaults.ndisplayresult end --- Set range radius. Defines the area in which e.g. bomb impacts are smoked. -- @param #RANGE self --- @param #number radius Radius in km. Default 10 km. +-- @param #number radius Radius in km. Default 5 km. function RANGE:SetRangeRadius(radius) - self.rangeradius=radius*1000 or 10000 + self.rangeradius=radius*1000 or RANGE.Defaults.rangeradius +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. +-- @param #RANGE self +-- @param Core.Point#COORDINATE coordinate Coordinate of the center of the range. +function RANGE:SetRangeLocation(coordinate) + self.location=coordinate +end + +--- Set range zone. For example, no bomb impact points are smoked if a bomb falls outside of this zone. +-- If a zone is not explicitly specified, the range zone is determined by its location and radius. +-- @param #RANGE self +-- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters. +function RANGE:SetRangeLocation(zone) + self.rangezone=zone end --- Set smoke color for marking bomb targets. By default bomb targets are marked by red smoke. @@ -388,7 +487,7 @@ end -- @param #RANGE self -- @param #number delay Time delay in seconds. Default is 3 seconds. function RANGE:SetSmokeTimeDelay(delay) - self.TdelaySmoke=delay or 3.0 + self.TdelaySmoke=delay or RANGE.Defaults.TdelaySmoke end --- Enable debug modus. @@ -403,24 +502,60 @@ function RANGE:DebugOFF() self.Debug=false end +--- Enables tracking of all bomb types. Note that this is the default setting. +-- @param #RANGE self +function RANGE:TrackBombsON() + self.trackbombs=true +end + +--- Disables tracking of all bomb types. +-- @param #RANGE self +function RANGE:TrackBombsOFF() + self.trackbombs=false +end + +--- Enables tracking of all rocket types. Note that this is the default setting. +-- @param #RANGE self +function RANGE:TrackRocketsON() + self.trackrockets=true +end + +--- Disables tracking of all rocket types. +-- @param #RANGE self +function RANGE:TrackRocketsOFF() + self.trackrockets=false +end + +--- Enables tracking of all missile types. Note that this is the default setting. +-- @param #RANGE self +function RANGE:TrackMissilesON() + self.trackmissiles=true +end + +--- Disables tracking of all missile types. +-- @param #RANGE self +function RANGE:TrackMissilesOFF() + self.trackmissiles=false +end + --- Add new strafe pit. For a strafe pit, hits from guns are counted. One pit can consist of several units. -- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading. -- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach. -- @param #RANGE self --- @param #table unitnames Table of unit names defining the strafe targets. The first target in the list determines the approach zone (heading and box). +-- @param #table targetnames Table of unit or static names defining the strafe targets. The first target in the list determines the approach zone (heading and box). -- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m. -- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m. -- @param #number heading (Optional) Approach heading in Degrees. Default is heading of the unit as defined in the mission editor. -- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. -- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. -- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. -function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) - self:F({unitnames=unitnames, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) +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. - if type(unitnames) ~= "table" then - unitnames={unitnames} + if type(targetnames) ~= "table" then + targetnames={targetnames} end -- Make targets @@ -428,11 +563,34 @@ function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inversehead local center=nil --Wrapper.Unit#UNIT local ntargets=0 - for _i,_name in ipairs(unitnames) do + for _i,_name in ipairs(targetnames) do - self:T(RANGE.id..string.format("Adding strafe target #%d %s", _i, _name)) - local unit=UNIT:FindByName(_name) + -- Check if we have a static or unit object. + local _isstatic=self:_CheckStatic(_name) + + 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. if unit then table.insert(_targets, unit) -- Define center as the first unit we find @@ -440,17 +598,21 @@ function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inversehead center=unit end ntargets=ntargets+1 - else - local text=string.format("ERROR! Could not find strafe target with name %s.", _name) - self:E(RANGE.id..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug) 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 + end -- Approach box dimensions. - local l=boxlength or 3000 - local w=(boxwidth or 300)/2 + 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() @@ -469,10 +631,10 @@ function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inversehead end -- Number of hits called a "good" pass. - local goodpass=goodpass or 20 + goodpass=goodpass or RANGE.Defaults.goodpass -- Foule line distance. - local foulline=foulline or 610 + foulline=foulline or RANGE.Defaults.foulline -- Coordinate of the range. local Ccenter=center:GetCoordinate() @@ -499,96 +661,195 @@ function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inversehead --_polygon:BoundZone() -- Add zone to table. - table.insert(self.strafeTargets, {name=_name, polygon=_polygon, goodPass=goodpass, targets=_targets, foulline=foulline, smokepoints=p, heading=heading}) + 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, boxlength, boxwidth, 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 ---- Add bombing target(s) to range. --- @param #RANGE self --- @param #table unitnames Table containing the unit names acting as bomb targets. --- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m. --- @param #boolean static (Optional) Target is static. Default false. -function RANGE:AddBombingTargets(unitnames, goodhitrange, static) - self:F({unitnames=unitnames, goodhitrange=goodhitrange, static=static}) - -- Create a table if necessary. - if type(unitnames) ~= "table" then - unitnames={unitnames} - end - - if static == nil or static == false then - static=false - else - static=true - end - - -- Default range is 25 m. - goodhitrange=goodhitrange or 25 - - for _,name in pairs(unitnames) do - local _unit - local _static +--- Add all units of a group as one new strafe target pit. +-- For a strafe pit, hits from guns are counted. One pit can consist of several units. +-- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading. +-- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach. +-- @param #RANGE self +-- @param Wrapper.Group#GROUP group MOOSE group of unit names defining the strafe target pit. The first unit in the group determines the approach zone (heading and box). +-- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m. +-- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m. +-- @param #number heading (Optional) Approach heading in Degrees. Default is heading of the unit as defined in the mission editor. +-- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. +-- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. +-- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. +function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) + self:F({group=group, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) + + if group and group:IsAlive() then - if static then - - -- Add static object. Workaround since cargo objects are not yet in database because DCS function does not add those. - local _DCSstatic=StaticObject.getByName(name) - if _DCSstatic and _DCSstatic:isExist() then - self:T(RANGE.id..string.format("Adding DCS static to database. Name = %s.", name)) - _DATABASE:AddStatic(name) - else - self:E(RANGE.id..string.format("ERROR! DCS static DOES NOT exist! Name = %s.", name)) - end - - -- Now we can find it... - _static=STATIC:FindByName(name) - if _static then - self:AddBombingTargetUnit(_static, goodhitrange) - self:T(RANGE.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange)) - else - self:E(RANGE.id..string.format("ERROR! Cound not find static bombing target %s.", name)) - end - - else + -- Get units of group. + local _units=group:GetUnits() - _unit=UNIT:FindByName(name) - if _unit then - self:AddBombingTargetUnit(_unit, goodhitrange) - self:T(RANGE.id..string.format("Adding bombing target %s with hit range %d.", name, goodhitrange)) - else - self:E(RANGE.id..string.format("ERROR! Could not find bombing target %s.", name)) + -- 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) + end +end + +--- Add bombing target(s) to range. +-- @param #RANGE self +-- @param #table targetnames Table containing names of unit or static objects serving as bomb targets. +-- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m. +-- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) + self:F({targetnames=targetnames, goodhitrange=goodhitrange, randommove=randommove}) + + -- Create a table if necessary. + 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)) + self:AddBombingTargetUnit(_static, goodhitrange) + elseif _isstatic==false then + local _unit=UNIT:FindByName(name) + self:T2(RANGE.id..string.format("Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove)) + self:AddBombingTargetUnit(_unit, goodhitrange) + else + self:E(RANGE.id..string.format("ERROR! Could not find bombing target %s.", name)) + end + end end ---- Add a unit as bombing target. +--- Add a unit or static object as bombing target. -- @param #RANGE self --- @param Wrapper.Unit#UNIT unit Unit of the strafe target. +-- @param Wrapper.Positionable#POSITIONABLE unit Positionable (unit or static) of the strafe target. -- @param #number goodhitrange Max distance from unit which is considered as a good hit. -function RANGE:AddBombingTargetUnit(unit, goodhitrange) - self:F({unit=unit, goodhitrange=goodhitrange}) +-- @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}) - local coord=unit:GetCoordinate() + -- Get name of positionable. local name=unit:GetName() - -- Default range is 25 m. - goodhitrange=goodhitrange or 25 + -- Check if we have a static or unit object. + local _isstatic=self:_CheckStatic(name) - -- Create a zone around the unit. - local Vec2=coord:GetVec2() - local Rzone=ZONE_RADIUS:New(name, Vec2, goodhitrange) + -- 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 + + -- 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))) + elseif _isstatic==false then + self:T(RANGE.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + 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, point=coord, zone=Rzone, target=unit, goodhitrange=goodhitrange}) + table.insert(self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed}) end +--- Add all units of a group as bombing targets. +-- @param #RANGE self +-- @param Wrapper.Group#GROUP group Group of bombing targets. +-- @param #number goodhitrange Max distance from unit which is considered as a good hit. +-- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +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. +-- @param #RANGE self +-- @param #string namepit Name of the strafe pit target object. +-- @param #string namefoulline Name of the fould line distance marker object. +-- @return #number Foul line distance in meters. +function RANGE:GetFoullineDistance(namepit, namefoulline) + self:F({namepit=namepit, namefoulline=namefoulline}) + + -- 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 + pit=STATIC:FindByName(namepit, false) + elseif _staticpit==false then + pit=UNIT:FindByName(namepit) + else + self:E(RANGE.id..string.format("ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit)) + end + + -- Get the unit or static foul line object. + local foul=nil + if _staticfoul==true then + foul=STATIC:FindByName(namefoulline, false) + elseif _staticfoul==false then + foul=UNIT:FindByName(namefoulline) + else + self:E(RANGE.id..string.format("ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline)) + end + + -- Get the distance between the two objects. + local fouldist=0 + if pit~=nil and foul~=nil then + fouldist=pit:GetCoordinate():Get2DDistance(foul:GetCoordinate()) + else + self:E(RANGE.id..string.format("ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline)) + end + + self:T(RANGE.id..string.format("Foul line distance = %.1f m.", fouldist)) + return fouldist +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event Handling @@ -600,11 +861,11 @@ function RANGE:onEvent(Event) self:F3(Event) if Event == nil or Event.initiator == nil then - self:T2("Skipping onEvent. Event or Event.initiator unknown.") + self:T3("Skipping onEvent. Event or Event.initiator unknown.") return true end if Unit.getByName(Event.initiator:getName()) == nil then - self:T2("Skipping onEvent. Initiator unit name unknown.") + self:T3("Skipping onEvent. Initiator unit name unknown.") return true end @@ -646,19 +907,16 @@ function RANGE:onEvent(Event) -- Call event Birth function. if Event.id==world.event.S_EVENT_BIRTH and _playername then self:OnEventBirth(EventData) - --self:_OnBirth(EventData) end -- Call event Shot function. if Event.id==world.event.S_EVENT_SHOT and _playername and Event.weapon then self:OnEventShot(EventData) - --self:_OnShot(EventData) end -- Call event Hit function. if Event.id==world.event.S_EVENT_HIT and _playername and DCStgtunit then self:OnEventHit(EventData) - --self:_OnHit(EventData) end end @@ -668,7 +926,6 @@ end -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventBirth(EventData) ---function RANGE:_OnBirth(EventData) self:F({eventbirth = EventData}) local _unitName=EventData.IniUnitName @@ -690,6 +947,8 @@ function RANGE:OnEventBirth(EventData) self:T(RANGE.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:_GetAmmo(_unitName) + -- Reset current strafe status. self.strafeStatus[_uid] = nil @@ -717,7 +976,6 @@ end -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventHit(EventData) ---function RANGE:_OnHit(EventData) self:F({eventhit = EventData}) -- Debug info. @@ -733,7 +991,7 @@ function RANGE:OnEventHit(EventData) end -- Unit ID - local _unitID = _unit:GetID() + local _unitID = _unit:GetID() -- Target local target = EventData.TgtUnit @@ -743,7 +1001,7 @@ function RANGE:OnEventHit(EventData) local _currentTarget = self.strafeStatus[_unitID] -- Player has rolled in on a strafing target. - if _currentTarget then + if _currentTarget and target:IsAlive() then local playerPos = _unit:GetCoordinate() local targetPos = target:GetCoordinate() @@ -752,7 +1010,7 @@ function RANGE:OnEventHit(EventData) for _,_target in pairs(_currentTarget.zone.targets) do -- Check the the target is the same that was actually hit. - if _target:GetName() == targetname then + if _target and _target:IsAlive() and _target:GetName() == targetname then -- Get distance between player and target. local dist=playerPos:Get2DDistance(targetPos) @@ -768,7 +1026,7 @@ function RANGE:OnEventHit(EventData) else -- Too close to the target. if _currentTarget.pastfoulline==false and _unit and _playername then - local _d=_currentTarget.zone.foulline + local _d=_currentTarget.zone.foulline local text=string.format("%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname(_unitName), _d, targetname) self:_DisplayMessageToGroup(_unit, text, 10) self:T2(RANGE.id..text) @@ -781,15 +1039,17 @@ function RANGE:OnEventHit(EventData) end -- Bombing Targets - for _,_target in pairs(self.bombingTargets) do + 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.name == targetname then + if _target and _target:IsAlive() and _bombtarget.name == targetname then if _unit and _playername then - local playerPos = _unit:GetCoordinate() - local targetPos = target:GetCoordinate() + -- Position of target. + local targetPos = _target:GetCoordinate() -- Message to player. --local text=string.format("%s, direct hit on target %s.", self:_myname(_unitName), targetname) @@ -809,7 +1069,6 @@ end -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventShot(EventData) ---function RANGE:_OnShot(EventData) self:F({eventshot = EventData}) -- Weapon data. @@ -818,13 +1077,23 @@ function RANGE:OnEventShot(EventData) local _weaponName = _weaponStrArray[#_weaponStrArray] -- Debug info. - self:T3(RANGE.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) - self:T3(RANGE.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) - self:T3(RANGE.id.."EVENT SHOT: Weapon type = ".._weapon) - self:T3(RANGE.id.."EVENT SHOT: Weapon name = ".._weaponName) + 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) - -- Monitor only bombs and rockets. - if (string.match(_weapon, "weapons.bombs") or string.match(_weapon, "weapons.nurs")) then + -- 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 _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) + + if _track then -- Weapon local _ordnance = EventData.weapon @@ -887,11 +1156,15 @@ function RANGE:OnEventShot(EventData) -- Loop over defined bombing targets. for _,_bombtarget in pairs(self.bombingTargets) do - -- Distance between bomb and target. - local _temp = impactcoord:Get2DDistance(_bombtarget.point) - - -- Find closest target to last known position of the bomb. - if _distance == nil or _temp < _distance then + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + + if _target and _target:IsAlive() then + + -- Distance between bomb and target. + local _temp = impactcoord:Get2DDistance(_target:GetCoordinate()) + + -- Find closest target to last known position of the bomb. + if _distance == nil or _temp < _distance then _distance = _temp _closetTarget = _bombtarget if _distance <= 0.5*_bombtarget.goodhitrange then @@ -903,6 +1176,8 @@ function RANGE:OnEventShot(EventData) else _hitquality = "POOR" end + + end end end @@ -1335,7 +1610,7 @@ end -- @param #RANGE self -- @param #string _unitName Name of player unit. function RANGE:_CheckInZone(_unitName) - self:F(_unitName) + self:F2(_unitName) -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) @@ -1364,7 +1639,7 @@ function RANGE:_CheckInZone(_unitName) -- 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:T(RANGE.id..text) + self:T2(RANGE.id..text) -- Check if player is in strafe zone and below max alt. if unitinzone then @@ -1407,13 +1682,13 @@ function RANGE:_CheckInZone(_unitName) _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. 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 @@ -1455,7 +1730,7 @@ function RANGE:_CheckInZone(_unitName) -- Debug info. local text=string.format("Checking zone %s. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _targetZone.name, _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) - self:T(RANGE.id..text) + self:T2(RANGE.id..text) -- Player is inside zone. if unitinzone then @@ -1509,7 +1784,7 @@ function RANGE:_AddF10Commands(_unitName) -- Enable switch so we don't do this twice. self.MenuAddedTo[_gid] = true - -- Main F10 menu: F10/On the Range + -- Main F10 menu: F10/On the Range// if RANGE.MenuF10[_gid] == nil then RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") end @@ -1517,40 +1792,39 @@ function RANGE:_AddF10Commands(_unitName) local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Statistics", _rangePath) local _markPath = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath) local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) - -- F10/On the Range/My Settings/ + -- F10/On the Range//My Settings/ local _mysmokePath = missionCommands.addSubMenuForGroup(_gid, "Smoke Color", _settingsPath) local _myflarePath = missionCommands.addSubMenuForGroup(_gid, "Flare Color", _settingsPath) - --TODO: Convert to MOOSE menu. - -- F10/On the Range/Mark Targets/ + -- 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, "Smoke Strafe Tgts", _markPath, self._SmokeStrafeTargets, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Bomb Tgts", _markPath, self._SmokeBombTargets, self, _unitName) - -- F10/On the Range/Stats/ + -- F10/On the Range//Stats/ missionCommands.addCommandForGroup(_gid, "All Strafe Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) missionCommands.addCommandForGroup(_gid, "All Bombing Results", _statsPath, self._DisplayBombingResults, self, _unitName) missionCommands.addCommandForGroup(_gid, "My Strafe Results", _statsPath, self._DisplayMyStrafePitResults, self, _unitName) missionCommands.addCommandForGroup(_gid, "My Bomb Results", _statsPath, self._DisplayMyBombingResults, self, _unitName) missionCommands.addCommandForGroup(_gid, "Reset All Stats", _statsPath, self._ResetRangeStats, self, _unitName) - -- F10/On the Range/My Settings/Smoke Color/ + -- F10/On the Range//My Settings/Smoke Color/ missionCommands.addCommandForGroup(_gid, "Blue Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Blue) missionCommands.addCommandForGroup(_gid, "Green Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Green) missionCommands.addCommandForGroup(_gid, "Orange Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Orange) missionCommands.addCommandForGroup(_gid, "Red Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Red) missionCommands.addCommandForGroup(_gid, "White Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.White) - -- F10/On the Range/My Settings/Flare Color/ + -- F10/On the Range//My Settings/Flare Color/ missionCommands.addCommandForGroup(_gid, "Green Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Green) missionCommands.addCommandForGroup(_gid, "Red Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Red) missionCommands.addCommandForGroup(_gid, "White Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White) missionCommands.addCommandForGroup(_gid, "Yellow Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow) - -- F10/On the Range/My Settings/ + -- F10/On the Range//My Settings/ missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) - -- F10/On the Range/ + -- F10/On the Range// missionCommands.addCommandForGroup(_gid, "Range Information", _rangePath, self._DisplayRangeInfo, self, _unitName) missionCommands.addCommandForGroup(_gid, "Weather Report", _rangePath, self._DisplayRangeWeather, self, _unitName) end @@ -1571,7 +1845,7 @@ end -- @param #string unitname Name of the player unit. -- @return Number of shells left function RANGE:_GetAmmo(unitname) - self:F(unitname) + self:F2(unitname) -- Init counter. local ammo=0 @@ -1603,7 +1877,11 @@ function RANGE:_GetAmmo(unitname) 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) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + else + local text=string.format("Player %s has %d ammo of type %s", playername, Nammo, Tammo) + self:T(RANGE.id..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) end end end @@ -1619,31 +1897,45 @@ function RANGE:_MarkTargetsOnMap(_unitName) self:F(_unitName) -- Get group. - local group=UNIT:FindByName(_unitName):GetGroup() - - if group then + local group=nil + if _unitName then + group=UNIT:FindByName(_unitName):GetGroup() + end - -- Mark bomb targets. - for _,_target in pairs(self.bombingTargets) do - local coord=_target.point --Core.Point#COORDINATE - coord:MarkToGroup("Bomb target ".._target.name, group) - end - - -- Mark strafe targets. - for _,_strafepit in pairs(self.strafeTargets) do - for _,_target in pairs(_strafepit.targets) do - local coord=_target:GetCoordinate() --Core.Point#COORDINATE - coord:MarkToGroup("Strafe target ".._target:GetName(), group) + -- Mark bomb targets. + for _,_bombtarget in pairs(self.bombingTargets) do + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord=_target:GetCoordinate() --Core.Point#COORDINATE + if group then + coord:MarkToGroup("Bomb target ".._bombtarget.name, group) + else + coord:MarkToAll("Bomb target ".._bombtarget.name) end 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 + + -- Mark strafe targets. + for _,_strafepit in pairs(self.strafeTargets) do + for _,_target in pairs(_strafepit.targets) do + local _target=_target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord=_target:GetCoordinate() --Core.Point#COORDINATE + if group then + coord:MarkToGroup("Strafe target ".._target:GetName(), group) + else + coord:MarkToAll("Strafe target ".._target:GetName()) + end + 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. @@ -1655,9 +1947,12 @@ function RANGE:_IlluminateBombTargets(_unitName) -- All bombing target coordinates. local bomb={} - for _,_target in pairs(self.bombingTargets) do - local coord=_target.point --Core.Point#COORDINATE - table.insert(bomb, coord) + for _,_bombtarget in pairs(self.bombingTargets) do + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord=_target:GetCoordinate() --Core.Point#COORDINATE + table.insert(bomb, coord) + end end if #bomb>0 then @@ -1671,8 +1966,11 @@ function RANGE:_IlluminateBombTargets(_unitName) for _,_strafepit in pairs(self.strafeTargets) do for _,_target in pairs(_strafepit.targets) do - local coord=_target:GetCoordinate() --Core.Point#COORDINATE - table.insert(strafe, coord) + local _target=_target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord=_target:GetCoordinate() --Core.Point#COORDINATE + table.insert(strafe, coord) + end end end @@ -1803,9 +2101,12 @@ end function RANGE:_SmokeBombTargets(unitname) self:F(unitname) - for _,_target in pairs(self.bombingTargets) do - local coord = _target.point --Core.Point#COORDINATE - coord:Smoke(self.BombSmokeColor) + for _,_bombtarget in pairs(self.bombingTargets) do + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord = _target:GetCoordinate() --Core.Point#COORDINATE + coord:Smoke(self.BombSmokeColor) + end end if unitname then @@ -1823,10 +2124,7 @@ function RANGE:_SmokeStrafeTargets(unitname) self:F(unitname) for _,_target in pairs(self.strafeTargets) do - for _,_unit in pairs(_target.targets) do - local coord = _unit:GetCoordinate() --Core.Point#COORDINATE - coord:Smoke(self.StrafeSmokeColor) - end + _target.coordinate:Smoke(self.StrafeSmokeColor) end if unitname then @@ -1939,6 +2237,63 @@ function RANGE:_flarecolor2text(color) return txt end +--- Checks if a static object with a certain name exists. It also added it to the MOOSE data base, if it is not already in there. +-- @param #RANGE self +-- @param #string name Name of the potential static object. +-- @return #boolean Returns true if a static with this name exists. Retruns false if a unit with this name exists. Returns nil if neither unit or static exist. +function RANGE:_CheckStatic(name) + self:F2(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 + else + self:T3(RANGE.id..string.format("No unit object with name %s exists.", name)) + end + + -- If not unit or static exist, we return nil. + return nil +end + +--- Get max speed of controllable. +-- @param #RANGE self +-- @param Wrapper.Controllable#CONTROLLABLE controllable +-- @return Maximum speed in km/h. +function RANGE:_GetSpeed(controllable) + self:F2(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. -- @param #RANGE self -- @param #string _unitName Name of the player unit. @@ -1946,7 +2301,7 @@ end -- @return #string Name of the player. -- @return nil If player does not exist. function RANGE:_GetPlayerUnitAndName(_unitName) - self:F(_unitName) + self:F2(_unitName) if _unitName ~= nil then @@ -1958,7 +2313,7 @@ function RANGE:_GetPlayerUnitAndName(_unitName) local playername=DCSunit:getPlayerName() local unit=UNIT:Find(DCSunit) - self:T({DCSunit=DCSunit, unit=unit, playername=playername}) + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) if DCSunit and unit and playername then return unit, playername end @@ -1975,7 +2330,7 @@ end -- @param #RANGE self -- @param #string unitname Name of the player unit. function RANGE:_myname(unitname) - self:F(unitname) + self:F2(unitname) local unit=UNIT:FindByName(unitname) local pname=unit:GetPlayerName() @@ -1984,13 +2339,13 @@ function RANGE:_myname(unitname) return string.format("%s (%s)", csign, pname) end ---- http://stackoverflow.com/questions/1426954/split-string-in-lua +--- Split string. Cf http://stackoverflow.com/questions/1426954/split-string-in-lua -- @param #RANGE self -- @param #string str Sting to split. -- @param #string sep Speparator for split. -- @return #table Split text. function RANGE:_split(str, sep) - self:F({str=str, sep=sep}) + self:F2({str=str, sep=sep}) local result = {} local regex = ("([^%s]+)"):format(sep) From 22976468736a8a102b454ae38825a363c84cf74e Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Fri, 20 Apr 2018 00:13:30 +0200 Subject: [PATCH 04/31] Range v1.1.1 Added strafe pit/bombing targets info --- Moose Development/Moose/Functional/Range.lua | 79 ++++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 9748e89d6..937dc9002 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -279,7 +279,7 @@ RANGE.id="RANGE | " --- Range script version. -- @field #number version -RANGE.version="1.1.0" +RANGE.version="1.1.1" --TODO list: --TODO: Add custom weapons, which can be specified by the user. @@ -1537,6 +1537,73 @@ function RANGE:_DisplayRangeInfo(_unitname) end end +--- Display bombing target locations to player. +-- @param #RANGE self +-- @param #string _unitname Name of the player unit. +function RANGE:_DisplayBombTargets(_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 + + -- 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 + +--- Display pit location and heading to player. +-- @param #RANGE self +-- @param #string _unitname Name of the player unit. +function RANGE:_DisplayStrafePits(_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 + + -- 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 + + 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 + + --- Report weather conditions at range. Temperature, QFE pressure and wind data. -- @param #RANGE self -- @param #string _unitname Name of the player unit. @@ -1792,11 +1859,11 @@ function RANGE:_AddF10Commands(_unitName) local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Statistics", _rangePath) local _markPath = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath) local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) + local _infoPath = missionCommands.addSubMenuForGroup(_gid, "Range Info", _rangePath) -- F10/On the Range//My Settings/ local _mysmokePath = missionCommands.addSubMenuForGroup(_gid, "Smoke Color", _settingsPath) local _myflarePath = missionCommands.addSubMenuForGroup(_gid, "Flare Color", _settingsPath) - --TODO: Convert to MOOSE menu. -- 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) @@ -1824,9 +1891,11 @@ function RANGE:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) - -- F10/On the Range// - missionCommands.addCommandForGroup(_gid, "Range Information", _rangePath, self._DisplayRangeInfo, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Weather Report", _rangePath, self._DisplayRangeWeather, 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) + missionCommands.addCommandForGroup(_gid, "Bombing Targets", _infoPath, self._DisplayBombTargets, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName) end else self:T(RANGE.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) From 462564cd01448282d8fc594fa31c4c5a0623840a Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Sun, 22 Apr 2018 23:55:21 +0200 Subject: [PATCH 05/31] PseudoATC and RANGE Range: corrected heading PseudoATC: lots of changes --- .../Moose/Functional/PseudoATC.lua | 279 +++++++++++++----- Moose Development/Moose/Functional/Range.lua | 7 + 2 files changed, 212 insertions(+), 74 deletions(-) diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua index 9e93073d6..6295fc646 100644 --- a/Moose Development/Moose/Functional/PseudoATC.lua +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -13,7 +13,7 @@ -- -- * Report QFE or QNH pressures at nearby airbases. -- * Report wind direction and strength at airbases. --- * Report temperature at airbases +-- * Report temperature at airbases. -- * Report absolute bearing and range to nearest airports. -- * Report current altitude AGL of own aircraft. -- * Upon request, ATC reports altitude until touchdown. @@ -43,7 +43,7 @@ -- ### Contributions: **Sven van de Velde ([FlightControl](https://forums.eagle.ru/member.php?u=89536))** -- -- ==== --- @module PeusoATC +-- @module PseudoATC ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- PSEUDOATC class @@ -68,11 +68,12 @@ -- @field #PSEUDOATC PSEUDOATC={ ClassName = "PSEUDOATC", - Debug=true, + Debug=false, player={}, maxairport=9, mdur=30, mrefresh=120, + eventsmoose=true, } ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -93,7 +94,7 @@ PSEUDOATC.id="PseudoATC | " --- PSEUDOATC version. -- @field #list PSEUDOATC.version={ - version = "0.5.0", + version = "0.6.0", print = true, } @@ -107,7 +108,7 @@ function PSEUDOATC:Start() local self=BASE:Inherit(self, BASE:New()) -- #PSEUDOATC -- Debug info - env.info(PSEUDOATC.id..string.format("Creating PseudoATC object. PseudoATC version %s", PSEUDOATC.version.version)) + self:E(PSEUDOATC.id..string.format("Creating PseudoATC object. PseudoATC version %s", PSEUDOATC.version.version)) -- Handle events. if self.eventsmoose then @@ -192,11 +193,13 @@ end -- @param #PSEUDOATC self -- @param Core.Event#EVENTDATA EventData function PSEUDOATC:_OnBirth(EventData) - env.info(PSEUDOATC.id.."PlayerEntered event caught my MOOSE.") + self:F({EventData=EventData}) + -- Get unit and player. local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + -- Check if a player entered. if _unit and _playername then self:PlayerEntered(_unit) end @@ -207,10 +210,13 @@ end -- @param #PSEUDOATC self -- @param Core.Event#EVENTDATA EventData function PSEUDOATC:_PlayerLeft(EventData) + self:F({EventData=EventData}) + -- Get unit and player. local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + -- Check if a player left. if _unit and _playername then self:PlayerLeft(_unit) end @@ -220,19 +226,22 @@ end -- @param #PSEUDOATC self -- @param Core.Event#EVENTDATA EventData function PSEUDOATC:_PlayerLanded(EventData) + self:F({EventData=EventData}) + -- Get unit, player and place. local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) local _base=EventData.Place local _baseName=EventData.PlaceName - if _unit and _playername and _base then + -- Call landed function. + if _unit and _playername and _base then self:PlayerLanded(_unit, _baseName) end end ----------------------------------------------------------------------------------------------------------------------------------------- --- Menu Functions +-- Event Functions --- Function called when a player enters a unit. -- @param #PSEUDOATC self @@ -240,6 +249,7 @@ end function PSEUDOATC:PlayerEntered(unit) self:F2({unit=unit}) + -- Get player info. local group=unit:GetGroup() --Wrapper.Group#GROUP local GID=group:GetID() local GroupName=group:GetName() @@ -259,10 +269,8 @@ function PSEUDOATC:PlayerEntered(unit) -- Info message. local text=string.format("Player %s entered unit %s of group %s. ID = %d", PlayerName, UnitName, GroupName, GID) - if self.Debug then - MESSAGE:New(text, 30):ToGroup(group) - env.info(PSEUDOATC.id..text) - end + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) -- Create main F10 menu, i.e. "F10/Pseudo ATC" self.player[GID].menu_main=missionCommands.addSubMenuForGroup(GID, "Pseudo ATC") @@ -279,7 +287,6 @@ function PSEUDOATC:PlayerEntered(unit) -- Start scheduler to refresh the F10 menues. self.player[GID].scheduler, self.player[GID].schedulerid=SCHEDULER:New(nil, self.MenuRefresh, {self, GID}, self.mrefresh, self.mrefresh) - self:T2(self.player[GID]) end --- Function called when a player has landed. @@ -298,11 +305,9 @@ function PSEUDOATC:PlayerLanded(unit, place) local CallSign=self.player[id].callsign -- Debug message. - if self.Debug then - local text=string.format("Player %s (%s) from group %s with ID %d landed at %s", PlayerName, UnitName, GroupName, place) - MESSAGE:New(text,30):ToAll() - env.info(PSEUDOATC.id..text) - end + local text=string.format("Player %s (%s) from group %s with ID %d landed at %s", PlayerName, UnitName, GroupName, place) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) -- Stop altitude reporting timer if its activated. self:AltidudeStopTimer(id) @@ -326,25 +331,20 @@ function PSEUDOATC:PlayerLeft(unit) local id=group:GetID() -- Debug message. - if self.Debug then - local text=string.format("Player %s (%s) callsign %s of group %s just left.", self.player[id].playername, self.player[id].unitname, self.player[id].callsign, self.player[id].groupname) - MESSAGE:New(text,30):ToAll() - env.info(PSEUDOATC.id..text) - end + local text=string.format("Player %s (%s) callsign %s of group %s just left.", self.player[id].playername, self.player[id].unitname, self.player[id].callsign, self.player[id].groupname) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) -- Stop scheduler for menu updates if self.player[id].schedulerid then self.player[id].scheduler:Stop(self.player[id].schedulerid) - self.player[id].scheduler=nil - self.player[id].schedulerid=nil end - -- Remove main menu + -- Remove main menu. missionCommands.removeItem(self.player[id].menu_main) -- Remove player array. self.player[id]=nil - end ----------------------------------------------------------------------------------------------------------------------------------------- @@ -354,13 +354,12 @@ end -- @param #PSEUDOATC self. -- @param #number id Group id of player unit. function PSEUDOATC:MenuRefresh(id) - self:F(id) + self:F({id=id}) - if self.Debug then - local text=string.format("Refreshing menues for player %s in group %s.", self.player[id].playername, self.player[id].groupname) - env.info(PSEUDOATC.id..text) - MESSAGE:New(text,30):ToAll() - end + -- Debug message. + local text=string.format("Refreshing menues for player %s in group %s.", self.player[id].playername, self.player[id].groupname) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text,30):ToAllIf(self.Debug) -- Clear menu. self:MenuClear(id) @@ -382,33 +381,27 @@ end function PSEUDOATC:MenuClear(id) self:F(id) - if self.Debug then - local text=string.format("Clearing menues for player %s in group %s.", self.player[id].playername, self.player[id].groupname) - env.info(PSEUDOATC.id..text) - MESSAGE:New(text,30):ToAll() - end - - BASE:E(self.player[id].menu_airports) + -- Debug message. + local text=string.format("Clearing menues for player %s in group %s.", self.player[id].playername, self.player[id].groupname) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text,30):ToAllIf(self.Debug) + if self.player[id].menu_airports then for name,item in pairs(self.player[id].menu_airports) do - - if self.Debug then - env.info(PSEUDOATC.id..string.format("Deleting menu item %s for ID %d", name, id)) - BASE:E(item) - end + -- Debug message. + self:E(PSEUDOATC.id..string.format("Deleting menu item %s for ID %d", name, id)) + + -- Remove menu item. missionCommands.removeItemForGroup(id, self.player[id].menu_airports[name]) - --missionCommands.removeItemForGroup(id, item) end else - if self.Debug then - local text=string.format("no airports to clear menues") - env.info(PSEUDOATC.id..text) - end + self:T2(PSEUDOATC.id.."No airports to clear menus.") end + -- Remove if self.player[id].menu_aircraft then missionCommands.removeItemForGroup(id, self.player[id].menu_aircraft.main) end @@ -443,15 +436,15 @@ function PSEUDOATC:MenuAirports(id) self.player[id].menu_airports[name]=submenu -- Create menu reporting commands + missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, name) missionCommands.addCommandForGroup(id, "Request QFE", submenu, self.ReportPressure, self, id, "QFE", pos, name) missionCommands.addCommandForGroup(id, "Request QNH", submenu, self.ReportPressure, self, id, "QNH", pos, name) missionCommands.addCommandForGroup(id, "Request Wind", submenu, self.ReportWind, self, id, pos, name) missionCommands.addCommandForGroup(id, "Request Temperature", submenu, self.ReportTemperature, self, id, pos, name) missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, name) - if self.Debug then - env.info(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, id)) - end + -- Debug message. + self:T(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, id)) end end @@ -469,9 +462,7 @@ function PSEUDOATC:MenuAircraft(id) local name=string.format("My Aircraft (%s)", callsign) -- Debug info. - if self.Debug then - env.info(PSEUDOATC.id..string.format("Creating menu item %s for ID %d", name,id)) - end + self:T(PSEUDOATC.id..string.format("Creating menu item %s for ID %d", name,id)) -- F10/PseudoATC/My Aircraft (callsign) self.player[id].menu_aircraft.main = missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_main) @@ -502,6 +493,7 @@ function PSEUDOATC:MenuAircraft(id) self.player[id].menu_aircraft_waypoints.pname=submenu -- Menu commands for each waypoint "F10/PseudoATC/My Aircraft (callsign)/Waypoints/Waypoint X/" + missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, pname) missionCommands.addCommandForGroup(id, "Request QFE", submenu, self.ReportPressure, self, id, "QFE", pos, pname) missionCommands.addCommandForGroup(id, "Request QNH", submenu, self.ReportPressure, self, id, "QNH", pos, pname) missionCommands.addCommandForGroup(id, "Request Wind", submenu, self.ReportWind, self, id, pos, pname) @@ -517,6 +509,75 @@ end ----------------------------------------------------------------------------------------------------------------------------------------- -- Reporting Functions +--- Weather Report. Report pressure QFE/QNH, temperature, wind at certain location +-- @param #PSEUDOATC self +-- @param #number id Group id to which the report is delivered. +-- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. +-- @param #string location Name of the location at which the pressure is measured. +function PSEUDOATC:ReportWeather(id, position, location) + self:F({id=id, position=position, location=location}) + + -- Player unit system settings. + local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + + local text=string.format("Local weather at %s:\n", location) + + -- Get pressure in hPa. + local Pqnh=position:GetPressure(0) -- Get pressure at sea level. + local Pqfe=position:GetPressure() -- Get pressure at (land) height of position. + + -- Unit conversion. + local _Pqnh=string.format("%.2f inHg", Pqnh * PSEUDOATC.unit.hPa2inHg) + local _Pqfe=string.format("%.2f inHg", Pqfe * PSEUDOATC.unit.hPa2inHg) + if settings:IsMetric() then + _Pqnh=string.format("%.1f mmHg", Pqnh * PSEUDOATC.unit.hPa2mmHg) + _Pqfe=string.format("%.1f mmHg", Pqfe * PSEUDOATC.unit.hPa2mmHg) + end + + -- Message text. + text=text..string.format("QFE %.1f hPa = %s.\n", Pqfe, _Pqfe) + text=text..string.format("QNH %.1f hPa = %s.\n", Pqnh, _Pqnh) + + --- convert celsius to fahrenheit + local function celsius2fahrenheit(degC) + return degC*1.8+32 + end + + -- Get temperature at position in degrees Celsius. + local T=position:GetTemperature() + + -- Correct unit system. + local _T=string.format('%d°F', celsius2fahrenheit(T)) + if settings:IsMetric() then + _T=string.format('%d°C', T) + end + + -- Message text. + local text=text..string.format("Temperature %s\n", _T) + + -- Get wind direction and speed. + local Dir,Vel=position:GetWind() + + -- Get Beaufort wind scale. + local Bn,Bd=UTILS.BeaufortScale(Vel) + + -- Formatted wind direction. + local Ds = string.format('%03d°', Dir) + + -- Velocity in player units. + local Vs=string.format('%.1f m/s', Vel) + if settings:IsImperial() then + Vs=string.format("%.1f knots", Vel*1.94384) + end + + -- Message text. + local text=text..string.format("Wind from %s at %s (%s).", Ds, Vs, Bd) + + -- Send message + self:_DisplayMessageToGroup(self.player[id].unit, text, self.mdur, true) + +end + --- Report pressure. -- @param #PSEUDOATC self -- @param #number id Group id to which the report is delivered. @@ -534,12 +595,20 @@ function PSEUDOATC:ReportPressure(id, Qcode, position, location) P=position:GetPressure() -- Get pressure at (land) height of position. end + -- Settings. + local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + -- Unit conversion. local P_inHg=P * PSEUDOATC.unit.hPa2inHg local P_mmHg=P * PSEUDOATC.unit.hPa2mmHg + + local P_set=string.format("%.2f inHg", P_inHg) + if settings:IsMetric() then + P_set=string.format("%.1f mmHg", P_mmHg) + end -- Message text. - local text=string.format("%s at %s: P = %.1f hPa = %.2f inHg = %.1f mmHg.", Qcode, location, P, P_inHg, P_mmHg) + local text=string.format("%s at %s: P = %.1f hPa = %s.", Qcode, location, P, P_set) -- Send message. MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) @@ -565,8 +634,17 @@ function PSEUDOATC:ReportTemperature(id, position, location) local Tc=string.format('%d°C', T) local Tf=string.format('%d°F', celsius2fahrenheit(T)) + -- Settings. + local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + + -- Correct unit system. + local _T=string.format('%d°F', celsius2fahrenheit(T)) + if settings:IsMetric() then + _T=string.format('%d°C', T) + end + -- Message text. - local text=string.format("Temperature at %s is %s = %s", location, Tc, Tf) + local text=string.format("Temperature at %s is %s", location, _T) -- Send message to player group. MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) @@ -589,8 +667,17 @@ function PSEUDOATC:ReportWind(id, position, location) -- Formatted wind direction. local Ds = string.format('%03d°', Dir) + -- Settings. + local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + + -- Velocity in player units. + local Vs=string.format('%.1f m/s', Vel) + if settings:IsImperial() then + Vs=string.format("%.1f knots", Vel*1.94384) + end + -- Message text. - local text=string.format("%s: Wind from %s at %.1f m/s (%s).", location, Ds, Vel, Bd) + local text=string.format("%s: Wind from %s at %s (%s).", location, Ds, Vs, Bd) -- Send message to player group. MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) @@ -614,10 +701,18 @@ function PSEUDOATC:ReportBR(id, position, location) local range=coord:Get2DDistance(position) -- Bearing string. - local Bs=string.format('%03d°', angle) + local Bs=string.format('%03d°', angle) + + -- Settings. + local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + + local Rs=string.format("%.1f km", range/1000) + if settings:IsImperial() then + Rs=string.format("%.1f NM", range/1000 * PSEUDOATC.unit.km2nm) + end -- Message text. - local text=string.format("%s: Bearing %s, Range %.1f km = %.1f NM.", location, Bs, range/1000, range/1000 * PSEUDOATC.unit.km2nm) + local text=string.format("%s: Bearing %s, Range %s.", location, Bs, Rs) -- Send message to player group. MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) @@ -627,11 +722,15 @@ end -- @param #PSEUDOATC self -- @param #number id Group id to the report is delivered. -- @param #number dt (Optional) Duration the message is displayed. +-- @param #boolean _clear (Optional) Clear previouse messages. -- @return #number Altuitude above ground. -function PSEUDOATC:ReportHeight(id, dt) +function PSEUDOATC:ReportHeight(id, dt, _clear) self:F({id=id, dt=dt}) local dt = dt or self.mdur + if _clear==nil then + _clear=false + end -- Return height [m] above ground level. local function get_AGL(p) @@ -647,11 +746,20 @@ function PSEUDOATC:ReportHeight(id, dt) local height=get_AGL(position) local callsign=unit:GetCallsign() + -- Settings. + local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + + local Hs=string.format("%d m", height) + if settings:IsMetric() then + Hs=string.format("%d ft", height*PSEUDOATC.unit.meter2feet) + end + -- Message text. - local text=string.format("%s: Your altitude is %d m = %d ft AGL.", callsign, height, height*PSEUDOATC.unit.meter2feet) + local _text=string.format("%s: Your altitude is %s AGL.", callsign, Hs) -- Send message to player group. - MESSAGE:New(text, dt):ToGroup(self.player[id].group) + --MESSAGE:New(text, dt):ToGroup(self.player[id].group) + self:_DisplayMessageToGroup(self.player[id].unit,_text, dt,_clear) -- Return height return height @@ -659,20 +767,18 @@ end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Start DCS scheduler function. +--- Start altitude reporting scheduler. -- @param #PSEUDOATC self. -- @param #number id Group id of player unit. function PSEUDOATC:AltidudeStartTimer(id) self:F(id) -- Debug info. - if self.Debug then - env.info(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.", id)) - end + self:T(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.", id)) -- Start timer. --self.player[id].altimer=timer.scheduleFunction(self.ReportAltTouchdown, self, id, Tnow+2) - self.player[id].altimer, self.player[id].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, id, 0.1}, 1, 5) + self.player[id].altimer, self.player[id].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, id, 0.1, true}, 1, 5) end --- Stop/destroy DCS scheduler function for reporting altitude. @@ -681,9 +787,7 @@ end function PSEUDOATC:AltidudeStopTimer(id) -- Debug info. - if self.Debug then - env.info(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", id)) - end + self:T(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", id)) -- Stop timer. --timer.removeFunction(self.player[id].alttimer) @@ -752,9 +856,10 @@ function PSEUDOATC:_GetPlayerUnitAndName(_unitName) if _unitName ~= nil then local DCSunit=Unit.getByName(_unitName) local playername=DCSunit:getPlayerName() - local unit=UNIT:Find(DCSunit) - if DCSunit and unit and playername then + + if DCSunit and playername then + local unit=UNIT:Find(DCSunit) return unit, playername end end @@ -763,4 +868,30 @@ function PSEUDOATC:_GetPlayerUnitAndName(_unitName) end +--- Display message to group. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT _unit Player unit. +-- @param #string _text Message text. +-- @param #number _time Duration how long the message is displayed. +-- @param #boolean _clear Clear up old messages. +function PSEUDOATC:_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 then + if _clear == true then + trigger.action.outTextForGroup(_gid, _text, _time, _clear) + else + trigger.action.outTextForGroup(_gid, _text, _time) + end + end + +end diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 937dc9002..a9ba91251 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -1594,6 +1594,13 @@ function RANGE:_DisplayStrafePits(_unitname) -- 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 + else + heading=heading+180 + end local mycoord=coord:ToStringA2G(_unit, _settings) _text=_text..string.format("\n- %s: %s - heading %03d",_strafepit.name, mycoord, heading) From 7dbc9436ed64fcc2b41632b7e0aa31949dfd7b00 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 24 Apr 2018 00:29:09 +0200 Subject: [PATCH 06/31] PseudoATC restructured menu --- .../Moose/Functional/PseudoATC.lua | 85 ++++++++++--------- Moose Development/Moose/Functional/Range.lua | 3 + 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua index 6295fc646..3d17615a7 100644 --- a/Moose Development/Moose/Functional/PseudoATC.lua +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -70,7 +70,7 @@ PSEUDOATC={ ClassName = "PSEUDOATC", Debug=false, player={}, - maxairport=9, + maxairport=10, mdur=30, mrefresh=120, eventsmoose=true, @@ -231,8 +231,16 @@ function PSEUDOATC:_PlayerLanded(EventData) -- Get unit, player and place. local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - local _base=EventData.Place - local _baseName=EventData.PlaceName + local _base=nil + local _baseName=nil + if EventData.place then + _base=EventData.place + _baseName=EventData.place:getName() + end +-- if EventData.subplace then +-- local _subPlace=EventData.subplace +-- local _subPlaceName=EventData.subplace:getName() +-- end -- Call landed function. if _unit and _playername and _base then @@ -300,12 +308,13 @@ function PSEUDOATC:PlayerLanded(unit, place) local group=unit:GetGroup() local id=group:GetID() local PlayerName=self.player[id].playername - local UnitName=self.player[id].playername + local Callsign=self.player[id].callsign + local UnitName=self.player[id].unitname local GroupName=self.player[id].groupname local CallSign=self.player[id].callsign -- Debug message. - local text=string.format("Player %s (%s) from group %s with ID %d landed at %s", PlayerName, UnitName, GroupName, place) + local text=string.format("Player %s (%s) from group %s (ID %d) landed at %s", PlayerName, UnitName, GroupName, id, place) self:T(PSEUDOATC.id..text) MESSAGE:New(text, 30):ToAllIf(self.Debug) @@ -368,26 +377,28 @@ function PSEUDOATC:MenuRefresh(id) self:LocalAirports(id) -- Create submenu My Positon. - self:MenuAircraft(id) + --self:MenuAircraft(id) -- Create submenu airports. self:MenuAirports(id) end ---- Clear player menues. +--- Clear player menus. -- @param #PSEUDOATC self. -- @param #number id Group id of player unit. function PSEUDOATC:MenuClear(id) self:F(id) -- Debug message. - local text=string.format("Clearing menues for player %s in group %s.", self.player[id].playername, self.player[id].groupname) + local text=string.format("Clearing menus for player %s in group %s.", self.player[id].playername, self.player[id].groupname) self:T(PSEUDOATC.id..text) MESSAGE:New(text,30):ToAllIf(self.Debug) if self.player[id].menu_airports then + missionCommands.removeItemForGroup(id, self.player[id].menu_airports) + --[[ for name,item in pairs(self.player[id].menu_airports) do -- Debug message. @@ -396,7 +407,7 @@ function PSEUDOATC:MenuClear(id) -- Remove menu item. missionCommands.removeItemForGroup(id, self.player[id].menu_airports[name]) end - + ]] else self:T2(PSEUDOATC.id.."No airports to clear menus.") end @@ -407,7 +418,7 @@ function PSEUDOATC:MenuClear(id) end self.player[id].menu_airports=nil - self.player[id].menu_aircraft=nil + --self.player[id].menu_aircraft=nil end --- Create "F10/Pseudo ATC" menu items "Airport Data". @@ -417,7 +428,7 @@ function PSEUDOATC:MenuAirports(id) self:F(id) -- Table for menu entries. - self.player[id].menu_airports={} + self.player[id].menu_airports=missionCommands.addSubMenuForGroup(id, "Airports", self.player[id].menu_main) local i=0 for _,airport in pairs(self.player[id].airports) do @@ -432,15 +443,15 @@ function PSEUDOATC:MenuAirports(id) local pos=AIRBASE:FindByName(name):GetCoordinate() --F10menu_ATC_airports[ID][name] = missionCommands.addSubMenuForGroup(ID, name, F10menu_ATC) - local submenu=missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_main) - self.player[id].menu_airports[name]=submenu + local submenu=missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_airports) + --self.player[id].menu_airports[name]=submenu -- Create menu reporting commands missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, name) - missionCommands.addCommandForGroup(id, "Request QFE", submenu, self.ReportPressure, self, id, "QFE", pos, name) - missionCommands.addCommandForGroup(id, "Request QNH", submenu, self.ReportPressure, self, id, "QNH", pos, name) - missionCommands.addCommandForGroup(id, "Request Wind", submenu, self.ReportWind, self, id, pos, name) - missionCommands.addCommandForGroup(id, "Request Temperature", submenu, self.ReportTemperature, self, id, pos, name) + --missionCommands.addCommandForGroup(id, "Request QFE", submenu, self.ReportPressure, self, id, "QFE", pos, name) + --missionCommands.addCommandForGroup(id, "Request QNH", submenu, self.ReportPressure, self, id, "QNH", pos, name) + --missionCommands.addCommandForGroup(id, "Request Wind", submenu, self.ReportWind, self, id, pos, name) + --missionCommands.addCommandForGroup(id, "Request Temperature", submenu, self.ReportTemperature, self, id, pos, name) missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, name) -- Debug message. @@ -455,7 +466,7 @@ function PSEUDOATC:MenuAircraft(id) self:F(id) -- Table for menu entries. - self.player[id].menu_aircraft={} + --self.player[id].menu_aircraft={} local unit=self.player[id].unit --Wrapper.Unit#UNIT local callsign=self.player[id].callsign @@ -465,14 +476,13 @@ function PSEUDOATC:MenuAircraft(id) self:T(PSEUDOATC.id..string.format("Creating menu item %s for ID %d", name,id)) -- F10/PseudoATC/My Aircraft (callsign) - self.player[id].menu_aircraft.main = missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_main) + --self.player[id].menu_aircraft.main = missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_main) + - -- F10/PseudoATC/My Aircraft (callsign)/Waypoints if #self.player[id].waypoints>0 then - --F10menu_ATC_waypoints[ID]={} - self.player[id].menu_aircraft_waypoints={} - self.player[id].menu_aircraft_waypoints.main=missionCommands.addSubMenuForGroup(id, "Waypoints", self.player[id].menu_aircraft.main) + -- F10/PseudoATC/Waypoints + self.player[id].menu_waypoints=missionCommands.addSubMenuForGroup(id, "Waypoints", self.player[id].menu_main) local j=0 for i, wp in pairs(self.player[id].waypoints) do @@ -483,27 +493,26 @@ function PSEUDOATC:MenuAircraft(id) break -- max ten menu entries end + -- Position of Waypoint local pos=COORDINATE:New(wp.x,wp.alt,wp.z) - - local fname=string.format("Waypoint %d for %s", i-1, callsign) - local pname=string.format("Waypoint %d", i-1) + local name=string.format("Waypoint %d", i-1) - -- "F10/PseudoATC/My Aircraft (callsign)/Waypoints/Waypoint X" - local submenu=missionCommands.addSubMenuForGroup(id, pname, self.player[id].menu_aircraft_waypoints.main) - self.player[id].menu_aircraft_waypoints.pname=submenu + -- "F10/PseudoATC/Waypoints/Waypoint X" + local submenu=missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_waypoints) -- Menu commands for each waypoint "F10/PseudoATC/My Aircraft (callsign)/Waypoints/Waypoint X/" - missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, pname) - missionCommands.addCommandForGroup(id, "Request QFE", submenu, self.ReportPressure, self, id, "QFE", pos, pname) - missionCommands.addCommandForGroup(id, "Request QNH", submenu, self.ReportPressure, self, id, "QNH", pos, pname) - missionCommands.addCommandForGroup(id, "Request Wind", submenu, self.ReportWind, self, id, pos, pname) - missionCommands.addCommandForGroup(id, "Request Temperature", submenu, self.ReportTemperature, self, id, pos, pname) - missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, pname) + missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, name) + --missionCommands.addCommandForGroup(id, "Request QFE", submenu, self.ReportPressure, self, id, "QFE", pos, pname) + --missionCommands.addCommandForGroup(id, "Request QNH", submenu, self.ReportPressure, self, id, "QNH", pos, pname) + --missionCommands.addCommandForGroup(id, "Request Wind", submenu, self.ReportWind, self, id, pos, pname) + --missionCommands.addCommandForGroup(id, "Request Temperature", submenu, self.ReportTemperature, self, id, pos, pname) + missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, name) end end - missionCommands.addCommandForGroup(id, "Request current altitude AGL", self.player[id].menu_aircraft.main, self.ReportHeight, self, id) - missionCommands.addCommandForGroup(id, "Report altitude until touchdown", self.player[id].menu_aircraft.main, self.AltidudeStartTimer, self, id) - missionCommands.addCommandForGroup(id, "Quit reporting altitude", self.player[id].menu_aircraft.main, self.AltidudeStopTimer, self, id) + + missionCommands.addCommandForGroup(id, "Request current altitude AGL", self.player[id].menu_main, self.ReportHeight, self, id) + missionCommands.addCommandForGroup(id, "Report altitude until touchdown", self.player[id].menu_main, self.AltidudeStartTimer, self, id) + missionCommands.addCommandForGroup(id, "Quit reporting altitude", self.player[id].menu_main, self.AltidudeStopTimer, self, id) end ----------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index a9ba91251..bf8cbb193 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -211,6 +211,9 @@ -- -- 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. +-- -- -- -- @field #RANGE From 441fba0830522e8f1f3cef6133a3437edbf6dd2d Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 24 Apr 2018 21:12:35 +0200 Subject: [PATCH 07/31] PseudoATC Fixed waypoints BR --- Moose Development/Moose/Functional/PseudoATC.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua index 3d17615a7..d70e9d069 100644 --- a/Moose Development/Moose/Functional/PseudoATC.lua +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -68,7 +68,7 @@ -- @field #PSEUDOATC PSEUDOATC={ ClassName = "PSEUDOATC", - Debug=false, + Debug=true, player={}, maxairport=10, mdur=30, @@ -477,8 +477,7 @@ function PSEUDOATC:MenuAircraft(id) -- F10/PseudoATC/My Aircraft (callsign) --self.player[id].menu_aircraft.main = missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_main) - - + if #self.player[id].waypoints>0 then -- F10/PseudoATC/Waypoints @@ -494,7 +493,7 @@ function PSEUDOATC:MenuAircraft(id) end -- Position of Waypoint - local pos=COORDINATE:New(wp.x,wp.alt,wp.z) + local pos=COORDINATE:New(wp.x, wp.alt, wp.y) local name=string.format("Waypoint %d", i-1) -- "F10/PseudoATC/Waypoints/Waypoint X" @@ -705,7 +704,8 @@ function PSEUDOATC:ReportBR(id, position, location) local coord=unit:GetCoordinate() -- Direction vector from current position (coord) to target (position). - local vec3=coord:GetDirectionVec3(position) + local pos=coord:Translate(30,90) + local vec3=coord:GetDirectionVec3(pos) local angle=coord:GetAngleDegrees(vec3) local range=coord:Get2DDistance(position) From 33271edf78a8bc03c74c18c71984c1f95f11f300 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Tue, 24 Apr 2018 23:38:41 +0200 Subject: [PATCH 08/31] Added ARTY class First draft... --- .../Moose/Functional/Artillery.lua | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 Moose Development/Moose/Functional/Artillery.lua diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua new file mode 100644 index 000000000..0695ae427 --- /dev/null +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -0,0 +1,267 @@ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- **Functional** - Control artillery units. +-- +-- ![Banner Image](..\Presentations\ARTILLERY\Artillery_Main.png) +-- +-- ==== +-- +-- Make artillery fire on targets. +-- +-- ==== +-- +-- # Demo Missions +-- +-- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- +-- ==== +-- +-- # YouTube Channel +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) +-- +-- === +-- +-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** +-- +-- ### Contributions: **Sven van de Velde ([FlightControl](https://forums.eagle.ru/member.php?u=89536))** +-- +-- ==== +-- @module Arty + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- ARTY class +-- @type ARTY +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. +-- @field #table targets Targets assigned. +-- @extends Core.Fsm#FSM_CONTROLLABLE +-- + +---# ARTY class, extends @{Core.Fsm#FSM_CONTROLLABLE} +-- Artillery class.. +-- +-- ## Target aquisition... +-- +-- ![Process](..\Presentations\ART\Arty_Process.png) +-- +-- The arty process can be described as follows. +-- +-- ### Submenu +-- +-- @field #ARTY +ARTY={ + ClassName = "ARTY", + Debug = false, + targets = {}, +} + +--- Enumerator of possible rules of engagement. +-- @field #list ROE +ARTY.ROE={ + Hold="Weapon Hold", + Free="Weapon Free", + Return="Return Fire", +} + +--- Enumerator of possible alarm states. +-- @field #list AlarmState +ARTY.AlarmState={ + Auto="Auto", + Green="Green", + Red="Red", +} + +--- Main F10 menu for suppresion, i.e. F10/Artillery. +-- @field #string MenuF10 +ARTY.MenuF10=nil + +--- Some ID to identify who we are in output of the DCS.log file. +-- @field #string id +ARTY.id="ARTY | " + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO list: +-- TODO: don't know yet... +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Creates a new ARTY object. +-- @param #ARTY self +-- @param Wrapper.Group#GROUP group The GROUP object for which artillery tasks should be assigned. +-- @return #ARTY ARTY object. +-- @return nil If group does not exist or is not a ground group. +function ARTY:New(group) + BASE:F2(group) + + -- Inherits from FSM_CONTROLLABLE + local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #ARTY + + -- Check that group is present. + if group then + self:T(ARTY.id.."ARTY group "..group:GetName()) + else + self:E(ARTY.id.."ARTY: Requested group does not exist! (Has to be a MOOSE group.)") + return nil + end + + -- Check that we actually have a GROUND group. + if group:IsGround()==false and group:IsShip()==false then + self:E(ARTY.id.."ARTY group "..group:GetName().." has to be a GROUND or SHIP group!") + return nil + end + + -- Set the controllable for the FSM. + self:SetControllable(group) + + -- Get DCS descriptors of group. + local DCSgroup=Group.getByName(group:GetName()) + local DCSunit=DCSgroup:getUnit(1) + self.DCSdesc=DCSunit:getDesc() + + -- Get max speed the group can do and convert to km/h. + --self.SpeedMax=self.DCSdesc.speedMaxOffRoad*3.6 + + -- Set speed to maximum. + --self.Speed=self.SpeedMax + + -- Is this infantry or not. + self.IsInfantry=DCSunit:hasAttribute("Infantry") + + -- Type of group. + self.Type=group:GetTypeName() + + -- Initial group strength. + self.IniGroupStrength=#group:GetUnits() + + -- Set ROE and Alarm State. + --self:SetDefaultROE("Free") + --self:SetDefaultAlarmState("Auto") + + -- Transitions + self:AddTransition("*", "Start", "CombatReady") + self:AddTransition("CombatReady", "OpenFire", "Firing") + self:AddTransition("Firing", "CeaseFire", "CombatReady") + self:AddTransition("*", "Dead", "*") + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Start" event. Initialized ROE and alarm state. Starts the event handler. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterStart(Controllable, From, Event, To) + self:_EventFromTo("onafterStart", Event, From, To) + + local text=string.format("Started ARTY for group %s.", Controllable:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + + -- Create main F10 menu if it is not there yet. + if self.MenuON then + if not ARTY.MenuF10 then + ARTY.MenuF10 = MENU_MISSION:New("ARTY") + end + self:_CreateMenuGroup() + end + + -- Set the current ROE and alam state. + --self:_SetAlarmState(self.DefaultAlarmState) + --self:_SetROE(self.DefaultROE) + + local text=string.format("\n******************************************************\n") + text=text..string.format("Arty group = %s\n", Controllable:GetName()) + text=text..string.format("Type = %s\n", self.Type) + text=text..string.format("******************************************************\n") + self:T(ARTY.id..text) + + -- Add event handler. + self:HandleEvent(EVENTS.Shot, self._OnEventShot) + self:HandleEvent(EVENTS.Dead, self._OnEventDead) + +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Assign a group of targets +-- @param #ARTY self +-- @param Wrapper.Group#GROUP group Group of targets. +-- @param #number range Range. +function ARTY:AssignTargetGroup(group, range) + self:E({group=group, range=range}) + + local _target={coord=group:GetCoordinate(), range=range} + + table.insert(self.targets, _target) + --table.insert(self.strafeTargets, {name=_name, polygon=_polygon, coordinate= Ccenter, goodPass=goodpass, targets=_targets, foulline=foulline, smokepoints=p, heading=heading}) + + local vec2=group:GetVec2() + --local zone=ZONE:New("target", vec2, range) + local zone=ZONE_RADIUS:New("target", vec2, range) + self:_FireAtZone(zone, 10) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +function ARTY:_OnEventShot(EventData) + self:F(EventData) +end + +function ARTY:_OnEventDead(EventData) + self:F(EventData) +end + +--- Set task for firing at a zone +-- @param #ARTY self +-- @param Wrapper.Zone#ZONE zone Zone to fire upon. +-- @param #number nshells Number of shells to fire. +function ARTY:_FireAtZone(zone, nshells) + self:E({zone=zone, nshells=nshells}) + + local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + + local units=group:GetUnits() + local nunits=#units + + local nshells_tot=nshells*nunits + + -- set ROE to weapon free + group:OptionROEWeaponFree() + + -- assign task + local q=zone:GetVec2() + local r=zone:GetRadius() + local fire=group:TaskFireAtPoint(q, r, nshells_tot) + + -- Execute task + group:SetTask(fire) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Print event-from-to string to DCS log file. +-- @param #ARTY self +-- @param #string BA Before/after info. +-- @param #string Event Event. +-- @param #string From From state. +-- @param #string To To state. +function ARTY:_EventFromTo(BA, Event, From, To) + local text=string.format("\n%s: %s EVENT %s: %s --> %s", BA, self.Controllable:GetName(), Event, From, To) + self:T(ARTY.id..text) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file From 0ec3192fb7d993e1b30e147f53e1044a8061915c Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 25 Apr 2018 22:49:36 +0200 Subject: [PATCH 09/31] Arty improvements --- .../Moose/Functional/Artillery.lua | 112 ++++++++++++++---- 1 file changed, 91 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 0695ae427..ea95af7dc 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -84,6 +84,7 @@ ARTY.id="ARTY | " -- TODO list: -- TODO: don't know yet... + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Creates a new ARTY object. @@ -185,6 +186,25 @@ function ARTY:onafterStart(Controllable, From, Event, To) self:HandleEvent(EVENTS.Shot, self._OnEventShot) self:HandleEvent(EVENTS.Dead, self._OnEventDead) + -- Start scheduler to monitor task queue. + self.TaskQueueSched=SCHEDULER:New(nil, ARTY._CheckTaskQueue, {self}, 5, 10) + +end + +--- Assign a group of targets +-- @param #ARTY self +function ARTY:_CheckTaskQueue() + self:F() + + local _counter=0 + for _,target in pairs(self.targets) do + if target.underfire==false then + env.info(ARTY.id..string.format("Opening fire on target %s", target.name)) + self:OpenFire(target) + break + end + end + end @@ -195,20 +215,29 @@ end --- Assign a group of targets -- @param #ARTY self -- @param Wrapper.Group#GROUP group Group of targets. --- @param #number range Range. -function ARTY:AssignTargetGroup(group, range) - self:E({group=group, range=range}) +-- @param #number radius (Optional) Radius. Default is 100 m. +-- @param #number nshells (Optional) How many shells are fired on target per unit. Default 5. +function ARTY:AssignTargetGroup(group, radius, nshells) + self:E({group=group, radius=radius, nshells=nshells}) - local _target={coord=group:GetCoordinate(), range=range} + nshells=nshells or 5 + radius=radius or 100 + local coord=group:GetCoordinate() + local name=group:GetName() + + -- Prepare target array. + local _target={name=name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false} + + -- Add to table. table.insert(self.targets, _target) - --table.insert(self.strafeTargets, {name=_name, polygon=_polygon, coordinate= Ccenter, goodPass=goodpass, targets=_targets, foulline=foulline, smokepoints=p, heading=heading}) - - local vec2=group:GetVec2() - --local zone=ZONE:New("target", vec2, range) - local zone=ZONE_RADIUS:New("target", vec2, range) - self:_FireAtZone(zone, 10) + -- Debug info. + env.info(ARTY.id.."Targets:") + for _,target in pairs(self.targets) do + env.info(ARTY.id..string.format("Name: %s", target.name)) + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -216,6 +245,7 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- function ARTY:_OnEventShot(EventData) + env.info("Event Shot") self:F(EventData) end @@ -223,29 +253,33 @@ function ARTY:_OnEventDead(EventData) self:F(EventData) end ---- Set task for firing at a zone +--- Set task for firing at a coordinate. -- @param #ARTY self --- @param Wrapper.Zone#ZONE zone Zone to fire upon. --- @param #number nshells Number of shells to fire. -function ARTY:_FireAtZone(zone, nshells) - self:E({zone=zone, nshells=nshells}) +-- @param Core.Point#COORDINATE coord Coordinates to fire upon. +-- @param #number radius Radius around coordinate. +-- @param #number nshells Number of shells to fire per unit. +function ARTY:_FireAtCoord(coord, radius, nshells) + self:E({coord=coord, radius=radius, nshells=nshells}) + -- Controllable. local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + -- Number of units. local units=group:GetUnits() local nunits=#units local nshells_tot=nshells*nunits - -- set ROE to weapon free + -- Set ROE to weapon free. group:OptionROEWeaponFree() - -- assign task - local q=zone:GetVec2() - local r=zone:GetRadius() - local fire=group:TaskFireAtPoint(q, r, nshells_tot) + -- Get Vec2 + local vec2=coord:GetVec2() - -- Execute task + -- Get task. + local fire=group:TaskFireAtPoint(vec2, radius, nshells_tot) + + -- Execute task. group:SetTask(fire) end @@ -253,6 +287,42 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Before "OpenFire" event. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target info. +-- @return boolean +function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) + self:_EventFromTo("onbeforeOpenFire", Event, From, To) + + return true +end + +--- After "OpenFire" event. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target info. _target={coord=coord, radius=radius, nshells=nshells, engaged=0, underattack=false} +function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) + self:_EventFromTo("onafterOpenFire", Event, From, To) + + local _coord=target.coord --Core.Point#COORDINATE + + --_coord:MarkToAll("Arty Target") + + self:_FireAtCoord(target.coord, target.radius, target.nshells) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Print event-from-to string to DCS log file. -- @param #ARTY self -- @param #string BA Before/after info. From b14a672b0ef438c1fb83619c1869ab6cc039d15b Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 26 Apr 2018 23:08:16 +0200 Subject: [PATCH 10/31] ARTY improvements. --- .../Moose/Functional/Artillery.lua | 360 ++++++++++++++---- 1 file changed, 295 insertions(+), 65 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index ea95af7dc..00c83a08a 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -52,8 +52,10 @@ -- @field #ARTY ARTY={ ClassName = "ARTY", - Debug = false, + Debug = true, targets = {}, + currentTarget = nil, + Nshots=0, } --- Enumerator of possible rules of engagement. @@ -80,6 +82,10 @@ ARTY.MenuF10=nil -- @field #string id ARTY.id="ARTY | " +--- Range script version. +-- @field #number version +ARTY.version="0.1.0" + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list: @@ -100,15 +106,15 @@ function ARTY:New(group) -- Check that group is present. if group then - self:T(ARTY.id.."ARTY group "..group:GetName()) + self:T(ARTY.id..string.format("ARTY script version %s. Added group %s.", ARTY.version, group:GetName())) else - self:E(ARTY.id.."ARTY: Requested group does not exist! (Has to be a MOOSE group.)") + self:E(ARTY.id.."ERROR! Requested ARTY group does not exist! (Has to be a MOOSE group.)") return nil end -- Check that we actually have a GROUND group. if group:IsGround()==false and group:IsShip()==false then - self:E(ARTY.id.."ARTY group "..group:GetName().." has to be a GROUND or SHIP group!") + self:E(ARTY.id..string.format("ERROR! ARTY group %s has to be a GROUND or SHIP group!",group:GetName())) return nil end @@ -142,7 +148,8 @@ function ARTY:New(group) -- Transitions self:AddTransition("*", "Start", "CombatReady") self:AddTransition("CombatReady", "OpenFire", "Firing") - self:AddTransition("Firing", "CeaseFire", "CombatReady") + self:AddTransition("Firing", "CeaseFire", "CombatReady") + self:AddTransition("*", "NoAmmo", "OutOfAmmo") self:AddTransition("*", "Dead", "*") return self @@ -152,6 +159,41 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Assign a group of target(s). +-- @param #ARTY self +-- @param Wrapper.Group#GROUP group Group of targets. +-- @param #number radius (Optional) Radius. Default is 100 m. +-- @param #number nshells (Optional) How many shells are fired on target per unit. Default 5. +-- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. +function ARTY:AssignTargetGroup(group, radius, nshells, prio) + self:E({group=group, radius=radius, nshells=nshells, prio=prio}) + + -- Set default values. + nshells=nshells or 5 + radius=radius or 100 + prio=prio or 50 + prio=math.max( 1, prio) + prio=math.min(100, prio) + + -- Coordinate and name. + local coord=group:GetCoordinate() + local name=group:GetName() + + -- Prepare target array. + local _target={name=name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio} + + -- Add to table. + table.insert(self.targets, _target) + + -- Debug info. + env.info(ARTY.id..string.format("Added target %s, radius=%d, nshells=%d, prio=%d.", name, radius, nshells, prio)) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- After "Start" event. Initialized ROE and alarm state. Starts the event handler. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -181,7 +223,14 @@ function ARTY:onafterStart(Controllable, From, Event, To) text=text..string.format("Type = %s\n", self.Type) text=text..string.format("******************************************************\n") self:T(ARTY.id..text) - + + -- Get Ammo. + self:_GetAmmo(self.Controllable) + + for _, target in pairs(self.targets) do + env.info(ARTY.id..string.format("Target %s, radius=%d, nshells=%d, prio=%d.", target.name, target.radius, target.nshells, target.prio)) + end + -- Add event handler. self:HandleEvent(EVENTS.Shot, self._OnEventShot) self:HandleEvent(EVENTS.Dead, self._OnEventDead) @@ -191,64 +240,57 @@ function ARTY:onafterStart(Controllable, From, Event, To) end ---- Assign a group of targets +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Eventhandler for shot event. -- @param #ARTY self -function ARTY:_CheckTaskQueue() - self:F() +-- @param Core.Event#EVENTDATA EventData +function ARTY:_OnEventShot(EventData) + self:F(EventData) - local _counter=0 - for _,target in pairs(self.targets) do - if target.underfire==false then - env.info(ARTY.id..string.format("Opening fire on target %s", target.name)) - self:OpenFire(target) - break - end - end - -end - - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Assign a group of targets --- @param #ARTY self --- @param Wrapper.Group#GROUP group Group of targets. --- @param #number radius (Optional) Radius. Default is 100 m. --- @param #number nshells (Optional) How many shells are fired on target per unit. Default 5. -function ARTY:AssignTargetGroup(group, radius, nshells) - self:E({group=group, radius=radius, nshells=nshells}) - - nshells=nshells or 5 - radius=radius or 100 - - local coord=group:GetCoordinate() - local name=group:GetName() - - -- Prepare target array. - local _target={name=name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false} - - -- Add to table. - table.insert(self.targets, _target) + -- 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. - env.info(ARTY.id.."Targets:") - for _,target in pairs(self.targets) do - env.info(ARTY.id..string.format("Name: %s", target.name)) + self:T(ARTY.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) + self:T(ARTY.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) + self:T(ARTY.id.."EVENT SHOT: Weapon type = ".._weapon) + self:T(ARTY.id.."EVENT SHOT: Weapon name = ".._weaponName) + + local group = EventData.IniGroup --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + if EventData.IniGroupName == self.Controllable:GetName() then + + if self.currentTarget then + + -- Increase number of shots fired by this group on this target. + self.Nshots=self.Nshots+1 + + -- Debug output. + self:T(ARTY.id..string.format("Group %s fired shot # %d on target %s.", self.Controllable:GetName(), self.Nshots, self.currentTarget.name)) + + -- Check if number of shots reached max. + if self.Nshots >= self.currentTarget.nshells then + self:CeaseFire(self.currentTarget) + self.Nshots=0 + end + + else + self:T(ARTY.id..string.format("No current target?!")) + end + end end - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - -function ARTY:_OnEventShot(EventData) - env.info("Event Shot") - self:F(EventData) end +--- Eventhandler for dead event. +-- @param #ARTY self +-- @param Core.Event#EVENTDATA EventData function ARTY:_OnEventDead(EventData) self:F(EventData) end @@ -257,7 +299,7 @@ end -- @param #ARTY self -- @param Core.Point#COORDINATE coord Coordinates to fire upon. -- @param #number radius Radius around coordinate. --- @param #number nshells Number of shells to fire per unit. +-- @param #number nshells Number of shells to fire. function ARTY:_FireAtCoord(coord, radius, nshells) self:E({coord=coord, radius=radius, nshells=nshells}) @@ -288,41 +330,211 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Before "OpenFire" event. --- @param #SUPPRESSION self +-- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #table target Array holding the target info. --- @return boolean +-- @return #boolean If true proceed to onafterOpenfire. function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onbeforeOpenFire", Event, From, To) - + + if self.currentTarget then + self:T(ARTY.id..string.format("Group %s already has a target %s.", self.Controllable:GetName(), target.name)) + return false + end + return true end --- After "OpenFire" event. --- @param #SUPPRESSION self +-- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #table target Array holding the target info. _target={coord=coord, radius=radius, nshells=nshells, engaged=0, underattack=false} -function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) +function ARTY:onafterOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onafterOpenFire", Event, From, To) local _coord=target.coord --Core.Point#COORDINATE --_coord:MarkToAll("Arty Target") - + + -- Get target array index. + local id=self:_GetTargetByName(target.name) + + -- Target is now under fire and has been engaged once more. + if id then + self.targets[id].underfire=true + self.targets[id].engaged=self.targets[id].engaged+1 + self.currentTarget=target + end + + -- Start firing. self:_FireAtCoord(target.coord, target.radius, target.nshells) end +--- Before "CeaseFire" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target info. +-- @return #boolean +function ARTY:onbeforeCeaseFire(Controllable, From, Event, To, target) + self:_EventFromTo("onbeforeCeaseFire", Event, From, To) + + return true +end + +--- After "CeaseFire" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target info. +function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) + self:_EventFromTo("onafterCeaseFire", Event, From, To) + + local name=self.currentTarget.name + + local id=self:_GetTargetByName(name) + + self.targets[id].underfire=false + + self.currentTarget=nil + + --Controllable:ClearTasks() + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Go through queue of assigned tasks. +-- @param #ARTY self +function ARTY:_CheckTaskQueue() + self:F() + + -- Sort targets. + self:_SortTaskQueue() + + for i=1,#self.targets do + + local _target=self.targets[i] + + if _target.underfire==false then + + env.info(ARTY.id..string.format("Opening fire on target %s. Prio = %d, engaged = %d", _target.name, _target.prio, _target.engaged)) + + -- Call OpenFire event. + self:OpenFire(_target) + + break + end + end + +end + + +--- Sort targets with respect to priority and number of times it was already engaged. +-- @param #ARTY self +function ARTY:_SortTaskQueue() + self:F() + + -- Sort results table wrt times they have already been engaged. + local function _sort(a, b) + return (a.engaged < b.engaged) or (a.engaged==b.engaged and a.prio < b.prio) + end + table.sort(self.targets, _sort) + + -- Debug output. + env.info(ARTY.id.."Sorted targets:") + for i=1,#self.targets do + env.info(ARTY.id..string.format("Target %s. Prio = %d, engaged = %d", self.targets[i].name, self.targets[i].prio, self.targets[i].engaged)) + end +end + + +--- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE controllable +-- @return Number of shells left +function ARTY:_GetAmmo(controllable) + self:F2(controllable) + + -- Get all units. + local units=controllable:GetUnits() + + -- Init counter. + local ammo=0 + + for _,unit in pairs(units) do + + local ammotable=unit:GetAmmo() + self:T2({ammotable=ammotable}) + + local name=unit:GetName() + + if ammotable ~= nil then + + local weapons=#ammotable + self:T2(ARTY.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("Unit %s has %d rounds ammo of type %s (shells)", name, Nammo, Tammo) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + else + local text=string.format("Unit %s has %d ammo of type %s", name, Nammo, Tammo) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + end + + end + end + end + + return ammo +end + + +--- Get a target by its name. +-- @param #ARTY self +-- @param #string name Name of target. +-- @return #number Arrayindex of target. +function ARTY:_GetTargetByName(name) + self:F2(name) + + for i=1,#self.targets do + local targetname=self.targets[i].name + if targetname==name then + self:E(ARTY.id..string.format("Found target with name %s. Index = %d", name, i)) + return i + end + end + + self:E(ARTY.id..string.format("ERROR: Target with name %s could not be found!", name)) + return nil +end + + --- Print event-from-to string to DCS log file. -- @param #ARTY self -- @param #string BA Before/after info. @@ -330,8 +542,26 @@ end -- @param #string From From state. -- @param #string To To state. function ARTY:_EventFromTo(BA, Event, From, To) - local text=string.format("\n%s: %s EVENT %s: %s --> %s", BA, self.Controllable:GetName(), Event, From, To) + local text=string.format("%s: %s EVENT %s: %s --> %s", BA, self.Controllable:GetName(), Event, From, To) self:T(ARTY.id..text) end + +--- Split string. Cf http://stackoverflow.com/questions/1426954/split-string-in-lua +-- @param #ARTY self +-- @param #string str Sting to split. +-- @param #string sep Speparator for split. +-- @return #table Split text. +function ARTY:_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 + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file From e5268a29cf48f3dd9eec9810a8a1d53544df872e Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Mon, 30 Apr 2018 12:05:51 +0200 Subject: [PATCH 11/31] ARTY v0.3 First working version. But still WIP. --- .../Moose/Functional/Artillery.lua | 914 +++++++++++++++--- .../Moose/Wrapper/Controllable.lua | 9 +- 2 files changed, 789 insertions(+), 134 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 00c83a08a..d18a1b725 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -1,11 +1,12 @@ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- **Functional** - Control artillery units. -- --- ![Banner Image](..\Presentations\ARTILLERY\Artillery_Main.png) +-- ![Banner Image](..\Presentations\ARTY\Artillery_Main.png) -- -- ==== -- --- Make artillery fire on targets. +-- The ARTY class can be used to easily assign targets for artillery units. Multiple targets can be assigned. +-- -- -- ==== -- @@ -35,6 +36,26 @@ -- @field #string ClassName Name of the class. -- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. -- @field #table targets Targets assigned. +-- @field #table currentTarget Holds the current target, if there is one assigned. +-- @field #number Nammo0 Initial amount total ammunition (shells+rockets+missiles) of the whole group. +-- @field #number Nshells0 Initial amount of shells of the whole group. +-- @field #number Nrockets0 Initial amount of rockets of the whole group. +-- @field #number Nmissiles0 Initial amount of missiles of the whole group. +-- @field Core.Scheduler#SCHEDULER TargetQueueSched Scheduler updating the target queue and calling OpenFire event. +-- @field #number TargetQueueUpdate Interval between updates of the target queue. +-- @field Core.Scheduler#SCHEDULER CheckRearmedSched Scheduler checking whether reaming of the ARTY group is complete. +-- @field #table DCSdesc DCS descriptors of the ARTY group. +-- @field #string Type Type of the ARTY group. +-- @field #number IniGroupStrength Inital number of units in the ARTY group. +-- @field #boolean IsArtillery If true, ARTY group has attribute "Artillery". +-- @field #number Speed Max speed of ARTY group. +-- @field Wrapper.Unit#UNIT RearmingUnit Unit designated to rearm the ARTY group. +-- @field #boolean report Arty group sends messages about their current state or target to its coaliton. +-- @field #table ammoshells Table holding names of the shell types which are included when counting the ammo. Default is {"weapons.shells"} which include most shells. +-- @field #table ammorockets Table holding names of the rocket types which are included when counting the ammo. Default is {"weapons.nurs"} which includes most unguided rockets. +-- @field #table ammomissiles Table holding names of the missile types which are included when counting the ammo. Default is {"weapons.missiles"} which includes some guided missiles. +-- @field #number Nshots Number of shots fired on current target. +-- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. -- @extends Core.Fsm#FSM_CONTROLLABLE -- @@ -43,7 +64,7 @@ -- -- ## Target aquisition... -- --- ![Process](..\Presentations\ART\Arty_Process.png) +-- ![Process](..\Presentations\ARTY\Artillery_Process.png) -- -- The arty process can be described as follows. -- @@ -55,41 +76,59 @@ ARTY={ Debug = true, targets = {}, currentTarget = nil, + Nammo0=0, + Nshells0=0, + Nrockets0=0, + Nmissiles0=0, + TargetQueueSched=nil, + TargetQueueUpdate=5, + CheckRearmedSched=nil, + DCSdesc=nil, + Type=nil, + IniGroupStrength=0, + IsArtillery=nil, + RearmingUnit=nil, + report=true, + ammoshells={"weapons.shells"}, + ammorockets={"weapons.nurs"}, + ammomissiles={"weapons.missiles"}, Nshots=0, + WaitForShotTime=300, } ---- Enumerator of possible rules of engagement. --- @field #list ROE -ARTY.ROE={ - Hold="Weapon Hold", - Free="Weapon Free", - Return="Return Fire", +--- Weapong type ID. http://wiki.hoggit.us/view/DCS_enum_weapon_flag +-- @list WeaponType +ARTY.WeaponType={ + Auto=1073741822, + UnguidedAny=805339120, + UnguidedCannon=805306368, + UnguidedRockets=30720, + GuidedAny=268402702, + GuidedMissile=268402688, + CruiseMissile=2097152, } ---- Enumerator of possible alarm states. --- @field #list AlarmState -ARTY.AlarmState={ - Auto="Auto", - Green="Green", - Red="Red", -} - ---- Main F10 menu for suppresion, i.e. F10/Artillery. --- @field #string MenuF10 -ARTY.MenuF10=nil - --- Some ID to identify who we are in output of the DCS.log file. -- @field #string id ARTY.id="ARTY | " --- Range script version. -- @field #number version -ARTY.version="0.1.0" +ARTY.version="0.3.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list: --- TODO: don't know yet... +-- DONE: Delete targets from queue user function. +-- TODO: Delete entire target queue user function. +-- TODO: Add weapon types. +-- DONE: Add user defined rearm weapon types. +-- TODO: Check if target is in range. Maybe this requires a data base with the ranges of all arty units. Pfff... +-- TODO: Make ARTY move to reaming position. +-- TODO: Check that right reaming vehicle is specified. Blue M818, Red Ural-375. Are there more? +-- TODO: Check if ARTY group is still alive. +-- TODO: Handle dead events. +-- TODO: Abort firing task if no shooting event occured with 5(?) minutes. Something went wrong then. Min/max range for example. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -108,13 +147,13 @@ function ARTY:New(group) if group then self:T(ARTY.id..string.format("ARTY script version %s. Added group %s.", ARTY.version, group:GetName())) else - self:E(ARTY.id.."ERROR! Requested ARTY group does not exist! (Has to be a MOOSE group.)") + self:E(ARTY.id.."ERROR: Requested ARTY group does not exist! (Has to be a MOOSE group.)") return nil end -- Check that we actually have a GROUND group. if group:IsGround()==false and group:IsShip()==false then - self:E(ARTY.id..string.format("ERROR! ARTY group %s has to be a GROUND or SHIP group!",group:GetName())) + self:E(ARTY.id..string.format("ERROR: ARTY group %s has to be a GROUND or SHIP group!", group:GetName())) return nil end @@ -126,31 +165,40 @@ function ARTY:New(group) local DCSunit=DCSgroup:getUnit(1) self.DCSdesc=DCSunit:getDesc() - -- Get max speed the group can do and convert to km/h. - --self.SpeedMax=self.DCSdesc.speedMaxOffRoad*3.6 + -- DCS descriptors. + self:T3(ARTY.id.."DCS descriptors for group "..group:GetName()) + for id,desc in pairs(self.DCSdesc) do + self:T3({id=id, desc=desc}) + end - -- Set speed to maximum. - --self.Speed=self.SpeedMax + -- Set speed to maximum in km/h. + self.Speed=self.DCSdesc.speedMax*3.6 + + -- Displayed name (similar to type name below) + self.DisplayName=self.DCSdesc.displayName -- Is this infantry or not. - self.IsInfantry=DCSunit:hasAttribute("Infantry") + self.IsArtillery=DCSunit:hasAttribute("Artillery") -- Type of group. self.Type=group:GetTypeName() -- Initial group strength. self.IniGroupStrength=#group:GetUnits() - + -- Set ROE and Alarm State. --self:SetDefaultROE("Free") --self:SetDefaultAlarmState("Auto") -- Transitions - self:AddTransition("*", "Start", "CombatReady") - self:AddTransition("CombatReady", "OpenFire", "Firing") - self:AddTransition("Firing", "CeaseFire", "CombatReady") - self:AddTransition("*", "NoAmmo", "OutOfAmmo") - self:AddTransition("*", "Dead", "*") + self:AddTransition("*", "Start", "CombatReady") + self:AddTransition("CombatReady", "OpenFire", "Firing") + self:AddTransition("Firing", "OpenFire", "Firing") -- Other target assigned + self:AddTransition("Firing", "CeaseFire", "CombatReady") + self:AddTransition("*", "Winchester", "OutOfAmmo") + self:AddTransition("OutOfAmmo", "Rearm", "Rearming") + self:AddTransition("Rearming", "Rearmed", "CombatReady") + --self:AddTransition("*", "Dead", "*") return self end @@ -159,35 +207,140 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Assign a group of target(s). +--- Add a group of target(s) for the ARTY group. -- @param #ARTY self -- @param Wrapper.Group#GROUP group Group of targets. --- @param #number radius (Optional) Radius. Default is 100 m. --- @param #number nshells (Optional) How many shells are fired on target per unit. Default 5. -- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. -function ARTY:AssignTargetGroup(group, radius, nshells, prio) - self:E({group=group, radius=radius, nshells=nshells, prio=prio}) +-- @param #number radius (Optional) Radius. Default is 100 m. +-- @param #number nshells (Optional) How many shells (or rockets) are fired on target per engagement. Default 5. +-- @param #number maxengage (Optional) How many times a target is engaged. Default 9999. +-- @param #string time Day time at which the target should be engaged. Passed as a string in format "08:13:45". Current task will be canceled. +-- @param #number weapontype Type of weapon to be used to attack this target. Default ARTY.WeaponType.Auto. +-- @return #string Name of the target. Can be used for further reference, e.g. deleting the target from the list. +-- @usage ARTY:AssignTargetGroup(GROUP:FindByName("Red Target"), 10, 250, 10, 2, "13:25:45") +function ARTY:AssignTargetGroup(group, prio, radius, nshells, maxengage, time, weapontype) + self:E({group=group, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage, time=time, weapontype=weapontype}) -- Set default values. nshells=nshells or 5 radius=radius or 100 + maxengage=maxengage or 9999 + prio=prio or 50 + prio=math.max( 1, prio) + prio=math.min(100, prio) + weapontype=weapontype or ARTY.WeaponType.Auto + + -- Coordinate of target. + local coord=group:GetCoordinate() + local name=group:GetName() + + -- Name of target defined my Lat/long in Degree Minute Second format. + --local name=coord:ToStringLLDMS() + + -- Check if the name has already been used for another target. If so, the function returns a new unique name. + name=self:_CheckTargetName(name) + + -- Time in seconds. + local _time=self:_ClockToSeconds(time) + + -- Prepare target array. + local _target={name=name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio, maxengage=maxengage, time=_time, weapontype=weapontype} + + -- Add to table. + table.insert(self.targets, _target) + + -- Clock. + local _clock=self:_SecondsToClock(_target.time) + + -- Debug info. + self:T(ARTY.id..string.format("Added target %s, prio=%d, radius=%d, nshells=%d, maxengage=%d, time=%s, weapontype=%d", name, prio, radius, nshells, maxengage, _clock, weapontype)) +end + + +--- Assign coordinates of a target for the ARTY group. +-- @param #ARTY self +-- @param Wrapper.Point#COORDINATE coord Coordinates of the target. +-- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. +-- @param #number radius (Optional) Radius. Default is 100 m. +-- @param #number nshells (Optional) How many shells are fired on target per engagement. Default 5. +-- @param #number maxengage (Optional) How many times a target is engaged. Default 9999. +-- @return #string targetname Name of the target. +function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage) + self:E({coord=coord, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage}) + + -- Set default values. + nshells=nshells or 5 + radius=radius or 100 + maxengage=maxengage or 9999 prio=prio or 50 prio=math.max( 1, prio) prio=math.min(100, prio) -- Coordinate and name. - local coord=group:GetCoordinate() - local name=group:GetName() + local name=coord:ToStringLLDMS() -- Prepare target array. - local _target={name=name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio} + local _target={name=name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio, maxengage=maxengage} -- Add to table. table.insert(self.targets, _target) -- Debug info. - env.info(ARTY.id..string.format("Added target %s, radius=%d, nshells=%d, prio=%d.", name, radius, nshells, prio)) + self:T(ARTY.id..string.format("Added target %s, radius=%d, nshells=%d, prio=%d, maxengage=%d.", name, prio, radius, nshells, maxengage)) + return name +end + +--- Assign a unit which is responsible for rearming the ARTY group. If the unit is too far away from the ARTY group it will be guided towards the ARTY group. +-- @param #ARTY self +-- @param Wrapper.Unit#UNIT unit Unit that is supposed to rearm the ARTY group. +function ARTY:SetRearmingUnit(unit) + self:F({unit=unit}) + self.RearmingUnit=unit +end + +--- Delete target from target list. +-- @param #ARTY self +-- @param #string name Name of the target. +function ARTY:RemoveTarget(name) + self:F2(name) + local id=self:_GetTargetByName(name) + if id then + table.remove(self.targets, id) + end +end + +--- Define shell types that are counted to determine the ammo amount the ARTY group has. +-- @param #ARTY self +-- @param #table tableofnames Table of shell type names. +function ARTY:SetShellTypes(tableofnames) + self:F2(tableofnames) + self.ammoshells={} + for _,_type in pairs(tableofnames) do + table.insert(self.ammoshells, _type) + end +end + +--- Define rocket types that are counted to determine the ammo amount the ARTY group has. +-- @param #ARTY self +-- @param #table tableofnames Table of rocket type names. +function ARTY:SetRocketTypes(tableofnames) + self:F2(tableofnames) + self.ammorockets={} + for _,_type in pairs(tableofnames) do + table.insert(self.ammorockets, _type) + end +end + +--- Define missile types that are counted to determine the ammo amount the ARTY group has. +-- @param #ARTY self +-- @param #table tableofnames Table of rocket type names. +function ARTY:SetMissileTypes(tableofnames) + self:F2(tableofnames) + self.ammomissiles={} + for _,_type in pairs(tableofnames) do + table.insert(self.ammomissiles, _type) + end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -206,37 +359,52 @@ function ARTY:onafterStart(Controllable, From, Event, To) local text=string.format("Started ARTY for group %s.", Controllable:GetName()) MESSAGE:New(text, 10):ToAllIf(self.Debug) - -- Create main F10 menu if it is not there yet. - if self.MenuON then - if not ARTY.MenuF10 then - ARTY.MenuF10 = MENU_MISSION:New("ARTY") - end - self:_CreateMenuGroup() - end - -- Set the current ROE and alam state. --self:_SetAlarmState(self.DefaultAlarmState) --self:_SetROE(self.DefaultROE) - local text=string.format("\n******************************************************\n") - text=text..string.format("Arty group = %s\n", Controllable:GetName()) - text=text..string.format("Type = %s\n", self.Type) - text=text..string.format("******************************************************\n") - self:T(ARTY.id..text) - -- Get Ammo. - self:_GetAmmo(self.Controllable) + self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0=self:_GetAmmo(self.Controllable) + local text=string.format("\n******************************************************\n") + text=text..string.format("Arty group = %s\n", Controllable:GetName()) + text=text..string.format("Artillery attribute = %s\n", tostring(self.IsArtillery)) + text=text..string.format("Type = %s\n", self.Type) + text=text..string.format("Number of units = %d\n", self.IniGroupStrength) + text=text..string.format("Max Speed [km/h] = %d\n", self.Speed) + text=text..string.format("Total ammo count = %d\n", self.Nammo0) + text=text..string.format("Number of shells = %d\n", self.Nshells0) + text=text..string.format("Number of rockets = %d\n", self.Nrockets0) + text=text..string.format("Number of missiles = %d\n", self.Nmissiles0) + text=text..string.format("******************************************************\n") + text=text..string.format("Targets:\n") for _, target in pairs(self.targets) do - env.info(ARTY.id..string.format("Target %s, radius=%d, nshells=%d, prio=%d.", target.name, target.radius, target.nshells, target.prio)) + local _clock=self:_SecondsToClock(target.time) + local _weapon=self:_WeaponTypeName(target.weapontype) + text=text..string.format("- %s, prio=%3d, radius=%5d, nshells=%4d, maxengage=%3d, time=%s, weapon=%s\n", target.name, target.prio, target.radius, target.nshells, target.maxengage, _clock, _weapon) end + text=text..string.format("******************************************************\n") + text=text..string.format("Shell types:\n") + for _,_type in pairs(self.ammoshells) do + text=text..string.format("- %s\n", _type) + end + text=text..string.format("Rocket types:\n") + for _,_type in pairs(self.ammorockets) do + text=text..string.format("- %s\n", _type) + end + text=text..string.format("Missile types:\n") + for _,_type in pairs(self.ammomissiles) do + text=text..string.format("- %s\n", _type) + end + text=text..string.format("******************************************************") + self:T(ARTY.id..text) -- Add event handler. self:HandleEvent(EVENTS.Shot, self._OnEventShot) self:HandleEvent(EVENTS.Dead, self._OnEventDead) -- Start scheduler to monitor task queue. - self.TaskQueueSched=SCHEDULER:New(nil, ARTY._CheckTaskQueue, {self}, 5, 10) + self.TargetQueueSched=SCHEDULER:New(nil, ARTY._TargetQueue, {self}, 5, self.TargetQueueUpdate) end @@ -256,10 +424,10 @@ function ARTY:_OnEventShot(EventData) local _weaponName = _weaponStrArray[#_weaponStrArray] -- Debug info. - self:T(ARTY.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) - self:T(ARTY.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) - self:T(ARTY.id.."EVENT SHOT: Weapon type = ".._weapon) - self:T(ARTY.id.."EVENT SHOT: Weapon name = ".._weaponName) + self:T3(ARTY.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) + self:T3(ARTY.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) + self:T3(ARTY.id.."EVENT SHOT: Weapon type = ".._weapon) + self:T3(ARTY.id.."EVENT SHOT: Weapon name = ".._weaponName) local group = EventData.IniGroup --Wrapper.Group#GROUP @@ -273,16 +441,70 @@ function ARTY:_OnEventShot(EventData) self.Nshots=self.Nshots+1 -- Debug output. - self:T(ARTY.id..string.format("Group %s fired shot # %d on target %s.", self.Controllable:GetName(), self.Nshots, self.currentTarget.name)) - + local text=string.format("Group %s fired shot %d of %d with weapon %s on target %s.", self.Controllable:GetName(), self.Nshots, self.currentTarget.nshells, _weaponName, self.currentTarget.name) + self:T(ARTY.id..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Get current ammo. + local _nammo,_nshells,_nrockets,_nmissiles=self:_GetAmmo(self.Controllable) + + if _nammo==0 then + + self:E(ARTY.id.."completely out of ammo") + self.Nshots=0 + self:Winchester() + + -- Current target is deallocated ==> return + return + end + + -- Weapon type name for current target. + local _weapontype=self:_WeaponTypeName(self.currentTarget.weapontype) + self:E(ARTY.id..string.format("nammo=%d, nshells=%d, nrockets=%d, nmissiles=%d", _nammo, _nshells, _nrockets, _nmissiles)) + self:E(ARTY.id..string.format("Weapontype = %s", _weapontype)) + + -- Special weapon type requested ==> Check if corresponding ammo is empty. + if self.currentTarget.weapontype==ARTY.WeaponType.UnguidedCannon and _nshells==0 then + + self:E(ARTY.id.."cannons requested and shells empty") + self.Nshots=0 + self:CeaseFire(self.currentTarget) + return + + elseif self.currentTarget.weapontype==ARTY.WeaponType.UnguidedRockets and _nrockets==0 then + + self:E(ARTY.id.."rockets requested and rockets empty") + self.Nshots=0 + self:CeaseFire(self.currentTarget) + return + + elseif self.currentTarget.weapontype==ARTY.WeaponType.UnguidedAny and _nshells+_nrockets==0 then + + self:E(ARTY.id.."unguided weapon requested and shells+rockets empty") + self.Nshots=0 + self:CeaseFire(self.currentTarget) + return + + elseif self.currentTarget.weapontype==ARTY.WeaponType.CruiseMissile and _nmissiles==0 then + + self:E(ARTY.id.."cruise missiles requested and missiles empty") + self.Nshots=0 + self:CeaseFire(self.currentTarget) + return + end + -- Check if number of shots reached max. if self.Nshots >= self.currentTarget.nshells then - self:CeaseFire(self.currentTarget) + local text=string.format("Group %s stop firing on target %s.", self.Controllable:GetName(), self.currentTarget.name) + self:T(ARTY.id..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self.Nshots=0 + self:CeaseFire(self.currentTarget) end else - self:T(ARTY.id..string.format("No current target?!")) + self:E(ARTY.id..string.format("ERROR: No current target?!")) end end end @@ -300,29 +522,25 @@ end -- @param Core.Point#COORDINATE coord Coordinates to fire upon. -- @param #number radius Radius around coordinate. -- @param #number nshells Number of shells to fire. -function ARTY:_FireAtCoord(coord, radius, nshells) +-- @param #number weapontype Type of weapon to use. +function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) self:E({coord=coord, radius=radius, nshells=nshells}) -- Controllable. local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE - -- Number of units. - local units=group:GetUnits() - local nunits=#units - - local nshells_tot=nshells*nunits - -- Set ROE to weapon free. - group:OptionROEWeaponFree() + group:OptionROEOpenFire() -- Get Vec2 local vec2=coord:GetVec2() -- Get task. - local fire=group:TaskFireAtPoint(vec2, radius, nshells_tot) + local fire=group:TaskFireAtPoint(vec2, radius, nshells, weapontype) -- Execute task. group:SetTask(fire) + --group:PushTask(fire) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -340,11 +558,34 @@ end function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onbeforeOpenFire", Event, From, To) - if self.currentTarget then - self:T(ARTY.id..string.format("Group %s already has a target %s.", self.Controllable:GetName(), target.name)) - return false + + -- If this target has an attack time and it's prio is higher than the current task, we allow the transition. + if target.time~=nil and self.currentTarget~=nil and self.currentTarget.prio > target.prio then + -- Debug info. + self:T(ARTY.id..string.format("Group %s current target %s has lower prio than new target %s with attack time.", self.Controllable:GetName(), self.currentTarget.name, target.name)) + + -- Reset current task. + --self.Controllable:ClearTasks() + + -- Set number of shots counter to zero. + self.Nshots=0 + + -- Stop firing on current target. + self:CeaseFire(self.currentTarget) + + -- Alow transition to onafterOpenfire. + return true end + -- Check that group has no current target already. + if self.currentTarget then + -- Debug info. + self:T(ARTY.id..string.format("Group %s already has a target %s.", self.Controllable:GetName(), self.currentTarget.name)) + + -- Deny transition. + return false + end + return true end @@ -367,16 +608,34 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) -- Target is now under fire and has been engaged once more. if id then + -- Set under fire flag. self.targets[id].underfire=true + -- Increase engaged counter self.targets[id].engaged=self.targets[id].engaged+1 + -- Clear the attack time. + self.targets[id].time=nil + -- Set current target. self.currentTarget=target end + -- Distance to target + local range=Controllable:GetCoordinate():Get2DDistance(target.coord) + + -- Send message. + local text=string.format("%s, opening fire on target %s with %s shells. Distance %.1f km.", Controllable:GetName(), target.name, target.nshells, range/1000) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) + -- Start firing. - self:_FireAtCoord(target.coord, target.radius, target.nshells) + self:_FireAtCoord(target.coord, target.radius, target.nshells, target.weapontype) + + -- Check that after a certain time a shot event occured. + --self.CheckShootingSched, self.CheckRearmedSchedID=SCHEDULER:New(nil, self._CheckShootingStarted, {self}, self.WaitForShotTime) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Before "CeaseFire" event. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -400,42 +659,195 @@ end -- @param #table target Array holding the target info. function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) self:_EventFromTo("onafterCeaseFire", Event, From, To) + + -- Send message. + local text=string.format("%s, ceasing fire on target %s.", Controllable:GetName(), target.name) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) - local name=self.currentTarget.name + -- ARTY group has no current target any more. + self.currentTarget=nil - local id=self:_GetTargetByName(name) + -- Get target array index. + local id=self:_GetTargetByName(target.name) + -- Target is not under fire any more. self.targets[id].underfire=false - self.currentTarget=nil - - --Controllable:ClearTasks() + -- If number of engagements has been reached, the target is removed. + if target.engaged >= target.maxengage then + self:RemoveTarget(target.name) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Winchester" event. Group is out of ammo. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterWinchester(Controllable, From, Event, To) + self:_EventFromTo("onafterWinchester", Event, From, To) + + local text=string.format("Group %s is winchester (out of ammo)!", Controllable:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + + -- Send message. + local text=string.format("%s, winchester.", Controllable:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) + + -- Remove current target. + if self.currentTarget then + local id=self:_GetTargetByName(self.currentTarget.name) + self.targets[id].underfire=false + self.currentTarget=nil + end + + -- Init rearming. + self:Rearm() end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Rearm" event. Check if a unit to rearm the ARTY group has been defined. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onbeforeRearm(Controllable, From, Event, To) + self:_EventFromTo("onbeforeRearm", Event, From, To) + + if self.RearmingUnit and self.RearmingUnit:IsAlive() then + return true + else + return false + end + +end + + +--- After "Rearm" event. Send message if reporting is on. Route rearming unit to ARTY group. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterRearm(Controllable, From, Event, To) + self:_EventFromTo("onafterRearm", Event, From, To) + + -- Send message. + local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingUnit:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) + + -- Random point 20-100 m away from unit. + local coord=self.Controllable:GetCoordinate() + local vec2=coord:GetRandomVec2InRadius(20, 100) + local pops=COORDINATE:NewFromVec2(vec2) + + -- Route unit to ARTY group. + self.RearmingUnit:RouteGroundOnRoad(pops, 50, 5) + + -- Start scheduler to monitor ammo count until rearming is complete. + self.CheckRearmedSched=SCHEDULER:New(nil,self._CheckRearmed, {self}, 5, 10) +end + + +--- Check if ARTY group is reamed. +-- @param #ARTY self +function ARTY:_CheckRearmed() + self:F2() + + -- Get current ammo. + local nammo,nshells,nrockets,nmissiles=self:_GetAmmo(self.Controllable) + + -- Rearming --> Rearmed --> CombatReady + if nammo==self.Nammo0 then + self:Rearmed() + end + +end + +--- After "Rearmed" event. Send message if reporting is on and stop the scheduler. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterRearmed(Controllable, From, Event, To) + self:_EventFromTo("onafterRearmed", Event, From, To) + + -- Send message. + local text=string.format("%s, rearming complete.", Controllable:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) + + -- Stop scheduler. + self.CheckRearmedSched:Stop() +end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Go through queue of assigned tasks. -- @param #ARTY self -function ARTY:_CheckTaskQueue() - self:F() +function ARTY:_TargetQueue() + self:F2() + + -- Debug info + self:T(ARTY.id..string.format("Group %s, number of targets = %d", self.Controllable:GetName(), #self.targets)) - -- Sort targets. - self:_SortTaskQueue() + -- We already have a target. +-- if self.currentTarget then +-- self:T(ARTY.id..string.format("Group %s already has a target %s.", self.Controllable:GetName(), self.currentTarget.name)) +-- return +-- end + + -- First check if there is a target with a certain time for attack. + for i=1,#self.targets do + local _target=self.targets[i] + if _target and _target.time then + if timer.getAbsTime() >= _target.time and _target.underfire==false then + + -- Clock time format. + local _clock=self:_SecondsToClock(_target.time) + local _Cnow=self:_SecondsToClock(timer.getAbsTime()) + -- Debug info. + self:T(ARTY.id..string.format("Engaging timed target %s. Prio=%d, engaged=%d, time=%s, tnow=%s",_target.name,_target.prio,_target.engaged,_clock,_Cnow)) + + -- Call OpenFire event. + self:OpenFire(_target) + + end + end + end + + -- Sort targets w.r.t. prio and number times engaged already. + self:_SortTargetQueuePrio() + + -- Loop over all sorted targets. for i=1,#self.targets do local _target=self.targets[i] - if _target.underfire==false then - - env.info(ARTY.id..string.format("Opening fire on target %s. Prio = %d, engaged = %d", _target.name, _target.prio, _target.engaged)) + if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged then + -- Debug info. + self:T(ARTY.id..string.format("Engaging target %s. Prio = %d, engaged = %d", _target.name, _target.prio, _target.engaged)) + -- Call OpenFire event. self:OpenFire(_target) - + break end end @@ -445,8 +857,8 @@ end --- Sort targets with respect to priority and number of times it was already engaged. -- @param #ARTY self -function ARTY:_SortTaskQueue() - self:F() +function ARTY:_SortTargetQueuePrio() + self:F2() -- Sort results table wrt times they have already been engaged. local function _sort(a, b) @@ -455,17 +867,44 @@ function ARTY:_SortTaskQueue() table.sort(self.targets, _sort) -- Debug output. - env.info(ARTY.id.."Sorted targets:") + self:T2(ARTY.id.."Sorted targets wrt prio and number of engagements:") for i=1,#self.targets do - env.info(ARTY.id..string.format("Target %s. Prio = %d, engaged = %d", self.targets[i].name, self.targets[i].prio, self.targets[i].engaged)) + self:T2(ARTY.id..string.format("Target %s, prio=%d, engaged=%d", self.targets[i].name, self.targets[i].prio, self.targets[i].engaged)) end end +--- Sort targets with respect to engage time. +-- @param #ARTY self +function ARTY:_SortTargetQueueTime() + self:F2() + + -- Sort targets w.r.t attack time. + local function _sort(a, b) + if a.time == nil and b.time == nil then + return false + end + if a.time == nil then + return false + end + if b.time == nil then + return true + end + return a.time < b.time + end + table.sort(self.targets, _sort) + + -- Debug output. + self:T2(ARTY.id.."Sorted targets wrt time:") + for i=1,#self.targets do + self:T(ARTY.id..string.format("Target %s, prio=%d, engaged=%d", self.targets[i].name, self.targets[i].prio, self.targets[i].engaged)) + end + +end --- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE controllable --- @return Number of shells left +-- @return Number of ALL shells left from the whole group. function ARTY:_GetAmmo(controllable) self:F2(controllable) @@ -473,48 +912,136 @@ function ARTY:_GetAmmo(controllable) local units=controllable:GetUnits() -- Init counter. - local ammo=0 + local nammo=0 + local nshells=0 + local nrockets=0 + local nmissiles=0 for _,unit in pairs(units) do - local ammotable=unit:GetAmmo() - self:T2({ammotable=ammotable}) - - local name=unit:GetName() - - if ammotable ~= nil then - - local weapons=#ammotable - self:T2(ARTY.id..string.format("Number of weapons %d.", weapons)) + if unit and unit:IsAlive() then + + local ammotable=unit:GetAmmo() + self:T({ammotable=ammotable}) - for w=1,weapons do + local name=unit:GetName() - local Nammo=ammotable[w]["count"] - local Tammo=ammotable[w]["desc"]["typeName"] + if ammotable ~= nil then + + local weapons=#ammotable - -- We are specifically looking for shells here. - if string.match(Tammo, "shell") then + self:T2(ARTY.id..string.format("Number of weapons %d.", weapons)) + self:T2(ammotable) - -- Add up all shells - ammo=ammo+Nammo + -- Loop over all weapons. + for w=1,weapons do - local text=string.format("Unit %s has %d rounds ammo of type %s (shells)", name, Nammo, Tammo) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug) - else - local text=string.format("Unit %s has %d ammo of type %s", name, Nammo, Tammo) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug) + -- Number of current weapon. + local Nammo=ammotable[w]["count"] + + -- Typename of current weapon + local Tammo=ammotable[w]["desc"]["typeName"] + + -- Check for correct shell type. + local _gotshell=false + for _,_type in pairs(self.ammoshells) do + if string.match(Tammo, _type) then + _gotshell=true + end + end + + -- Check for correct rocket type. + local _gotrocket=false + for _,_type in pairs(self.ammorockets) do + if string.match(Tammo, _type) then + _gotrocket=true + end + end + + -- Check for correct missile type. + local _gotmissile=false + for _,_type in pairs(self.ammomissiles) do + if string.match(Tammo,_type) then + _gotmissile=true + end + end + + + -- We are specifically looking for shells or rockets here. + if _gotshell then + + -- Add up all shells. + nshells=nshells+Nammo + + -- Debug info. + local text=string.format("Unit %s has %d shells of type %s", name, Nammo, Tammo) + self:T2(ARTY.id..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug and not self.report) + + elseif _gotrocket then + + -- Add up all rockets. + nrockets=nrockets+Nammo + + -- Debug info. + local text=string.format("Unit %s has %d rockets of type %s", name, Nammo, Tammo) + self:T2(ARTY.id..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug and not self.report) + + elseif _gotmissile then + + -- Add up all rockets. + nmissiles=nmissiles+Nammo + + -- Debug info. + local text=string.format("Unit %s has %d missiles of type %s", name, Nammo, Tammo) + self:T2(ARTY.id..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug and not self.report) + + else + + -- Debug info. + local text=string.format("Unit %s has %d ammo of type %s", name, Nammo, Tammo) + self:T2(ARTY.id..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug and not self.report) + + end + end - end end end - return ammo + -- Total amount of ammunition. + nammo=nshells+nrockets+nmissiles + + return nammo, nshells, nrockets, nmissiles end +--- Check whether shooting started within a certain time (~5 min). If not, the current target is considered invalid and removed from the target list. +-- @param #ARTY self +function ARTY:_CheckShootingStarted() + self:F2() + + if self.currentTarget and self.Nshots==0 then + + -- Get name and id of target. + local name=self.currentTarget.name + local id=self:_GetTargetByName(name) + + -- Debug info. + self:T(ARTY.id..string.format("No shot event after %d seconds. Removing current target %s from list.", self.WaitForShotTime, name)) + + -- CeaseFire. + self:CeaseFire(self.currentTarget) + + -- Remove target from list. + self:RemoveTarget(name) + + end +end + --- Get a target by its name. -- @param #ARTY self -- @param #string name Name of target. @@ -525,7 +1052,7 @@ function ARTY:_GetTargetByName(name) for i=1,#self.targets do local targetname=self.targets[i].name if targetname==name then - self:E(ARTY.id..string.format("Found target with name %s. Index = %d", name, i)) + self:T2(ARTY.id..string.format("Found target with name %s. Index = %d", name, i)) return i end end @@ -535,6 +1062,70 @@ function ARTY:_GetTargetByName(name) end +--- Get the weapon type name, which should be used to attack the target. +-- @param #ARTY self +-- @param #string name Desired target name. +-- @return #string Unique name, which is not already given for another target. +function ARTY:_CheckTargetName(name) + self:F2(name) + + local newname=name + local counter=1 + + repeat + -- We assume the name is unique. + local unique=true + + -- Loop over all targets already defined. + for _,_target in pairs(self.targets) do + + -- Target name. + local _targetname=_target.name + + if _targetname==newname then + -- Define new name = "name #01" + newname=string.format("%s #%02d", name, counter) + + -- Increase counter. + counter=counter+1 + + -- Name is already used for another target ==> try again with new name. + unique=false + end + end + + until (unique) + + -- Debug output and return new name. + self:T(string.format("Original name %s, new name = %s", name, newname)) + return newname +end + +--- Get the weapon type name, which should be used to attack the target. +-- @param #ARTY self +-- @param #number tnumber Number of weapon type ARTY.WeaponType.XXX +-- @return #number tnumber of weapon type. +function ARTY:_WeaponTypeName(tnumber) + local name="unknown" + if tnumber==ARTY.WeaponType.Auto then + name="Auto (Cannon, Rockets, Missiles)" + elseif tnumber==ARTY.WeaponType.CruiseMissile then + name="Cruise Missile" + elseif tnumber==ARTY.WeaponType.GuidedAny then + name="Any Guided Missile" + elseif tnumber==ARTY.WeaponType.GuidedMissile then + name="Guided Missile" + elseif tnumber==ARTY.WeaponType.UnguidedAny then + name="Any Unguided Weapon (Cannon or Rockets)" + elseif tnumber==ARTY.WeaponType.UnguidedCannon then + name="Unguided Cannon" + elseif tnumber==ARTY.WeaponType.UnguidedRockets then + name="Unguided Rockets" + end + + return name +end + --- Print event-from-to string to DCS log file. -- @param #ARTY self -- @param #string BA Before/after info. @@ -543,25 +1134,84 @@ end -- @param #string To To state. function ARTY:_EventFromTo(BA, Event, From, To) local text=string.format("%s: %s EVENT %s: %s --> %s", BA, self.Controllable:GetName(), Event, From, To) - self:T(ARTY.id..text) + self:T3(ARTY.id..text) end ---- Split string. Cf http://stackoverflow.com/questions/1426954/split-string-in-lua +--- Split string. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua -- @param #ARTY self -- @param #string str Sting to split. -- @param #string sep Speparator for split. -- @return #table Split text. function ARTY:_split(str, sep) - self:F2({str=str, sep=sep}) + self:F3({str=str, sep=sep}) local result = {} local regex = ("([^%s]+)"):format(sep) for each in str:gmatch(regex) do - table.insert(result, each) + table.insert(result, each) end return result end +--- Convert time in seconds to hours, minutes and seconds. +-- @param #ARTY self +-- @param #number seconds Time in seconds. +-- @return #string Time in format Hours:minutes:seconds. +function ARTY:_SecondsToClock(seconds) + self:F3({seconds=seconds}) + + -- Seconds + local seconds = tonumber(seconds) + + if seconds==nil then + return "00:00:00" + end + + if seconds <= 0 then + return "00:00:00" + else + local hours = string.format("%02.f", math.floor(seconds/3600)) + local mins = string.format("%02.f", math.floor(seconds/60 - (hours*60))) + local secs = string.format("%02.f", math.floor(seconds - hours*3600 - mins *60)) + return hours..":"..mins..":"..secs + --return hours, mins, secs + end +end + +--- Convert clock time from hours, minutes and seconds to seconds. +-- @param #ARTY self +-- @param #string clock String of clock time. E.g., "06:12:35". +function ARTY:_ClockToSeconds(clock) + self:F3({clock=clock}) + + if clock==nil then + return nil + end + + -- Split string by ":" + local tsplit=string.gmatch(clock, '([^:]+)') + + -- Get time in seconds + local seconds=0 + local i=1 + for time in tsplit do + if i==1 then + -- Hours + seconds=seconds+tonumber(time)*60*60 + elseif i==2 then + -- Minutes + seconds=seconds+tonumber(time)*60 + elseif i==3 then + -- Seconds + seconds=seconds+tonumber(time) + end + i=i+1 + end + + self:T3(ARTY.id..string.format("Clock %s = %d seconds", clock, seconds)) + return seconds +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 0b68255c5..92d258b5f 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1048,9 +1048,10 @@ end -- @param Dcs.DCSTypes#Vec2 Vec2 The point to fire at. -- @param Dcs.DCSTypes#Distance Radius The radius of the zone to deploy the fire at. -- @param #number AmmoCount (optional) Quantity of ammunition to expand (omit to fire until ammunition is depleted). +-- @param #number WeaponType (optional) Enum for weapon type ID. This value is only required if you want the group firing to use a specific weapon, for instance using the task on a ship to force it to fire guided missiles at targets within cannon range. See http://wiki.hoggit.us/view/DCS_enum_weapon_flag -- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount ) - self:F2( { self.ControllableName, Vec2, Radius, AmmoCount } ) +function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType ) + self:F2( { self.ControllableName, Vec2, Radius, AmmoCount, WeaponType } ) -- FireAtPoint = { -- id = 'FireAtPoint', @@ -1076,6 +1077,10 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount ) DCSTask.params.expendQty = AmmoCount DCSTask.params.expendQtyEnabled = true end + + if WeaponType then + DCSTask.params.weaponType=WeaponType + end self:T3( { DCSTask } ) return DCSTask From 394b5f5b89994732b04ba16565d120de4a98ecaf Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Mon, 30 Apr 2018 22:53:57 +0200 Subject: [PATCH 12/31] ARTY v0.4.0 Improved assigned time for engagement. Next day should be possible now. Added check whether a group started firing within a certain time. --- .../Moose/Functional/Artillery.lua | 169 ++++++++++-------- 1 file changed, 95 insertions(+), 74 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index d18a1b725..01d909b1b 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -55,7 +55,7 @@ -- @field #table ammorockets Table holding names of the rocket types which are included when counting the ammo. Default is {"weapons.nurs"} which includes most unguided rockets. -- @field #table ammomissiles Table holding names of the missile types which are included when counting the ammo. Default is {"weapons.missiles"} which includes some guided missiles. -- @field #number Nshots Number of shots fired on current target. --- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. +-- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. Default is 300 seconds. -- @extends Core.Fsm#FSM_CONTROLLABLE -- @@ -114,7 +114,7 @@ ARTY.id="ARTY | " --- Range script version. -- @field #number version -ARTY.version="0.3.0" +ARTY.version="0.4.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -128,7 +128,8 @@ ARTY.version="0.3.0" -- TODO: Check that right reaming vehicle is specified. Blue M818, Red Ural-375. Are there more? -- TODO: Check if ARTY group is still alive. -- TODO: Handle dead events. --- TODO: Abort firing task if no shooting event occured with 5(?) minutes. Something went wrong then. Min/max range for example. +-- DONE: Abort firing task if no shooting event occured with 5(?) minutes. Something went wrong then. Min/max range for example. +-- DONE: Improve assigned time for engagement. Next day? ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -185,10 +186,6 @@ function ARTY:New(group) -- Initial group strength. self.IniGroupStrength=#group:GetUnits() - - -- Set ROE and Alarm State. - --self:SetDefaultROE("Free") - --self:SetDefaultAlarmState("Auto") -- Transitions self:AddTransition("*", "Start", "CombatReady") @@ -213,7 +210,7 @@ end -- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. -- @param #number radius (Optional) Radius. Default is 100 m. -- @param #number nshells (Optional) How many shells (or rockets) are fired on target per engagement. Default 5. --- @param #number maxengage (Optional) How many times a target is engaged. Default 9999. +-- @param #number maxengage (Optional) How many times a target is engaged. Default 1. -- @param #string time Day time at which the target should be engaged. Passed as a string in format "08:13:45". Current task will be canceled. -- @param #number weapontype Type of weapon to be used to attack this target. Default ARTY.WeaponType.Auto. -- @return #string Name of the target. Can be used for further reference, e.g. deleting the target from the list. @@ -224,7 +221,7 @@ function ARTY:AssignTargetGroup(group, prio, radius, nshells, maxengage, time, w -- Set default values. nshells=nshells or 5 radius=radius or 100 - maxengage=maxengage or 9999 + maxengage=maxengage or 1 prio=prio or 50 prio=math.max( 1, prio) prio=math.min(100, prio) @@ -253,7 +250,7 @@ function ARTY:AssignTargetGroup(group, prio, radius, nshells, maxengage, time, w local _clock=self:_SecondsToClock(_target.time) -- Debug info. - self:T(ARTY.id..string.format("Added target %s, prio=%d, radius=%d, nshells=%d, maxengage=%d, time=%s, weapontype=%d", name, prio, radius, nshells, maxengage, _clock, weapontype)) + self:T(ARTY.id..string.format("Added target %s, prio=%d, radius=%d, nshells=%d, maxengage=%d, time=%s, weapontype=%d", name, prio, radius, nshells, maxengage, tostring(_clock), weapontype)) end @@ -381,7 +378,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) for _, target in pairs(self.targets) do local _clock=self:_SecondsToClock(target.time) local _weapon=self:_WeaponTypeName(target.weapontype) - text=text..string.format("- %s, prio=%3d, radius=%5d, nshells=%4d, maxengage=%3d, time=%s, weapon=%s\n", target.name, target.prio, target.radius, target.nshells, target.maxengage, _clock, _weapon) + text=text..string.format("- %s, prio=%3d, radius=%5d, nshells=%4d, maxengage=%3d, time=%11s, weapon=%s\n", target.name, target.prio, target.radius, target.nshells, target.maxengage, tostring(_clock), _weapon) end text=text..string.format("******************************************************\n") text=text..string.format("Shell types:\n") @@ -406,6 +403,9 @@ function ARTY:onafterStart(Controllable, From, Event, To) -- Start scheduler to monitor task queue. self.TargetQueueSched=SCHEDULER:New(nil, ARTY._TargetQueue, {self}, 5, self.TargetQueueUpdate) + -- Start scheduler to monitor if ARTY group started firing within a certain time. + self.CheckShootingSched=SCHEDULER:New(nil, ARTY._CheckShootingStarted, {self}, 60, 60) + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -450,8 +450,7 @@ function ARTY:_OnEventShot(EventData) if _nammo==0 then - self:E(ARTY.id.."completely out of ammo") - self.Nshots=0 + self:E(ARTY.id..string.format("Group %s completely out of ammo.", self.Controllable:GetName())) self:Winchester() -- Current target is deallocated ==> return @@ -466,29 +465,25 @@ function ARTY:_OnEventShot(EventData) -- Special weapon type requested ==> Check if corresponding ammo is empty. if self.currentTarget.weapontype==ARTY.WeaponType.UnguidedCannon and _nshells==0 then - self:E(ARTY.id.."cannons requested and shells empty") - self.Nshots=0 + self:T(ARTY.id.."Cannons requested and shells empty.") self:CeaseFire(self.currentTarget) return elseif self.currentTarget.weapontype==ARTY.WeaponType.UnguidedRockets and _nrockets==0 then - self:E(ARTY.id.."rockets requested and rockets empty") - self.Nshots=0 + self:T(ARTY.id.."Rockets requested and rockets empty.") self:CeaseFire(self.currentTarget) return elseif self.currentTarget.weapontype==ARTY.WeaponType.UnguidedAny and _nshells+_nrockets==0 then - self:E(ARTY.id.."unguided weapon requested and shells+rockets empty") - self.Nshots=0 + self:T(ARTY.id.."Unguided weapon requested and shells+rockets empty.") self:CeaseFire(self.currentTarget) return elseif self.currentTarget.weapontype==ARTY.WeaponType.CruiseMissile and _nmissiles==0 then - self:E(ARTY.id.."cruise missiles requested and missiles empty") - self.Nshots=0 + self:E(ARTY.id.."Cruise missiles requested and missiles empty.") self:CeaseFire(self.currentTarget) return end @@ -499,7 +494,7 @@ function ARTY:_OnEventShot(EventData) self:T(ARTY.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self.Nshots=0 + -- Cease fire. self:CeaseFire(self.currentTarget) end @@ -616,6 +611,8 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) self.targets[id].time=nil -- Set current target. self.currentTarget=target + -- Set time the target was assigned. + self.currentTarget.Tassigned=timer.getTime() end -- Distance to target @@ -629,9 +626,6 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) -- Start firing. self:_FireAtCoord(target.coord, target.radius, target.nshells, target.weapontype) - -- Check that after a certain time a shot event occured. - --self.CheckShootingSched, self.CheckRearmedSchedID=SCHEDULER:New(nil, self._CheckShootingStarted, {self}, self.WaitForShotTime) - end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -665,9 +659,9 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) self:T(ARTY.id..text) MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) - -- ARTY group has no current target any more. - self.currentTarget=nil - + -- Set number of shots to zero. + self.Nshots=0 + -- Get target array index. local id=self:_GetTargetByName(target.name) @@ -678,7 +672,10 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) if target.engaged >= target.maxengage then self:RemoveTarget(target.name) end - + + -- ARTY group has no current target any more. + self.currentTarget=nil + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -692,23 +689,15 @@ end function ARTY:onafterWinchester(Controllable, From, Event, To) self:_EventFromTo("onafterWinchester", Event, From, To) - local text=string.format("Group %s is winchester (out of ammo)!", Controllable:GetName()) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug) - -- Send message. local text=string.format("%s, winchester.", Controllable:GetName()) self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) + MESSAGE:New(text, 30):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - -- Remove current target. - if self.currentTarget then - local id=self:_GetTargetByName(self.currentTarget.name) - self.targets[id].underfire=false - self.currentTarget=nil - end - - -- Init rearming. + -- Cease fire first. + self:CeaseFire(self.currentTarget) + + -- Init rearming if possible. self:Rearm() end @@ -745,7 +734,7 @@ function ARTY:onafterRearm(Controllable, From, Event, To) -- Send message. local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingUnit:GetName()) self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) -- Random point 20-100 m away from unit. local coord=self.Controllable:GetCoordinate() @@ -756,7 +745,7 @@ function ARTY:onafterRearm(Controllable, From, Event, To) self.RearmingUnit:RouteGroundOnRoad(pops, 50, 5) -- Start scheduler to monitor ammo count until rearming is complete. - self.CheckRearmedSched=SCHEDULER:New(nil,self._CheckRearmed, {self}, 5, 10) + self.CheckRearmedSched=SCHEDULER:New(nil,self._CheckRearmed, {self}, 20, 20) end @@ -768,6 +757,14 @@ function ARTY:_CheckRearmed() -- Get current ammo. local nammo,nshells,nrockets,nmissiles=self:_GetAmmo(self.Controllable) + -- Rearming status in per cent. + local _rearmpc=nammo/self.Nammo0*100 + + -- Send message. + local text=string.format("%s, rearming %d %% complete.", self.Controllable:GetName(), _rearmpc) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + -- Rearming --> Rearmed --> CombatReady if nammo==self.Nammo0 then self:Rearmed() @@ -806,11 +803,11 @@ function ARTY:_TargetQueue() -- Debug info self:T(ARTY.id..string.format("Group %s, number of targets = %d", self.Controllable:GetName(), #self.targets)) - -- We already have a target. --- if self.currentTarget then --- self:T(ARTY.id..string.format("Group %s already has a target %s.", self.Controllable:GetName(), self.currentTarget.name)) --- return --- end + -- No targets assigned at the moment. + if #self.targets==0 then + self:T(ARTY.id..string.format("Group %s, no targets assigned at the moment. No need for _TargetQueue.", self.Controllable:GetName())) + return + end -- First check if there is a target with a certain time for attack. for i=1,#self.targets do @@ -851,7 +848,7 @@ function ARTY:_TargetQueue() break end end - + end @@ -896,7 +893,7 @@ function ARTY:_SortTargetQueueTime() -- Debug output. self:T2(ARTY.id.."Sorted targets wrt time:") for i=1,#self.targets do - self:T(ARTY.id..string.format("Target %s, prio=%d, engaged=%d", self.targets[i].name, self.targets[i].prio, self.targets[i].engaged)) + self:T2(ARTY.id..string.format("Target %s, prio=%d, engaged=%d", self.targets[i].name, self.targets[i].prio, self.targets[i].engaged)) end end @@ -1024,21 +1021,30 @@ end function ARTY:_CheckShootingStarted() self:F2() - if self.currentTarget and self.Nshots==0 then + if self.currentTarget then + + -- Current time. + local Tnow=timer.getTime() - -- Get name and id of target. - local name=self.currentTarget.name - local id=self:_GetTargetByName(name) + -- Time that passed after current target has been assigned. + local dt=Tnow-self.currentTarget.Tassigned - -- Debug info. - self:T(ARTY.id..string.format("No shot event after %d seconds. Removing current target %s from list.", self.WaitForShotTime, name)) + + if dt > self.WaitForShotTime and self.Nshots==0 then - -- CeaseFire. - self:CeaseFire(self.currentTarget) + -- Get name and id of target. + local name=self.currentTarget.name + + -- Debug info. + self:T(ARTY.id..string.format("%s, no shot event after %d seconds. Removing current target %s from list.", self.Controllable:GetName(), self.WaitForShotTime, name)) - -- Remove target from list. - self:RemoveTarget(name) + -- CeaseFire. + self:CeaseFire(self.currentTarget) + + -- Remove target from list. + self:RemoveTarget(name) + end end end @@ -1161,21 +1167,26 @@ end -- @return #string Time in format Hours:minutes:seconds. function ARTY:_SecondsToClock(seconds) self:F3({seconds=seconds}) - + + if seconds==nil then + return nil + --return "00:00:00" + end + -- Seconds local seconds = tonumber(seconds) - if seconds==nil then - return "00:00:00" - end + -- Seconds of this day. + local _seconds=seconds%(60*60*24) if seconds <= 0 then return "00:00:00" else - local hours = string.format("%02.f", math.floor(seconds/3600)) - local mins = string.format("%02.f", math.floor(seconds/60 - (hours*60))) - local secs = string.format("%02.f", math.floor(seconds - hours*3600 - mins *60)) - return hours..":"..mins..":"..secs + local hours = string.format("%02.f", math.floor(_seconds/3600)) + local mins = string.format("%02.f", math.floor(_seconds/60 - (hours*60))) + local secs = string.format("%02.f", math.floor(_seconds - hours*3600 - mins *60)) + local days = string.format("%d", seconds/(60*60*24)) + return hours..":"..mins..":"..secs.."+"..days --return hours, mins, secs end end @@ -1190,13 +1201,23 @@ function ARTY:_ClockToSeconds(clock) return nil end - -- Split string by ":" - local tsplit=string.gmatch(clock, '([^:]+)') - - -- Get time in seconds + -- Seconds init. local seconds=0 + + -- Split additional days. + local dsplit=self:_split(clock, "+") + + -- Convert days to seconds. + if #dsplit>1 then + seconds=seconds+tonumber(dsplit[2])*60*60*24 + end + + -- Split hours, minutes, seconds + local tsplit=self:_split(dsplit[1], ":") + + -- Get time in seconds local i=1 - for time in tsplit do + for _,time in ipairs(tsplit) do if i==1 then -- Hours seconds=seconds+tonumber(time)*60*60 From 274b44459ee0df7a63b2f56725f89f26a513298f Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 2 May 2018 00:14:05 +0200 Subject: [PATCH 13/31] ARTY v0.5 - Adjusted schedulers. - Improved transitions. - Make rearming unit go back to its original position. - Added user functions. - Using only coodinates for target assignments. - Added dead event handling. --- Moose Development/Moose/Core/Point.lua | 8 +- .../Moose/Functional/Artillery.lua | 498 +++++++++++++----- Moose Development/Moose/Wrapper/Group.lua | 1 + Moose Development/Moose/Wrapper/Unit.lua | 14 +- 4 files changed, 384 insertions(+), 137 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index de1a81cfc..6b46e8e78 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -471,8 +471,8 @@ do -- COORDINATE -- @param height (Optional) parameter specifying the height ASL. -- @return Temperature in Degrees Celsius. function COORDINATE:GetTemperature(height) + self:F2(height) local y=height or self.y - env.info("FF height = "..y) local point={x=self.x, y=height or self.y, z=self.z} -- get temperature [K] and pressure [Pa] at point local T,P=atmosphere.getTemperatureAndPressure(point) @@ -940,13 +940,13 @@ do -- COORDINATE -- @param #COORDINATE ToCoord Coordinate of destination. -- @return #table Table of coordinates on road. function COORDINATE:GetPathOnRoad(ToCoord) - local Path={} + self:F2(ToCoord) local path = land.findPathOnRoads("roads", self.x, self.z, ToCoord.x, ToCoord.z) + local Path={} for i, v in ipairs(path) do - --self:E(v) - local coord=COORDINATE:NewFromVec2(v) Path[#Path+1]=COORDINATE:NewFromVec2(v) end + self:F(string.format("Number of points in Path on Road = %d", #Path)) return Path end diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 01d909b1b..10c5282eb 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -5,7 +5,7 @@ -- -- ==== -- --- The ARTY class can be used to easily assign targets for artillery units. Multiple targets can be assigned. +-- The ARTY class can be used to easily assign targets for artillery units. Multiple targets can be assigned. -- -- -- ==== @@ -41,21 +41,29 @@ -- @field #number Nshells0 Initial amount of shells of the whole group. -- @field #number Nrockets0 Initial amount of rockets of the whole group. -- @field #number Nmissiles0 Initial amount of missiles of the whole group. --- @field Core.Scheduler#SCHEDULER TargetQueueSched Scheduler updating the target queue and calling OpenFire event. +-- @field #number FullAmmo Full amount of all ammunition taking the number of alive units into account. +-- @field Core.Scheduler#SCHEDULER scheduler Scheduler object handling various timed functions. +-- @field #number SchedIDTargetQueue Scheduler ID for updating the target queue and calling OpenFire event. -- @field #number TargetQueueUpdate Interval between updates of the target queue. --- @field Core.Scheduler#SCHEDULER CheckRearmedSched Scheduler checking whether reaming of the ARTY group is complete. +-- @field #number SchedIDCheckRearmed Scheduler ID responsible for checking whether reaming of the ARTY group is complete. +-- @field #number SchedIDCheckShooting Scheduler ID for checking whether a group startet firing within a certain time after the fire at point task was assigned. +-- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. Default is 300 seconds. +-- @field #number SchedIDStatusReport Scheduler ID for status report messages. The scheduler is only launched in debug mode. -- @field #table DCSdesc DCS descriptors of the ARTY group. -- @field #string Type Type of the ARTY group. +-- @field #string DisplayName Extended type name of the ARTY group. -- @field #number IniGroupStrength Inital number of units in the ARTY group. -- @field #boolean IsArtillery If true, ARTY group has attribute "Artillery". -- @field #number Speed Max speed of ARTY group. -- @field Wrapper.Unit#UNIT RearmingUnit Unit designated to rearm the ARTY group. +-- @field Wrapper.Point#COORDINATE RearmingUnitCoord Initial coordinates of the rearming unit. After rearming complete, the unit will return to this position. -- @field #boolean report Arty group sends messages about their current state or target to its coaliton. -- @field #table ammoshells Table holding names of the shell types which are included when counting the ammo. Default is {"weapons.shells"} which include most shells. -- @field #table ammorockets Table holding names of the rocket types which are included when counting the ammo. Default is {"weapons.nurs"} which includes most unguided rockets. -- @field #table ammomissiles Table holding names of the missile types which are included when counting the ammo. Default is {"weapons.missiles"} which includes some guided missiles. -- @field #number Nshots Number of shots fired on current target. --- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. Default is 300 seconds. +-- @field #number minrange Minimum firing range in kilometers. Targets closer than this distance are not engaged. Default 0 km. +-- @field #number maxrange Maximum firing range in kilometers. Targets further away than this distance are not engaged. Default 10000 km. -- @extends Core.Fsm#FSM_CONTROLLABLE -- @@ -80,20 +88,28 @@ ARTY={ Nshells0=0, Nrockets0=0, Nmissiles0=0, - TargetQueueSched=nil, + FullAmmo=0, + scheduler=nil, + SchedIDTargetQueue=nil, TargetQueueUpdate=5, - CheckRearmedSched=nil, + SchedIDCheckRearmed=nil, + SchedIDCheckShooting=nil, + WaitForShotTime=300, + SchedIDStatusReport=nil, DCSdesc=nil, Type=nil, + DisplayName=nil, IniGroupStrength=0, IsArtillery=nil, RearmingUnit=nil, + RearmingUnitCoord=nil, report=true, ammoshells={"weapons.shells"}, ammorockets={"weapons.nurs"}, ammomissiles={"weapons.missiles"}, Nshots=0, - WaitForShotTime=300, + minrange=0, + maxrange=1000000, } --- Weapong type ID. http://wiki.hoggit.us/view/DCS_enum_weapon_flag @@ -114,22 +130,24 @@ ARTY.id="ARTY | " --- Range script version. -- @field #number version -ARTY.version="0.4.0" +ARTY.version="0.5.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list: -- DONE: Delete targets from queue user function. --- TODO: Delete entire target queue user function. --- TODO: Add weapon types. +-- DONE: Delete entire target queue user function. +-- TODO: Add weapon types. Done but needs improvements. -- DONE: Add user defined rearm weapon types. -- TODO: Check if target is in range. Maybe this requires a data base with the ranges of all arty units. Pfff... -- TODO: Make ARTY move to reaming position. -- TODO: Check that right reaming vehicle is specified. Blue M818, Red Ural-375. Are there more? -- TODO: Check if ARTY group is still alive. --- TODO: Handle dead events. +-- DONE: Handle dead events. -- DONE: Abort firing task if no shooting event occured with 5(?) minutes. Something went wrong then. Min/max range for example. -- DONE: Improve assigned time for engagement. Next day? +-- TODO: Improve documentation. +-- TODO: Add pseudo user transitions. OnAfter... ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -137,7 +155,7 @@ ARTY.version="0.4.0" -- @param #ARTY self -- @param Wrapper.Group#GROUP group The GROUP object for which artillery tasks should be assigned. -- @return #ARTY ARTY object. --- @return nil If group does not exist or is not a ground group. +-- @return nil If group does not exist or is not a ground or naval group. function ARTY:New(group) BASE:F2(group) @@ -161,6 +179,9 @@ function ARTY:New(group) -- Set the controllable for the FSM. self:SetControllable(group) + -- Create scheduler object. + self.scheduler=SCHEDULER:New(self) + -- Get DCS descriptors of group. local DCSgroup=Group.getByName(group:GetName()) local DCSunit=DCSgroup:getUnit(1) @@ -193,9 +214,9 @@ function ARTY:New(group) self:AddTransition("Firing", "OpenFire", "Firing") -- Other target assigned self:AddTransition("Firing", "CeaseFire", "CombatReady") self:AddTransition("*", "Winchester", "OutOfAmmo") - self:AddTransition("OutOfAmmo", "Rearm", "Rearming") + self:AddTransition("*", "Rearm", "Rearming") self:AddTransition("Rearming", "Rearmed", "CombatReady") - --self:AddTransition("*", "Dead", "*") + self:AddTransition("*", "Dead", "*") return self end @@ -204,19 +225,22 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Add a group of target(s) for the ARTY group. +--- Assign target coordinates to the ARTY group. Only the first parameter, i.e. the coordinate of the target is mandatory. The remaining parameters are optional and can be used to fine tune the engagement. -- @param #ARTY self --- @param Wrapper.Group#GROUP group Group of targets. +-- @param Wrapper.Point#COORDINATE coord Coordinates of the target. -- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. -- @param #number radius (Optional) Radius. Default is 100 m. -- @param #number nshells (Optional) How many shells (or rockets) are fired on target per engagement. Default 5. -- @param #number maxengage (Optional) How many times a target is engaged. Default 1. --- @param #string time Day time at which the target should be engaged. Passed as a string in format "08:13:45". Current task will be canceled. --- @param #number weapontype Type of weapon to be used to attack this target. Default ARTY.WeaponType.Auto. +-- @param #string time (Optional) Day time at which the target should be engaged. Passed as a string in format "08:13:45". Current task will be canceled. +-- @param #number weapontype (Optional) Type of weapon to be used to attack this target. Default ARTY.WeaponType.Auto, i.e. the DCS logic automatically determins the appropriate weapon. +-- @param #string name (Optional) Name of the target. Default is LL DMS coordinate of the target. If the name was already given, the numbering "#01", "#02",... is appended automatically. -- @return #string Name of the target. Can be used for further reference, e.g. deleting the target from the list. --- @usage ARTY:AssignTargetGroup(GROUP:FindByName("Red Target"), 10, 250, 10, 2, "13:25:45") -function ARTY:AssignTargetGroup(group, prio, radius, nshells, maxengage, time, weapontype) - self:E({group=group, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage, time=time, weapontype=weapontype}) +-- @usage paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 10, 300, 10, 1, "08:02:00", ARTY.WeaponType.Auto, "Red Targets 1") +-- paladin:Start() +function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, weapontype, name) + self:T({coord=coord, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage, time=time, weapontype=weapontype, name=name}) -- Set default values. nshells=nshells or 5 @@ -227,21 +251,17 @@ function ARTY:AssignTargetGroup(group, prio, radius, nshells, maxengage, time, w prio=math.min(100, prio) weapontype=weapontype or ARTY.WeaponType.Auto - -- Coordinate of target. - local coord=group:GetCoordinate() - local name=group:GetName() - - -- Name of target defined my Lat/long in Degree Minute Second format. - --local name=coord:ToStringLLDMS() - + -- Name of the target. + local _name=name or coord:ToStringLLDMS() + -- Check if the name has already been used for another target. If so, the function returns a new unique name. - name=self:_CheckTargetName(name) + _name=self:_CheckTargetName(_name) -- Time in seconds. local _time=self:_ClockToSeconds(time) -- Prepare target array. - local _target={name=name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio, maxengage=maxengage, time=_time, weapontype=weapontype} + local _target={name=_name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio, maxengage=maxengage, time=_time, weapontype=weapontype} -- Add to table. table.insert(self.targets, _target) @@ -254,38 +274,28 @@ function ARTY:AssignTargetGroup(group, prio, radius, nshells, maxengage, time, w end ---- Assign coordinates of a target for the ARTY group. +--- Set minimum firing range. Targets closer than this distance are not engaged. -- @param #ARTY self --- @param Wrapper.Point#COORDINATE coord Coordinates of the target. --- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. --- @param #number radius (Optional) Radius. Default is 100 m. --- @param #number nshells (Optional) How many shells are fired on target per engagement. Default 5. --- @param #number maxengage (Optional) How many times a target is engaged. Default 9999. --- @return #string targetname Name of the target. -function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage) - self:E({coord=coord, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage}) - - -- Set default values. - nshells=nshells or 5 - radius=radius or 100 - maxengage=maxengage or 9999 - prio=prio or 50 - prio=math.max( 1, prio) - prio=math.min(100, prio) - - -- Coordinate and name. - local name=coord:ToStringLLDMS() - - -- Prepare target array. - local _target={name=name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio, maxengage=maxengage} - - -- Add to table. - table.insert(self.targets, _target) - - -- Debug info. - self:T(ARTY.id..string.format("Added target %s, radius=%d, nshells=%d, prio=%d, maxengage=%d.", name, prio, radius, nshells, maxengage)) +-- @param #number range Min range in kilometers. Default is 0 km. +function ARTY:SetMinFiringRange(range) + self:F({range=range}) + self.minrange=range or 0 +end - return name +--- Set maximum firing range. Targets further away than this distance are not engaged. +-- @param #ARTY self +-- @param #number range Max range in kilometers. Default is 1000 km. +function ARTY:SetMaxFiringRange(range) + self:F({range=range}) + self.maxrange=range*1000 or 1000*1000 +end + +--- Set time how it is waited a unit the first shot event happens. If no shot is fired after this time, the task to fire is aborted and the target removed. +-- @param #ARTY self +-- @param #number waittime Time in seconds. Default 300 seconds. +function ARTY:SetWaitForShotTime(waittime) + self:F({waittime=waittime}) + self.WaitForShotTime=waittime or 300 end --- Assign a unit which is responsible for rearming the ARTY group. If the unit is too far away from the ARTY group it will be guided towards the ARTY group. @@ -296,6 +306,26 @@ function ARTY:SetRearmingUnit(unit) self.RearmingUnit=unit end +--- Report messages of ARTY group turned on. This is the default. +-- @param #ARTY self +function ARTY:SetReportON() + self.report=true +end + +--- Report messages of ARTY group turned off. Default is on. +-- @param #ARTY self +function ARTY:SetReportOFF() + self.report=false +end + +--- Set target queue update time interval. +-- @param #ARTY self +-- @param #number interval Time interval in seconds. Default is 5 seconds. +function ARTY:SetTargetQueueUpdateInterval(interval) + self:F2({interval=interval}) + self.TargetQueueUpdate=interval or 5 +end + --- Delete target from target list. -- @param #ARTY self -- @param #string name Name of the target. @@ -303,8 +333,19 @@ function ARTY:RemoveTarget(name) self:F2(name) local id=self:_GetTargetByName(name) if id then + self:T(ARTY.id..string.format("Group %s: Removing target %s (id=%d).", self.Controllable:GetName(), name, id)) table.remove(self.targets, id) end + self:T(ARTY.id..string.format("Group %s: Number of targets = %d.", self.Controllable:GetName(), #self.targets)) +end + +--- Delete ALL targets from current target list. +-- @param #ARTY self +function ARTY:RemoveAllTargets() + self:F2() + for _,target in pairs(self.targets) do + self:RemoveTarget(target.name) + end end --- Define shell types that are counted to determine the ammo amount the ARTY group has. @@ -353,13 +394,10 @@ end function ARTY:onafterStart(Controllable, From, Event, To) self:_EventFromTo("onafterStart", Event, From, To) + -- Debug output. local text=string.format("Started ARTY for group %s.", Controllable:GetName()) MESSAGE:New(text, 10):ToAllIf(self.Debug) - -- Set the current ROE and alam state. - --self:_SetAlarmState(self.DefaultAlarmState) - --self:_SetROE(self.DefaultROE) - -- Get Ammo. self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0=self:_GetAmmo(self.Controllable) @@ -367,8 +405,11 @@ function ARTY:onafterStart(Controllable, From, Event, To) text=text..string.format("Arty group = %s\n", Controllable:GetName()) text=text..string.format("Artillery attribute = %s\n", tostring(self.IsArtillery)) text=text..string.format("Type = %s\n", self.Type) + text=text..string.format("Display Name = %s\n", self.DisplayName) text=text..string.format("Number of units = %d\n", self.IniGroupStrength) text=text..string.format("Max Speed [km/h] = %d\n", self.Speed) + text=text..string.format("Min range [km] = %d\n", self.minrange/1000) + text=text..string.format("Max range [km] = %d\n", self.maxrange/1000) text=text..string.format("Total ammo count = %d\n", self.Nammo0) text=text..string.format("Number of shells = %d\n", self.Nshells0) text=text..string.format("Number of rockets = %d\n", self.Nrockets0) @@ -401,13 +442,50 @@ function ARTY:onafterStart(Controllable, From, Event, To) self:HandleEvent(EVENTS.Dead, self._OnEventDead) -- Start scheduler to monitor task queue. - self.TargetQueueSched=SCHEDULER:New(nil, ARTY._TargetQueue, {self}, 5, self.TargetQueueUpdate) + self.SchedIDTargetQueue=self.scheduler:Schedule(self, ARTY._TargetQueue, {self}, 5, self.TargetQueueUpdate) -- Start scheduler to monitor if ARTY group started firing within a certain time. - self.CheckShootingSched=SCHEDULER:New(nil, ARTY._CheckShootingStarted, {self}, 60, 60) + self.SchedIDCheckShooting=self.scheduler:Schedule(self, ARTY._CheckShootingStarted, {self}, 60, 60) + + -- Start cheduler for status reports. + if self.Debug then + self.SchedIDStatusReport=self.scheduler:Schedule(self, ARTY._StatusReport, {self}, 30, 30) + end end +--- After "Start" event. Initialized ROE and alarm state. Starts the event handler. +-- @param #ARTY self +function ARTY:_StatusReport() + + -- Get Ammo. + local Nammo, Nshells, Nrockets, Nmissiles=self:_GetAmmo(self.Controllable) + + local text=string.format("\n******************************************************\n") + text=text..string.format("Status of ARTY = %s\n", self.Controllable:GetName()) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Total ammo count = %d\n", Nammo) + text=text..string.format("Number of shells = %d\n", Nshells) + text=text..string.format("Number of rockets = %d\n", Nrockets) + text=text..string.format("Number of missiles = %d\n", Nmissiles) + if self.currentTarget then + text=text..string.format("Current Target = %s\n", tostring(self.currentTarget.name)) + else + text=text..string.format("Current Target = %s\n", "none") + end + text=text..string.format("Nshots curr. Target = %d\n", self.Nshots) + text=text..string.format("Targets:\n") + for _, target in pairs(self.targets) do + local _clock=self:_SecondsToClock(target.time) + local _weapon=self:_WeaponTypeName(target.weapontype) + text=text..string.format("- %s, prio=%3d, radius=%5d, nshells=%4d, engaged=%3d, maxengage=%3d, weapon=%s, time=%s\n", + target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, _weapon, tostring(_clock)) + end + text=text..string.format("******************************************************") + env.info(ARTY.id..text) + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -465,19 +543,19 @@ function ARTY:_OnEventShot(EventData) -- Special weapon type requested ==> Check if corresponding ammo is empty. if self.currentTarget.weapontype==ARTY.WeaponType.UnguidedCannon and _nshells==0 then - self:T(ARTY.id.."Cannons requested and shells empty.") + self:T(ARTY.id.."Cannons requested but shells empty.") self:CeaseFire(self.currentTarget) return elseif self.currentTarget.weapontype==ARTY.WeaponType.UnguidedRockets and _nrockets==0 then - self:T(ARTY.id.."Rockets requested and rockets empty.") + self:T(ARTY.id.."Rockets requested but rockets empty.") self:CeaseFire(self.currentTarget) return elseif self.currentTarget.weapontype==ARTY.WeaponType.UnguidedAny and _nshells+_nrockets==0 then - self:T(ARTY.id.."Unguided weapon requested and shells+rockets empty.") + self:T(ARTY.id.."Unguided weapon requested but shells and rockets empty.") self:CeaseFire(self.currentTarget) return @@ -505,37 +583,27 @@ function ARTY:_OnEventShot(EventData) end end ---- Eventhandler for dead event. +--- Event handler for event Dead. -- @param #ARTY self -- @param Core.Event#EVENTDATA EventData function ARTY:_OnEventDead(EventData) self:F(EventData) -end - ---- Set task for firing at a coordinate. --- @param #ARTY self --- @param Core.Point#COORDINATE coord Coordinates to fire upon. --- @param #number radius Radius around coordinate. --- @param #number nshells Number of shells to fire. --- @param #number weapontype Type of weapon to use. -function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) - self:E({coord=coord, radius=radius, nshells=nshells}) - - -- Controllable. - local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE - - -- Set ROE to weapon free. - group:OptionROEOpenFire() - -- Get Vec2 - local vec2=coord:GetVec2() + env.info("FF event dead") - -- Get task. - local fire=group:TaskFireAtPoint(vec2, radius, nshells, weapontype) - - -- Execute task. - group:SetTask(fire) - --group:PushTask(fire) + -- Name of controllable. + local _name=self.Controllable:GetName() + + -- Check for correct group. + if EventData.IniGroupName==_name then + + -- Dead Unit. + self:T2(string.format("%s: Captured dead event for unit %s.", _name, EventData.IniUnitName)) + + -- FSM Dead event. We give one second for update of data base. + self:__Dead(1) + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -558,13 +626,7 @@ function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) if target.time~=nil and self.currentTarget~=nil and self.currentTarget.prio > target.prio then -- Debug info. self:T(ARTY.id..string.format("Group %s current target %s has lower prio than new target %s with attack time.", self.Controllable:GetName(), self.currentTarget.name, target.name)) - - -- Reset current task. - --self.Controllable:ClearTasks() - - -- Set number of shots counter to zero. - self.Nshots=0 - + -- Stop firing on current target. self:CeaseFire(self.currentTarget) @@ -580,7 +642,30 @@ function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) -- Deny transition. return false end + +-- Distance to target + local range=Controllable:GetCoordinate():Get2DDistance(target.coord) + + -- Check that distance to target is within range. + if rangeself.maxrange then + + -- Debug output. + local text + if rangeself.maxrange then + text=string.format("%s, target is out of range. Distance of %d km is greater than max range of %d km.", Controllable:GetName(), range/1000, self.maxrange/1000) + end + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + -- Remove target. + self:RemoveTarget(target.name) + + -- Deny transition. + return false + end + return true end @@ -605,10 +690,6 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) if id then -- Set under fire flag. self.targets[id].underfire=true - -- Increase engaged counter - self.targets[id].engaged=self.targets[id].engaged+1 - -- Clear the attack time. - self.targets[id].time=nil -- Set current target. self.currentTarget=target -- Set time the target was assigned. @@ -630,7 +711,7 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Before "CeaseFire" event. +--- Before "CeaseFire" event. Nothing to do at the moment. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. @@ -658,16 +739,28 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) local text=string.format("%s, ceasing fire on target %s.", Controllable:GetName(), target.name) self:T(ARTY.id..text) MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) - - -- Set number of shots to zero. - self.Nshots=0 - + -- Get target array index. local id=self:_GetTargetByName(target.name) - -- Target is not under fire any more. - self.targets[id].underfire=false + -- Increase engaged counter + if id then + -- Target was actually engaged. (Could happen that engagement was aborted while group was still aiming.) + if self.Nshots>0 then + self.targets[id].engaged=self.targets[id].engaged+1 + -- Clear the attack time. + self.targets[id].time=nil + end + -- Target is not under fire any more. + self.targets[id].underfire=false + end + -- Clear tasks. + self.Controllable:ClearTasks() + + -- Set number of shots to zero. + self.Nshots=0 + -- If number of engagements has been reached, the target is removed. if target.engaged >= target.maxengage then self:RemoveTarget(target.name) @@ -679,6 +772,21 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Before "Winchester" event. Cease fire on current target. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onbeforeWinchester(Controllable, From, Event, To) + + -- Cease fire first. + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + return true +end --- After "Winchester" event. Group is out of ammo. -- @param #ARTY self @@ -693,10 +801,7 @@ function ARTY:onafterWinchester(Controllable, From, Event, To) local text=string.format("%s, winchester.", Controllable:GetName()) self:T(ARTY.id..text) MESSAGE:New(text, 30):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - - -- Cease fire first. - self:CeaseFire(self.currentTarget) - + -- Init rearming if possible. self:Rearm() @@ -741,11 +846,14 @@ function ARTY:onafterRearm(Controllable, From, Event, To) local vec2=coord:GetRandomVec2InRadius(20, 100) local pops=COORDINATE:NewFromVec2(vec2) + -- Remember the coordinates of the rearming unit. After rearming it will go back to this position. + self.RearmingUnitCoord=self.RearmingUnit:GetCoordinate() + -- Route unit to ARTY group. self.RearmingUnit:RouteGroundOnRoad(pops, 50, 5) -- Start scheduler to monitor ammo count until rearming is complete. - self.CheckRearmedSched=SCHEDULER:New(nil,self._CheckRearmed, {self}, 20, 20) + self.SchedIDCheckRearmed=self.scheduler:Schedule(self, ARTY._CheckRearmed, {self}, 20, 20) end @@ -757,8 +865,18 @@ function ARTY:_CheckRearmed() -- Get current ammo. local nammo,nshells,nrockets,nmissiles=self:_GetAmmo(self.Controllable) + -- Number of units still alive. + local units=self.Controllable:GetUnits() + local nunits=0 + if units then + nunits=#units + end + + -- Full Ammo count. + self.FullAmmo=self.Nammo0 * nunits / self.IniGroupStrength + -- Rearming status in per cent. - local _rearmpc=nammo/self.Nammo0*100 + local _rearmpc=nammo/self.FullAmmo*100 -- Send message. local text=string.format("%s, rearming %d %% complete.", self.Controllable:GetName(), _rearmpc) @@ -766,7 +884,7 @@ function ARTY:_CheckRearmed() MESSAGE:New(text, 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) -- Rearming --> Rearmed --> CombatReady - if nammo==self.Nammo0 then + if nammo==self.FullAmmo then self:Rearmed() end @@ -784,17 +902,127 @@ function ARTY:onafterRearmed(Controllable, From, Event, To) -- Send message. local text=string.format("%s, rearming complete.", Controllable:GetName()) self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) -- Stop scheduler. - self.CheckRearmedSched:Stop() + --self.SchedCheckRearmed:Stop() + if self.SchedIDCheckRearmed then + self.scheduler:Stop(self.SchedIDCheckRearmed) + end + + -- Route unit back to where it came from. + self.RearmingUnit:RouteGroundOnRoad(self.RearmingUnitCoord, 50, 5) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Dead" event, when a unit has died. When all units of a group are dead, FSM is stopped and eventhandler removed. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterDead(Controllable, From, Event, To) + self:_EventFromTo("onafterDead", Event, From, To) + + -- Number of units left in the group. + local units=self.Controllable:GetUnits() + local nunits=0 + if units~=nil then + nunits=#units + end + + -- Adjust full ammo count + self.FullAmmo=self.Nammo0*nunits/self.IniGroupStrength + + -- Message. + local text=string.format("%s, one of our units just died! %d units left.", self.Controllable:GetName(), nunits) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(ARTY.id..text) + + -- Go to stop state. + if nunits==0 then + self:Stop() + end + +end + +--- Before "Stop" event. Cease fire on current target. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onbeforeStop(Controllable, From, Event, To) + self:_EventFromTo("onbeforeStop", Event, From, To) + + -- Cease Fire on current target. + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + return true +end + +--- After "Stop" event. Remove all target, stop schedulers, unhandle events and stop the FSM. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterStop(Controllable, From, Event, To) + self:_EventFromTo("onafterStop", Event, From, To) + + -- Debug info. + self:T(ARTY.id..string.format("Stopping ARTY FSM for group %s.", Controllable:GetName())) + -- Remove all targets. + --self:RemoveAllTargets() + -- Stop schedulers. + if self.SchedIDTargetQueue then + self.scheduler:Stop(self.SchedIDTargetQueue) + end + if self.SchedIDCheckShooting then + self.scheduler:Stop(self.SchedIDCheckShooting) + end + if self.SchedIDCheckRearmed then + self.scheduler:Stop(self.SchedIDCheckRearmed) + end + -- Unhandle event. + self:UnHandleEvent(EVENTS.Shot) + self:UnHandleEvent(EVENTS.Dead) +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Set task for firing at a coordinate. +-- @param #ARTY self +-- @param Core.Point#COORDINATE coord Coordinates to fire upon. +-- @param #number radius Radius around coordinate. +-- @param #number nshells Number of shells to fire. +-- @param #number weapontype Type of weapon to use. +function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) + self:E({coord=coord, radius=radius, nshells=nshells}) + + -- Controllable. + local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + + -- Set ROE to weapon free. + group:OptionROEOpenFire() + + -- Get Vec2 + local vec2=coord:GetVec2() + + -- Get task. + local fire=group:TaskFireAtPoint(vec2, radius, nshells, weapontype) + + -- Execute task. + group:SetTask(fire) + --group:PushTask(fire) +end + + --- Go through queue of assigned tasks. -- @param #ARTY self function ARTY:_TargetQueue() @@ -805,7 +1033,7 @@ function ARTY:_TargetQueue() -- No targets assigned at the moment. if #self.targets==0 then - self:T(ARTY.id..string.format("Group %s, no targets assigned at the moment. No need for _TargetQueue.", self.Controllable:GetName())) + self:T3(ARTY.id..string.format("Group %s, no targets assigned at the moment. No need for _TargetQueue.", self.Controllable:GetName())) return end @@ -904,15 +1132,18 @@ end -- @return Number of ALL shells left from the whole group. function ARTY:_GetAmmo(controllable) self:F2(controllable) - - -- Get all units. - local units=controllable:GetUnits() - + -- Init counter. local nammo=0 local nshells=0 local nrockets=0 local nmissiles=0 + + -- Get all units. + local units=controllable:GetUnits() + if units==nil then + return nammo, nshells, nrockets, nmissiles + end for _,unit in pairs(units) do @@ -1024,17 +1255,22 @@ function ARTY:_CheckShootingStarted() if self.currentTarget then -- Current time. - local Tnow=timer.getTime() + local Tnow=timer.getTime() + -- Get name and id of target. + local name=self.currentTarget.name + -- Time that passed after current target has been assigned. local dt=Tnow-self.currentTarget.Tassigned - + -- Debug info + if self.Nshots==0 then + self:T(ARTY.id..string.format("%s, waiting for %d seconds for first shot on target %s.", self.Controllable:GetName(), dt, name)) + end + + -- Check if we waited long enough and no shot was fired. if dt > self.WaitForShotTime and self.Nshots==0 then - -- Get name and id of target. - local name=self.currentTarget.name - -- Debug info. self:T(ARTY.id..string.format("%s, no shot event after %d seconds. Removing current target %s from list.", self.Controllable:GetName(), self.WaitForShotTime, name)) diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index c595a5dbb..db2092e9e 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -264,6 +264,7 @@ function GROUP:Destroy( GenerateEvent ) if self:IsAir() then self:CreateEventCrash( timer.getTime(), UnitData ) else + env.info("FF create event dead") self:CreateEventDead( timer.getTime(), UnitData ) end end diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 2734279bd..1c4236dc3 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -161,18 +161,28 @@ end --- Destroys the UNIT. -- @param #UNIT self +-- @param #boolean GenerateEvent (Optional) true if you want to generate a crash or dead event for the unit. -- @return #nil The DCS Unit is not existing or alive. -function UNIT:Destroy() +function UNIT:Destroy( GenerateEvent ) self:F2( self.ObjectName ) local DCSObject = self:GetDCSObject() if DCSObject then + local UnitGroup = self:GetGroup() local UnitGroupName = UnitGroup:GetName() self:F( { UnitGroupName = UnitGroupName } ) + + if GenerateEvent and GenerateEvent == true then + if self:IsAir() then + self:CreateEventCrash( timer.getTime(), DCSObject ) + else + self:CreateEventDead( timer.getTime(), DCSObject ) + end + end + USERFLAG:New( UnitGroupName ):Set( 100 ) - --BASE:CreateEventCrash( timer.getTime(), DCSObject ) DCSObject:destroy() end From 531c1d7e9018ef5d9b968d2f16e69f1aeb94fd29 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 3 May 2018 00:24:23 +0200 Subject: [PATCH 14/31] ARTY v0.6 (WIP) Added documentation Added user FSM functions. Adjusted FSM (untested) Added task route function. --- .../Moose/Functional/Artillery.lua | 714 +++++++++++++++--- 1 file changed, 604 insertions(+), 110 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 10c5282eb..d54830eb0 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -1,12 +1,20 @@ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **Functional** - Control artillery units. +--- **Functional** - (R2.4) Control artillery units. +-- +-- === -- -- ![Banner Image](..\Presentations\ARTY\Artillery_Main.png) -- -- ==== -- --- The ARTY class can be used to easily assign targets for artillery units. Multiple targets can be assigned. +-- The ARTY class can be used to easily assign targets for artillery units. -- +-- ## Features: +-- +-- * Multiple targets can be assigned. No restriction on number of targets. +-- * Targets can be given a priority. Engagement of targets is executed a according to their priority. +-- * Engagements can be scheduled, i.e. will be executed at a certain time of the day. +-- * Special weapon types can be selected. -- -- ==== -- @@ -24,13 +32,12 @@ -- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** -- --- ### Contributions: **Sven van de Velde ([FlightControl](https://forums.eagle.ru/member.php?u=89536))** +-- ### Contributions: **[FlightControl](https://forums.eagle.ru/member.php?u=89536)** -- -- ==== -- @module Arty ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- ARTY class -- @type ARTY -- @field #string ClassName Name of the class. @@ -45,7 +52,7 @@ -- @field Core.Scheduler#SCHEDULER scheduler Scheduler object handling various timed functions. -- @field #number SchedIDTargetQueue Scheduler ID for updating the target queue and calling OpenFire event. -- @field #number TargetQueueUpdate Interval between updates of the target queue. --- @field #number SchedIDCheckRearmed Scheduler ID responsible for checking whether reaming of the ARTY group is complete. +-- @field #number SchedIDCheckRearmed Scheduler ID responsible for checking whether rearming of the ARTY group is complete. -- @field #number SchedIDCheckShooting Scheduler ID for checking whether a group startet firing within a certain time after the fire at point task was assigned. -- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. Default is 300 seconds. -- @field #number SchedIDStatusReport Scheduler ID for status report messages. The scheduler is only launched in debug mode. @@ -57,6 +64,8 @@ -- @field #number Speed Max speed of ARTY group. -- @field Wrapper.Unit#UNIT RearmingUnit Unit designated to rearm the ARTY group. -- @field Wrapper.Point#COORDINATE RearmingUnitCoord Initial coordinates of the rearming unit. After rearming complete, the unit will return to this position. +-- @field Wrapper.Point#COORDINATE RearmingPlaceCoord Coordinates of the rearming place. If the place is more than 100 m away from the ARTY group, the group will go there. +-- @field Wrapper.Point#COORDINATE InitialCoord Initial coordinates of the ARTY group. -- @field #boolean report Arty group sends messages about their current state or target to its coaliton. -- @field #table ammoshells Table holding names of the shell types which are included when counting the ammo. Default is {"weapons.shells"} which include most shells. -- @field #table ammorockets Table holding names of the rocket types which are included when counting the ammo. Default is {"weapons.nurs"} which includes most unguided rockets. @@ -65,18 +74,152 @@ -- @field #number minrange Minimum firing range in kilometers. Targets closer than this distance are not engaged. Default 0 km. -- @field #number maxrange Maximum firing range in kilometers. Targets further away than this distance are not engaged. Default 10000 km. -- @extends Core.Fsm#FSM_CONTROLLABLE --- ---# ARTY class, extends @{Core.Fsm#FSM_CONTROLLABLE} --- Artillery class.. +-- +-- The ARTY class enables mission designers easily to assign targets for artillery units. Since the implementation is based on a Finite State Model (FSM), the mission designer can +-- interact with the process at certain events or states. -- --- ## Target aquisition... +-- A new ARTY object can be created with the @{#ARTY.New}(*group*) contructor. +-- The parameter *group* has to be a MOOSE Group object and defines ARTY group. +-- +-- The ARTY FSM process can be started by the @{#ARTY.Start}() command. +-- +-- ## The ARTY Process -- -- ![Process](..\Presentations\ARTY\Artillery_Process.png) -- --- The arty process can be described as follows. +-- After the FMS process is started the ARTY group will be in the state **CombatReady**. Once a target is assigned the **OpenFire** event will be triggered and the group starts +-- firing. At this point the group in in the state **Firing**. -- --- ### Submenu +-- When the defined number of shots has been fired on the current target the event **CeaseFire** is triggered. The group will stop firing and go back to the state **CombatReady**. +-- If another target is defined (or multiple engagements of the same target), the cycle starts anew. +-- +-- When the ARTY group runs out of ammunition, the event **Winchester** is triggered and the group enters the state **OutOfAmmo**. +-- In this state, the group is unable to engage further targets. +-- +-- With the @{#ARTY.SetRearmingUnit}(*unit*) command, a special unit can be defined to rearm the ARTY group. If this unit has been assigned and the group has entered the state +-- **OutOfAmmo** the event **Rearm** is triggered followed by a transition to the state **Rearming**. +-- If the rearming unit is less than 100 meters away from the ARTY group, the rearming process starts. If the rearming unit is more than 100 meters away from the ARTY unit, the +-- rearming unit is routed to a point 20 to 100 m from the ARTY group. +-- +-- Once the rearming is complete, the **Rearmed** event is triggered and the group enters the state **CombatReady**. At this point targeted can be engaged again. +-- +-- ## Assigning Targets +-- Assigning targets is a central point of the ARTY class. Multiple targets can be assigned simultanioulsly and are put into a queue. +-- Of course, targets can be added at any time during the mission. For example, once they are detected by a reconnaissance unit. +-- +-- In order to add a target, the function @{#ARTY.AssignTargetCoord}(*coord*, *prio*, *radius*, *nshells*, *maxengage*, *time*, *weapontype*, *name*) has to be used. +-- Only the first parameter *coord* is mandatory while all remaining parameters are all optional. +-- +-- ### Parameters: +-- +-- * *coord*: Coordinates of the target, given as @{Point#COORDINATE} object. +-- * *prio*: Priority of the target. This a number between 1 (high prio) and 100 (low prio). Targets with higher priority are engaged before targets with lower priority. +-- * *radius*: Radius in meters which defines the area the ARTY group will attempt to be hitting. Default is 100 meters. +-- * *nshells*: Number of shots (shells, rockets, missiles) fired by the group at each engagement of a target. Default is 5. +-- * *maxengage*: Number of times a target is engaged. +-- * *time*: Time of day the engagement is schedule in the format "hh:mm:ss" for hh=hours, mm=minutes, ss=seconds. +-- For example "10:15:35". In the case the attack will be executed at a quarter past ten in the morning at the day the mission started. +-- If the engagement should start on the following day the format can be specified as "10:15:35+1", where the +1 denots the following day. +-- This is useful for longer running missions or if the mission starts at 23:00 hours and the attack should be scheduled at 01:00 hours on the following day. +-- Of course, later days are also possible by appending "+2", "+3", etc. +-- **Note** that the time has to be given as a string. So the enclosing quotation marks "" are important. +-- * *weapontype*: Specified the weapon type that should be used for this attack if the ARTY group has multiple weapons to engage the target. +-- For example, this is useful for naval units which carry a bigger arsenal (cannons and missiles). Default is Auto, i.e. DCS logic selects the appropriate weapon type. +-- *name*: A special name can be defined for this target. Default name are the coordinates of the target in LL DMS format. If a name is already given for another target +-- or the same target should be attacked two or more times with different parameters a suffix "#01", "#02", "#03" is automatically appended to the specified name. +-- +-- ## Target Queue +-- In case, multiple targets have been defined, it is important to understand how the target queue works. +-- +-- Here, the important parameters are the priority *prio*, the number of engagements *maxengage* and the scheduled *time* as described above. +-- +-- For example, we have assigned two targets one with *prio*=10 and the other with *prio*=50 and both targets should be engaged three times (*maxengage*=3). +-- Let's first consider the case that none of the targets is scheduled to be executed at a certain time (*time*=nil). +-- The ARTY group will first engage the target with higher priority (*prio*=10). After the engagement is finished, the target with lower priority is attacked. +-- This is because the target with lower prio has been attacked one time less. After the attack on the lower priority task is finished and both targets +-- have been engaged equally often, the target with the higher priority is engaged again. This coninues until a target has engaged three times. +-- Once the maximum number of engagements is reached, the target is deleted from the queue. +-- +-- In other works, the queue is first sorted with respect to the number of engagements and targets with the same number of engagements are sorted with +-- respect to their priority. +-- +-- ### Timed Engagements +-- +-- As mentioned above, targets can be engaged at a specific time of the day via the *time* parameter. +-- +-- If the *time* parameter is specified for a target, the first engagement of that target will happen at that time of the day and not before. +-- This also applies when multiple engagements are requested via the *maxengage* parameter. The first attack will not happen before the specifed time. +-- When that timed attack is finished, the *time* parameter is deleted and the remaining engagements are carried out in the same manner as for untimed targets (described above). +-- +-- Of course, it can happen that a scheduled task should be executed at a time, when another target is already under attack. +-- If the priority of the target is higher than the priority of the current target, then the current attack is cancelled and the engagement of the target with the higher +-- priority is started. +-- +-- By contrast, if the current target has a higher priority than the target scheduled at that time, the current attack is finished before the scheduled attack is started. +-- +-- ## Determining the Amount of Ammo +-- +-- In order to determin when a unit is out of ammo and possible initiate the rearming process it is necessary to know which types of weapons have to be counted. +-- For most artillery unit types, this is simple because they only have one type of weapon and hence ammunition. +-- +-- However, there are more complex scenarios. For example, naval units carry a big arsenal of different ammunition types ranging from various cannon shell types +-- over surface-to-air missiles to cruise missiles. Obviously, not all of these ammo types can be employed for artillery tasks. +-- +-- Unfortunately, there is no easy way to count only those ammo types useable as artillery. Therefore, to keep the implementation general the user +-- can specify the names of the ammo types by the following functions: +-- +-- * @{#ARTY.SetShellTypes}(*tableofnames*): Defines the ammo types for unguided cannons. Default is *tableofnames*={"weapons.shells"}, i.e. **all** types of shells are counted. +-- * @{#ARTY.SetRocketTypes}(*tableofnames*): Defines the ammo types of unguided rockets. Default is *tableofnames*={"weapons.nurs"}, i.e. **all** types of rockets are counted. +-- * @{#ARTY.SetMissileTypes}(*tableofnames*): Defines the ammo types of guided missiles. Default is *tableofnames*={"weapons.missiles"}, i.e. **all** types of missiles are counted. +-- +-- **Note** that the default parameters "weapons.shells", "weapons.nurs", "weapons.missiles" **should in priciple** capture all the corresponding ammo types. +-- However, the logic searches for the string "weapon.missies" in the ammo type. Especially for missiles, this string is often not contained in the ammo type descriptor. +-- +-- One way to determin which types of ammo the unit carries, one can use the debug mode of the arty class via @{#ARTY.SetDebugON}(). +-- In debug mode, the all ammo types of the group are printed to the monitor as message and can be found in the DCS.log file. +-- +-- ## Empoying Selected Weapons +-- +-- If an ARTY group carries multiple weapons, which can be used for artillery task, a certain weapon type can be selected to attack the target. +-- This is done via the *weapontype* parameter of the @{#ARTY.AssignTargetCoord}(..., *weapontype*, ...) function. +-- +-- The enumerator @{#ARTY.WeaponType} has been defined to select a certain weapon type. Supported values are: +-- +-- * @{#ARTY.WeaponType}.Auto: Automatic weapon selection by the DCS logic. This is the default setting. +-- * @{#ARTY.WeaponType}.Cannon: Only cannons are used during the attack. Corresponding ammo type are shells and can be defined by @{#ARTY.SetShellTypes}. +-- * @{#ARTY.WeaponType}.Rockets: Only unguided are used during the attack. Corresponding ammo type are rockets/nurs and can be defined by @{#ARTY.SetRocketTypes}. +-- * @{#ARTY.WeaponType}.UnguidedAny: Any unguided weapon (cannons or rockes) will be used. +-- * @{#ARTY.WeaponType}.GuidedMissile: Any guided missiles are used during the attack. Corresponding ammo type are missiles and can be defined by @{#ARTY.SetMissileTypes}. +-- * @{#ARTY.WeaponType}.CruiseMissile: Only cruise missiles are used during the attack. Corresponding ammo type are missiles and can be defined by @{#ARTY.SetMissileTypes}. +-- +-- ## Fine Tuning +-- +-- The mission designer has a few options to tailor the ARTY object according to his needs. +-- +-- * @{#ARTY.RemoveAllTargets}() removes all targets from the target queue. +-- * @{#ARTY.RemoveTarget}(*name*) deletes the target with *name* from the target queue. +-- * @{#ARTY.SetMaxFiringRange}(*range*) defines the maximum firing range. Targets further away than this distance are not engaged. +-- * @{#ARTY.SetMinFiringRange}(*range*) defines the minimum firing range. Targets closer than this distance are not engaged. +-- * @{#ARTY.SetRearmingUnit}(*unit*) sets the unit resposible for rearming of the ARTY group once it is out of ammo. +-- * @{#ARTY.SetReportON}() and @{#ARTY.SetReportOFF}() can be used to enable/disable status reports of the ARTY group send to all coalition members. +-- * @{#ARTY.SetTargetQueueUpdateInterval}(*interval*) sets the interval (in seconds) at which the target queue is updated. Default is every 5 seconds. +-- * @{#ARTY.SetWaitForShotTime}(*waittime*) sets the time after which a target is deleted from the queue if no shooting event occured after the target engagement started. +-- Default is 300 seconds. Note that this can for example happen, when the assigned target is out of range. +-- * @{#ARTY.SetDebugON}() and @{#ARTY.SetDebugOFF}() can be used to enable/disable the debug mode. +-- +-- ## Examples +-- +-- ### Assigning Multiple Targets +-- This basic example illustrates how to assign multiple targets. +-- +-- ### Scheduled Engagements +-- This example shows how to execute an engagement at a certain time. +-- +-- ### Specific Weapons +-- This example demonstrates how to use specific weapons during an engagement. +-- -- -- @field #ARTY ARTY={ @@ -103,6 +246,8 @@ ARTY={ IsArtillery=nil, RearmingUnit=nil, RearmingUnitCoord=nil, + RearmingPlaceCoord=nil, + InitialCoord=nil, report=true, ammoshells={"weapons.shells"}, ammorockets={"weapons.nurs"}, @@ -116,10 +261,9 @@ ARTY={ -- @list WeaponType ARTY.WeaponType={ Auto=1073741822, + Cannon=805306368, + Rockets=30720, UnguidedAny=805339120, - UnguidedCannon=805306368, - UnguidedRockets=30720, - GuidedAny=268402702, GuidedMissile=268402688, CruiseMissile=2097152, } @@ -128,26 +272,26 @@ ARTY.WeaponType={ -- @field #string id ARTY.id="ARTY | " ---- Range script version. +--- Arty script version. -- @field #number version -ARTY.version="0.5.0" +ARTY.version="0.6.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list: -- DONE: Delete targets from queue user function. -- DONE: Delete entire target queue user function. --- TODO: Add weapon types. Done but needs improvements. +-- DONE: Add weapon types. Done but needs improvements. -- DONE: Add user defined rearm weapon types. --- TODO: Check if target is in range. Maybe this requires a data base with the ranges of all arty units. Pfff... --- TODO: Make ARTY move to reaming position. --- TODO: Check that right reaming vehicle is specified. Blue M818, Red Ural-375. Are there more? --- TODO: Check if ARTY group is still alive. +-- DONE: Check if target is in range. Maybe this requires a data base with the ranges of all arty units. +-- DONE: Make ARTY move to rearming position. +-- DONE: Check that right rearming vehicle is specified. Blue M818, Red Ural-375. Are there more? +-- DONE: Check if ARTY group is still alive. -- DONE: Handle dead events. -- DONE: Abort firing task if no shooting event occured with 5(?) minutes. Something went wrong then. Min/max range for example. -- DONE: Improve assigned time for engagement. Next day? --- TODO: Improve documentation. --- TODO: Add pseudo user transitions. OnAfter... +-- DONE: Improve documentation. +-- DONE: Add pseudo user transitions. OnAfter... ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -179,6 +323,9 @@ function ARTY:New(group) -- Set the controllable for the FSM. self:SetControllable(group) + -- Set the initial coordinates of the ARTY group. + self.InitialCoord=group:GetCoordinate() + -- Create scheduler object. self.scheduler=SCHEDULER:New(self) @@ -211,13 +358,177 @@ function ARTY:New(group) -- Transitions self:AddTransition("*", "Start", "CombatReady") self:AddTransition("CombatReady", "OpenFire", "Firing") - self:AddTransition("Firing", "OpenFire", "Firing") -- Other target assigned + self:AddTransition("CombatReady", "Winchester", "OutOfAmmo") + self:AddTransition("Firing", "OpenFire", "Firing") self:AddTransition("Firing", "CeaseFire", "CombatReady") - self:AddTransition("*", "Winchester", "OutOfAmmo") - self:AddTransition("*", "Rearm", "Rearming") + self:AddTransition("OutOfAmmo", "Rearm", "Rearming") self:AddTransition("Rearming", "Rearmed", "CombatReady") + self:AddTransition("CombatReady", "Move", "Moving") + self:AddTransition("Moving", "Arrived", "CombatReady") self:AddTransition("*", "Dead", "*") + --- User function for OnBefore "OpenFire" event. + -- @function [parent=#ARTY] OnBeforeOpenFire + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table target Array holding the target info. + -- @return #boolean If true, allow transition to OnAfterOpenFire. + + --- User function for OnAfter "OpenFire" event. + -- @function [parent=#ARTY] OnAfterOpenFire + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table target Array holding the target info. + + + --- User function for OnBefore "CeaseFire" event. + -- @function [parent=#ARTY] OnBeforeCeaseFire + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table target Array holding the target info. + -- @return #boolean If true, allow transition to OnAfterCeaseFire. + + --- User function for OnAfter "CeaseFire" event. + -- @function [parent=#ARTY] OnAfterCeaseFire + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table target Array holding the target info. + + + --- User function for OnBefore "Winchester" event. + -- @function [parent=#ARTY] OnBeforeWinchester + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean If true, allow transition to OnAfterWinchester. + + --- User function for OnAfter "Winchester" event. + -- @function [parent=#ARTY] OnAfterWinchester + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- User function for OnBefore "Rearm" event. + -- @function [parent=#ARTY] OnBeforeRearm + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean If true, allow transition to OnAfterRearm. + + --- User function for OnAfter "Rearm" event. + -- @function [parent=#ARTY] OnAfterRearm + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- User function for OnBefore "Rearmed" event. + -- @function [parent=#ARTY] OnBeforeRearmed + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean If true, allow transition to OnAfterRearmed. + + --- User function for OnAfter "Rearmed" event. + -- @function [parent=#ARTY] OnAfterRearmed + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- User function for OnBefore "Start" event. + -- @function [parent=#ARTY] OnBeforeStart + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean If true, allow transition to OnAfterStart. + + --- User function for OnAfter "Start" event. + -- @function [parent=#ARTY] OnAfterStart + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- User function for OnBefore "Dead" event. + -- @function [parent=#ARTY] OnBeforeDead + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean If true, allow transition to OnAfterDead. + + --- User function for OnAfter "Dead" event. + -- @function [parent=#ARTY] OnAfterDead + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- User function for OnEnter "CombatReady" state. + -- @function [parent=#ARTY] OnEnterCombatReady + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "Firing" state. + -- @function [parent=#ARTY] OnEnterFiring + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "OutOfAmmo" state. + -- @function [parent=#ARTY] OnEnterOutOfAmmo + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "Rearming" state. + -- @function [parent=#ARTY] OnEnterRearming + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + return self end @@ -306,6 +617,14 @@ function ARTY:SetRearmingUnit(unit) self.RearmingUnit=unit end +--- Defines the rearming place of the ARTY group. If the place is too far away from the ARTY group it will be routed to the place. +-- @param #ARTY self +-- @param Wrapper.Point#COORDINATE coord Coordinates of the rearming place. +function ARTY:SetRearmingPlace(coord) + self:F({coord=coord}) + self.RearmingPlaceCoord=coord +end + --- Report messages of ARTY group turned on. This is the default. -- @param #ARTY self function ARTY:SetReportON() @@ -318,6 +637,18 @@ function ARTY:SetReportOFF() self.report=false end +--- Turn debug mode on. Information is printed to screen. +-- @param #ARTY self +function ARTY:SetDebugON() + self.Debug=true +end + +--- Turn debug mode off. This is the default setting. +-- @param #ARTY self +function ARTY:SetDebugOFF() + self.Debug=false +end + --- Set target queue update time interval. -- @param #ARTY self -- @param #number interval Time interval in seconds. Default is 5 seconds. @@ -407,13 +738,20 @@ function ARTY:onafterStart(Controllable, From, Event, To) text=text..string.format("Type = %s\n", self.Type) text=text..string.format("Display Name = %s\n", self.DisplayName) text=text..string.format("Number of units = %d\n", self.IniGroupStrength) - text=text..string.format("Max Speed [km/h] = %d\n", self.Speed) - text=text..string.format("Min range [km] = %d\n", self.minrange/1000) - text=text..string.format("Max range [km] = %d\n", self.maxrange/1000) + text=text..string.format("Max Speed = %d km/h\n", self.Speed) + text=text..string.format("Min range = %d km\n", self.minrange/1000) + text=text..string.format("Max range = %d km\n", self.maxrange/1000) text=text..string.format("Total ammo count = %d\n", self.Nammo0) text=text..string.format("Number of shells = %d\n", self.Nshells0) text=text..string.format("Number of rockets = %d\n", self.Nrockets0) text=text..string.format("Number of missiles = %d\n", self.Nmissiles0) + if self.RearmingUnit then + text=text..string.format("Reaming unit = %s\n", self.RearmingUnit:GetName()) + end + if self.RearmingPlaceCoord then + local dist=self.InitialCoord:Get2DDistance(self.RearmingPlaceCoord) + text=text..string.format("Reaming coord dist. = %d m\n", dist) + end text=text..string.format("******************************************************\n") text=text..string.format("Targets:\n") for _, target in pairs(self.targets) do @@ -460,6 +798,7 @@ function ARTY:_StatusReport() -- Get Ammo. local Nammo, Nshells, Nrockets, Nmissiles=self:_GetAmmo(self.Controllable) + local Tnow=timer.getTime() local text=string.format("\n******************************************************\n") text=text..string.format("Status of ARTY = %s\n", self.Controllable:GetName()) @@ -469,7 +808,8 @@ function ARTY:_StatusReport() text=text..string.format("Number of rockets = %d\n", Nrockets) text=text..string.format("Number of missiles = %d\n", Nmissiles) if self.currentTarget then - text=text..string.format("Current Target = %s\n", tostring(self.currentTarget.name)) + text=text..string.format("Current Target = %s\n", tostring(self.currentTarget.name)) + text=text..string.format("Curr. Tgt assigned = %d\n", Tnow-self.currentTarget.Tassigned) else text=text..string.format("Current Target = %s\n", "none") end @@ -528,7 +868,9 @@ function ARTY:_OnEventShot(EventData) if _nammo==0 then - self:E(ARTY.id..string.format("Group %s completely out of ammo.", self.Controllable:GetName())) + self:T(ARTY.id..string.format("Group %s completely out of ammo.", self.Controllable:GetName())) + -- Cease fire first. + self:CeaseFire(self.currentTarget) self:Winchester() -- Current target is deallocated ==> return @@ -537,17 +879,17 @@ function ARTY:_OnEventShot(EventData) -- Weapon type name for current target. local _weapontype=self:_WeaponTypeName(self.currentTarget.weapontype) - self:E(ARTY.id..string.format("nammo=%d, nshells=%d, nrockets=%d, nmissiles=%d", _nammo, _nshells, _nrockets, _nmissiles)) - self:E(ARTY.id..string.format("Weapontype = %s", _weapontype)) + self:T(ARTY.id..string.format("nammo=%d, nshells=%d, nrockets=%d, nmissiles=%d", _nammo, _nshells, _nrockets, _nmissiles)) + self:T(ARTY.id..string.format("Weapontype = %s", _weapontype)) -- Special weapon type requested ==> Check if corresponding ammo is empty. - if self.currentTarget.weapontype==ARTY.WeaponType.UnguidedCannon and _nshells==0 then + if self.currentTarget.weapontype==ARTY.WeaponType.Cannon and _nshells==0 then self:T(ARTY.id.."Cannons requested but shells empty.") self:CeaseFire(self.currentTarget) return - elseif self.currentTarget.weapontype==ARTY.WeaponType.UnguidedRockets and _nrockets==0 then + elseif self.currentTarget.weapontype==ARTY.WeaponType.Rockets and _nrockets==0 then self:T(ARTY.id.."Rockets requested but rockets empty.") self:CeaseFire(self.currentTarget) @@ -559,9 +901,9 @@ function ARTY:_OnEventShot(EventData) self:CeaseFire(self.currentTarget) return - elseif self.currentTarget.weapontype==ARTY.WeaponType.CruiseMissile and _nmissiles==0 then + elseif (self.currentTarget.weapontype==ARTY.WeaponType.CruiseMissile or self.currentTarget.weapontype==ARTY.WeaponType.CruiseMissile) and _nmissiles==0 then - self:E(ARTY.id.."Cruise missiles requested and missiles empty.") + self:T(ARTY.id.."Guided or Cruise missiles requested but all missiles empty.") self:CeaseFire(self.currentTarget) return end @@ -588,9 +930,7 @@ end -- @param Core.Event#EVENTDATA EventData function ARTY:_OnEventDead(EventData) self:F(EventData) - - env.info("FF event dead") - + -- Name of controllable. local _name=self.Controllable:GetName() @@ -610,18 +950,17 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Before "OpenFire" event. +--- Before "OpenFire" event. Checks if group already has a target. Checks for valid min/max range and removes the target if necessary. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #table target Array holding the target info. --- @return #boolean If true proceed to onafterOpenfire. +-- @return #boolean If true, proceed to onafterOpenfire. function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onbeforeOpenFire", Event, From, To) - - + -- If this target has an attack time and it's prio is higher than the current task, we allow the transition. if target.time~=nil and self.currentTarget~=nil and self.currentTarget.prio > target.prio then -- Debug info. @@ -637,7 +976,7 @@ function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) -- Check that group has no current target already. if self.currentTarget then -- Debug info. - self:T(ARTY.id..string.format("Group %s already has a target %s.", self.Controllable:GetName(), self.currentTarget.name)) + self:T2(ARTY.id..string.format("Group %s already has a target %s.", self.Controllable:GetName(), self.currentTarget.name)) -- Deny transition. return false @@ -669,18 +1008,17 @@ function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) return true end ---- After "OpenFire" event. +--- After "OpenFire" event. Sets the current target and starts the fire at point task. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #table target Array holding the target info. _target={coord=coord, radius=radius, nshells=nshells, engaged=0, underattack=false} +-- @param #table target Array holding the target info. function ARTY:onafterOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onafterOpenFire", Event, From, To) - local _coord=target.coord --Core.Point#COORDINATE - + --local _coord=target.coord --Core.Point#COORDINATE --_coord:MarkToAll("Arty Target") -- Get target array index. @@ -711,21 +1049,7 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Before "CeaseFire" event. Nothing to do at the moment. --- @param #ARTY self --- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param #table target Array holding the target info. --- @return #boolean -function ARTY:onbeforeCeaseFire(Controllable, From, Event, To, target) - self:_EventFromTo("onbeforeCeaseFire", Event, From, To) - - return true -end - ---- After "CeaseFire" event. +--- After "CeaseFire" event. Clears task of the group and removes the target if max engagement was reached. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. @@ -778,17 +1102,15 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. +-- @return #boolean If true, proceed to onafterWinchester. function ARTY:onbeforeWinchester(Controllable, From, Event, To) - -- Cease fire first. - if self.currentTarget then - self:CeaseFire(self.currentTarget) - end + return true end ---- After "Winchester" event. Group is out of ammo. +--- After "Winchester" event. Group is out of ammo. Trigger "Rearm" event. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. @@ -815,18 +1137,20 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. +-- @return #boolean If true, proceed to onafterRearm. function ARTY:onbeforeRearm(Controllable, From, Event, To) self:_EventFromTo("onbeforeRearm", Event, From, To) if self.RearmingUnit and self.RearmingUnit:IsAlive() then return true + elseif self.RearmingPlaceCoord then + return true else return false end end - --- After "Rearm" event. Send message if reporting is on. Route rearming unit to ARTY group. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -836,28 +1160,95 @@ end function ARTY:onafterRearm(Controllable, From, Event, To) self:_EventFromTo("onafterRearm", Event, From, To) - -- Send message. - local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingUnit:GetName()) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + -- Coordinate of ARTY unit. + local coordARTY=self.Controllable:GetCoordinate() + local coordRARM + if self.RearmingUnit then + -- Coordinate of the rearming unit. + coordRARM=self.RearmingUnit:GetCoordinate() + -- Remember the coordinates of the rearming unit. After rearming it will go back to this position. + self.RearmingUnitCoord=coordRARM + end - -- Random point 20-100 m away from unit. - local coord=self.Controllable:GetCoordinate() - local vec2=coord:GetRandomVec2InRadius(20, 100) - local pops=COORDINATE:NewFromVec2(vec2) + if self.RearmingUnit and self.RearmingPlaceCoord and self.Speed>0 then - -- Remember the coordinates of the rearming unit. After rearming it will go back to this position. - self.RearmingUnitCoord=self.RearmingUnit:GetCoordinate() + -- Rearming unit and ARTY group meet at rearming place. + local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) + local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord) + + -- Route ARTY group to rearming place. + if dA>100 then + self.Controllable:RouteGroundOnRoad(self.RearmingPlaceCoord, self.Speed, 1) + end + + -- Route Rearming unit to rearming place + if dR>100 then + self.RearmingUnit:RouteGroundOnRoad(self.RearmingPlaceCoord, 50, 1) + end - -- Route unit to ARTY group. - self.RearmingUnit:RouteGroundOnRoad(pops, 50, 5) + elseif self.RearmingUnit then + + -- Send message. + local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingUnit:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + + -- Distance between ARTY group and rearming unit. + local distance=coordARTY:Get2DDistance(coordRARM) + + -- If distance is larger than 100 m, the Rearming unit is routed to the ARTY group. + if distance > 100 then + -- Random point 20-100 m away from unit. + local vec2=coord:GetRandomVec2InRadius(20, 100) + local pops=COORDINATE:NewFromVec2(vec2) + + -- Route unit to ARTY group. + self.RearmingUnit:RouteGroundOnRoad(pops, 50, 1) + end + end -- Start scheduler to monitor ammo count until rearming is complete. self.SchedIDCheckRearmed=self.scheduler:Schedule(self, ARTY._CheckRearmed, {self}, 20, 20) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Check if ARTY group is reamed. +--- After "Rearmed" event. Send message if reporting is on and stop the scheduler. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterRearmed(Controllable, From, Event, To) + self:_EventFromTo("onafterRearmed", Event, From, To) + + -- Send message. + local text=string.format("%s, rearming complete.", Controllable:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + + -- Stop scheduler. + if self.SchedIDCheckRearmed then + self.scheduler:Stop(self.SchedIDCheckRearmed) + end + + -- Route ARTY group backto where it came from (if distance is > 100 m). + local d1=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) + if d1>100 then + self.Controllable:RouteGroundOnRoad(self.InitialCoord, self.Speed, 5) + end + + -- Route unit back to where it came from (if distance is > 100 m). + if self.RearmingUnit and self.RearmingUnit:IsAlive() then + local d=self.RearmingUnit:GetCoordinate():Get2DDistance(self.RearmingUnitCoord) + if d>100 then + self.RearmingUnit:RouteGroundOnRoad(self.RearmingUnitCoord, 50, 1) + end + end + +end + +--- Check if ARTY group is rearmed. -- @param #ARTY self function ARTY:_CheckRearmed() self:F2() @@ -890,33 +1281,61 @@ function ARTY:_CheckRearmed() end ---- After "Rearmed" event. Send message if reporting is on and stop the scheduler. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Move" event. Check if a unit to rearm the ARTY group has been defined. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function ARTY:onafterRearmed(Controllable, From, Event, To) - self:_EventFromTo("onafterRearmed", Event, From, To) +-- @param Wrapper.Point#COORDINATE ToCoord Coordinate to which the ARTY group should move. +-- @param #boolean OnRoad If true group should move on road mainly. +-- @return #boolean If true, proceed to onafterMove. +function ARTY:onbeforeMove(Controllable, From, Event, To, ToCoord, OnRoad) + self:_EventFromTo("onbeforeMove", Event, From, To) - -- Send message. - local text=string.format("%s, rearming complete.", Controllable:GetName()) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - - -- Stop scheduler. - --self.SchedCheckRearmed:Stop() - if self.SchedIDCheckRearmed then - self.scheduler:Stop(self.SchedIDCheckRearmed) + -- Check if group can actually move... + if self.Speed==0 then + return false + end + + -- Cease fire first. + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + return true +end + +--- After "Move" event. Route group to given coordinate. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Point#COORDINATE ToCoord Coordinate to which the ARTY group should move. +-- @param #boolean OnRoad If true group should move on road mainly. +function ARTY:onafterMove(Controllable, From, Event, To, ToCoord, OnRoad) + self:_EventFromTo("onafterMove", Event, From, To) + + -- Set alarm state to green and ROE to weapon hold. + self.Controllable:OptionAlarmStateGreen() + self.Controllable:OptionROEHoldFire() + + -- Route group to coodinate. + if OnRoad then + self.Controllable:RouteGroundOnRoad(ToCoord, self.Speed, 1) + else + self.Controllable:RouteGroundTo(ToCoord, self.Speed, "Vee", 1) end - -- Route unit back to where it came from. - self.RearmingUnit:RouteGroundOnRoad(self.RearmingUnitCoord, 50, 5) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- After "Dead" event, when a unit has died. When all units of a group are dead, FSM is stopped and eventhandler removed. +--- After "Dead" event, when a unit has died. When all units of a group are dead trigger "Stop" event. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. @@ -953,6 +1372,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. +-- @return #boolean If true, proceed to onafterStop. function ARTY:onbeforeStop(Controllable, From, Event, To) self:_EventFromTo("onbeforeStop", Event, From, To) @@ -964,7 +1384,7 @@ function ARTY:onbeforeStop(Controllable, From, Event, To) return true end ---- After "Stop" event. Remove all target, stop schedulers, unhandle events and stop the FSM. +--- After "Stop" event. Stop schedulers and unhandle events. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. @@ -1351,20 +1771,17 @@ function ARTY:_WeaponTypeName(tnumber) local name="unknown" if tnumber==ARTY.WeaponType.Auto then name="Auto (Cannon, Rockets, Missiles)" - elseif tnumber==ARTY.WeaponType.CruiseMissile then - name="Cruise Missile" - elseif tnumber==ARTY.WeaponType.GuidedAny then - name="Any Guided Missile" - elseif tnumber==ARTY.WeaponType.GuidedMissile then - name="Guided Missile" + elseif tnumber==ARTY.WeaponType.Cannon then + name="Cannon" + elseif tnumber==ARTY.WeaponType.Rockets then + name="Rockets" elseif tnumber==ARTY.WeaponType.UnguidedAny then name="Any Unguided Weapon (Cannon or Rockets)" - elseif tnumber==ARTY.WeaponType.UnguidedCannon then - name="Unguided Cannon" - elseif tnumber==ARTY.WeaponType.UnguidedRockets then - name="Unguided Rockets" + elseif tnumber==ARTY.WeaponType.CruiseMissile then + name="Cruise Missile" + elseif tnumber==ARTY.WeaponType.GuidedMissile then + name="Guided Missile" end - return name end @@ -1471,4 +1888,81 @@ function ARTY:_ClockToSeconds(clock) return seconds end -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Route group to a certain point. +-- @param #ARTY self +-- @param Wrapper.Group#GROUP group Group to route. +-- @param Core.Point#COORDINATE ToCoord Coordinate where we want to go. +-- @param #number Speed Speed in km/h. +-- @param #boolean OnRoad If true, use (mainly) roads. +function ARTY:_Move(group, ToCoord, Speed, OnRoad) + + -- Clear all tasks. + group:ClearTasks() + group:OptionAlarmStateGreen() + group:OptionROEHoldFire() + + -- Set formation. + local formation = "Off road" + + -- Current coordinates of group. + local cpini=group:GetCoordinate() + cpini:SmokeWhite() + + -- Distance between current and final point. + local dist=cpini:Get2DDistance(ToCoord) + + -- Waypoint and task arrays. + local path={} + local task={} + + -- First waypoint is the current position of the group. + path[#path+1]=cpini:WaypointGround(Speed, formation) + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, 0, false) + + path[#path+1]=ToCoord:WaypointGround(Speed, formation) + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, 1, true) + + -- Init waypoints of the group. + local Waypoints={} + + -- New points are added to the default route. + for i,p in ipairs(path) do + table.insert(Waypoints, i, path[i]) + end + + -- Set task for all waypoints. + for i,wp in ipairs(Waypoints) do + group:SetTaskWaypoint(Waypoints[i], task[i]) + end + + -- Submit task and route group along waypoints. + group:Route(Waypoints) + +end + +--- Function called when group is passing a waypoint. +-- @param Wrapper.Group#GROUP group Group for which waypoint passing should be monitored. +-- @param #ARTY arty ARTY object. +-- @param #number i Waypoint number that has been reached. +-- @param #boolean final True if it is the final waypoint. +function ARTY._PassingWaypoint(group, arty, i, final) + + -- Debug message. + local text=string.format("Group %s passing waypoint %d (final=%s)", group:GetName(), i, tostring(final)) + + local pos=group:GetCoordinate() + local MarkerID=pos:MarkToAll(string.format("Reached Waypoint %d of group %s", i, group:GetName())) + pos:SmokeRed() + + MESSAGE:New(text,10):ToAll() + env.info(ARTY.id..text) + + -- Move --> Moving --> Arrived --> CombatReady. + if final then + arty:Arrived() + end + +end + \ No newline at end of file From 4fccfa38d425967ab60347bccc36635e1a5ee8f2 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 3 May 2018 23:42:03 +0200 Subject: [PATCH 15/31] ARTY v0.7 Added "NewTarget" event. Improved task function for waypoints Removed TargetQueue scheduler. --- Moose Development/Moose/Core/Fsm.lua | 8 +- .../Moose/Functional/Artillery.lua | 221 +++++++++++++----- 2 files changed, 166 insertions(+), 63 deletions(-) diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index 4e959004f..62b80aee2 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -682,15 +682,15 @@ do -- FSM end if execute then + self:_call_handler("onafter", EventName, Params, EventName ) + self:_call_handler("OnAfter", EventName, Params, EventName ) + -- only execute the call if the From state is not equal to the To state! Otherwise this function should never execute! --if from ~= to then self:_call_handler("onenter", To, Params, EventName ) self:_call_handler("OnEnter", To, Params, EventName ) --end - - self:_call_handler("onafter", EventName, Params, EventName ) - self:_call_handler("OnAfter", EventName, Params, EventName ) - + self:_call_handler("onstate", "change", Params, EventName ) end else diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index d54830eb0..41e6e1f29 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -3,7 +3,7 @@ -- -- === -- --- ![Banner Image](..\Presentations\ARTY\Artillery_Main.png) +-- ![Banner Image](..\Presentations\ARTY\ARTY_Main.png) -- -- ==== -- @@ -50,8 +50,6 @@ -- @field #number Nmissiles0 Initial amount of missiles of the whole group. -- @field #number FullAmmo Full amount of all ammunition taking the number of alive units into account. -- @field Core.Scheduler#SCHEDULER scheduler Scheduler object handling various timed functions. --- @field #number SchedIDTargetQueue Scheduler ID for updating the target queue and calling OpenFire event. --- @field #number TargetQueueUpdate Interval between updates of the target queue. -- @field #number SchedIDCheckRearmed Scheduler ID responsible for checking whether rearming of the ARTY group is complete. -- @field #number SchedIDCheckShooting Scheduler ID for checking whether a group startet firing within a certain time after the fire at point task was assigned. -- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. Default is 300 seconds. @@ -87,7 +85,7 @@ -- -- ## The ARTY Process -- --- ![Process](..\Presentations\ARTY\Artillery_Process.png) +-- ![Process](..\Presentations\ARTY\ARTY_Process.png) -- -- After the FMS process is started the ARTY group will be in the state **CombatReady**. Once a target is assigned the **OpenFire** event will be triggered and the group starts -- firing. At this point the group in in the state **Firing**. @@ -204,7 +202,6 @@ -- * @{#ARTY.SetMinFiringRange}(*range*) defines the minimum firing range. Targets closer than this distance are not engaged. -- * @{#ARTY.SetRearmingUnit}(*unit*) sets the unit resposible for rearming of the ARTY group once it is out of ammo. -- * @{#ARTY.SetReportON}() and @{#ARTY.SetReportOFF}() can be used to enable/disable status reports of the ARTY group send to all coalition members. --- * @{#ARTY.SetTargetQueueUpdateInterval}(*interval*) sets the interval (in seconds) at which the target queue is updated. Default is every 5 seconds. -- * @{#ARTY.SetWaitForShotTime}(*waittime*) sets the time after which a target is deleted from the queue if no shooting event occured after the target engagement started. -- Default is 300 seconds. Note that this can for example happen, when the assigned target is out of range. -- * @{#ARTY.SetDebugON}() and @{#ARTY.SetDebugOFF}() can be used to enable/disable the debug mode. @@ -233,8 +230,6 @@ ARTY={ Nmissiles0=0, FullAmmo=0, scheduler=nil, - SchedIDTargetQueue=nil, - TargetQueueUpdate=5, SchedIDCheckRearmed=nil, SchedIDCheckShooting=nil, WaitForShotTime=300, @@ -274,7 +269,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #number version -ARTY.version="0.6.0" +ARTY.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -340,8 +335,8 @@ function ARTY:New(group) self:T3({id=id, desc=desc}) end - -- Set speed to maximum in km/h. - self.Speed=self.DCSdesc.speedMax*3.6 + -- Set speed to 1/2 of maximum in km/h. + self.Speed=self.DCSdesc.speedMax*3.6 * 0.5 -- Displayed name (similar to type name below) self.DisplayName=self.DCSdesc.displayName @@ -365,6 +360,7 @@ function ARTY:New(group) self:AddTransition("Rearming", "Rearmed", "CombatReady") self:AddTransition("CombatReady", "Move", "Moving") self:AddTransition("Moving", "Arrived", "CombatReady") + self:AddTransition("*", "NewTarget", "*") self:AddTransition("*", "Dead", "*") --- User function for OnBefore "OpenFire" event. @@ -582,6 +578,9 @@ function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, w -- Debug info. self:T(ARTY.id..string.format("Added target %s, prio=%d, radius=%d, nshells=%d, maxengage=%d, time=%s, weapontype=%d", name, prio, radius, nshells, maxengage, tostring(_clock), weapontype)) + + -- Trigger new target event. + self:NewTarget(_target) end @@ -649,14 +648,6 @@ function ARTY:SetDebugOFF() self.Debug=false end ---- Set target queue update time interval. --- @param #ARTY self --- @param #number interval Time interval in seconds. Default is 5 seconds. -function ARTY:SetTargetQueueUpdateInterval(interval) - self:F2({interval=interval}) - self.TargetQueueUpdate=interval or 5 -end - --- Delete target from target list. -- @param #ARTY self -- @param #string name Name of the target. @@ -715,6 +706,17 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Before "Start" event. Initialized ROE and alarm state. Starts the event handler. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onbeforeStart(Controllable, From, Event, To) + self:_EventFromTo("onbeforeStart", Event, From, To) + + env.info("FF: onbeforeStart") +end --- After "Start" event. Initialized ROE and alarm state. Starts the event handler. -- @param #ARTY self @@ -779,9 +781,6 @@ function ARTY:onafterStart(Controllable, From, Event, To) self:HandleEvent(EVENTS.Shot, self._OnEventShot) self:HandleEvent(EVENTS.Dead, self._OnEventDead) - -- Start scheduler to monitor task queue. - self.SchedIDTargetQueue=self.scheduler:Schedule(self, ARTY._TargetQueue, {self}, 5, self.TargetQueueUpdate) - -- Start scheduler to monitor if ARTY group started firing within a certain time. self.SchedIDCheckShooting=self.scheduler:Schedule(self, ARTY._CheckShootingStarted, {self}, 60, 60) @@ -950,6 +949,23 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- After "NewTarget" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target info. +-- @return #boolean If true, proceed to onafterOpenfire. +function ARTY:onafterNewTarget(Controllable, From, Event, To, target) + self:_EventFromTo("onafterNewTarget", Event, From, To) + + -- Debug message. + local text=string.format("Adding new target %s.", target.name) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T(ARTY.id..text) +end + --- Before "OpenFire" event. Checks if group already has a target. Checks for valid min/max range and removes the target if necessary. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -973,6 +989,7 @@ function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) return true end + -- Check that group has no current target already. if self.currentTarget then -- Debug info. @@ -1095,6 +1112,36 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) end +--- Enter "CombatReady" state. Route the group back if necessary. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onenterCombatReady(Controllable, From, Event, To) + + env.info(string.format("FF: onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) + + if From=="Rearming" and Event=="Rearmed" then + env.info("FF: Comabatready after Rearmed") + + -- Distance to initial position. + local dist=Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) + + if dist>100 then + -- Route group back to its original position, when rearming was at another place. + self:T(ARTY.id..string.format("%s is routed back to its initial position. Distance = %d m.", Controllable:GetName(), dist)) + self:__Move(30, self.InitialCoord, true) + end + + else + -- Update target queue and open fire. + env.info("FF: Comabatready ==> _openfireontarget.") + self:_openfireontarget() + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Before "Winchester" event. Cease fire on current target. -- @param #ARTY self @@ -1105,8 +1152,6 @@ end -- @return #boolean If true, proceed to onafterWinchester. function ARTY:onbeforeWinchester(Controllable, From, Event, To) - - return true end @@ -1141,6 +1186,7 @@ end function ARTY:onbeforeRearm(Controllable, From, Event, To) self:_EventFromTo("onbeforeRearm", Event, From, To) + -- Check if a reaming unit or rearming place was specified. if self.RearmingUnit and self.RearmingUnit:IsAlive() then return true elseif self.RearmingPlaceCoord then @@ -1162,7 +1208,9 @@ function ARTY:onafterRearm(Controllable, From, Event, To) -- Coordinate of ARTY unit. local coordARTY=self.Controllable:GetCoordinate() - local coordRARM + + -- Coordinate of rearming unit. + local coordRARM=nil if self.RearmingUnit then -- Coordinate of the rearming unit. coordRARM=self.RearmingUnit:GetCoordinate() @@ -1172,22 +1220,28 @@ function ARTY:onafterRearm(Controllable, From, Event, To) if self.RearmingUnit and self.RearmingPlaceCoord and self.Speed>0 then - -- Rearming unit and ARTY group meet at rearming place. + -- CASE 1: Rearming unit and ARTY group meet at rearming place. + + -- Distances. local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord) -- Route ARTY group to rearming place. if dA>100 then - self.Controllable:RouteGroundOnRoad(self.RearmingPlaceCoord, self.Speed, 1) + --self.Controllable:RouteGroundOnRoad(self.RearmingPlaceCoord, self.Speed, 1) + self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) end -- Route Rearming unit to rearming place if dR>100 then self.RearmingUnit:RouteGroundOnRoad(self.RearmingPlaceCoord, 50, 1) + --self:_Move(self.RearmingUnit, self.RearmingPlaceCoord, 50, true) end elseif self.RearmingUnit then + -- CASE 2: Rearming unit drives to ARTY group. + -- Send message. local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingUnit:GetName()) self:T(ARTY.id..text) @@ -1199,12 +1253,26 @@ function ARTY:onafterRearm(Controllable, From, Event, To) -- If distance is larger than 100 m, the Rearming unit is routed to the ARTY group. if distance > 100 then -- Random point 20-100 m away from unit. - local vec2=coord:GetRandomVec2InRadius(20, 100) + local vec2=coordARTY:GetRandomVec2InRadius(20, 100) local pops=COORDINATE:NewFromVec2(vec2) -- Route unit to ARTY group. self.RearmingUnit:RouteGroundOnRoad(pops, 50, 1) end + + elseif self.RearmingPlaceCoord then + + -- CASE 3: ARTY drives to rearming place. + + -- Distance. + local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) + + -- Route ARTY group to rearming place. + if dA>100 then + --self.Controllable:RouteGroundOnRoad(self.RearmingPlaceCoord, self.Speed, 1) + self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) + end + end -- Start scheduler to monitor ammo count until rearming is complete. @@ -1325,14 +1393,30 @@ function ARTY:onafterMove(Controllable, From, Event, To, ToCoord, OnRoad) self.Controllable:OptionROEHoldFire() -- Route group to coodinate. - if OnRoad then - self.Controllable:RouteGroundOnRoad(ToCoord, self.Speed, 1) - else - self.Controllable:RouteGroundTo(ToCoord, self.Speed, "Vee", 1) - end + self:_Move(self.Controllable, ToCoord, self.Speed, OnRoad) end +--- After "Arrived" event. Group has reached its destination. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterArrived(Controllable, From, Event, To) + self:_EventFromTo("onafterArrived", Event, From, To) + + -- Set alarm state to auto. + self.Controllable:OptionAlarmStateAuto() + + -- Send message + local text=string.format("%s, arrived at destination.", Controllable:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + +end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- After "Dead" event, when a unit has died. When all units of a group are dead trigger "Stop" event. @@ -1398,9 +1482,6 @@ function ARTY:onafterStop(Controllable, From, Event, To) -- Remove all targets. --self:RemoveAllTargets() -- Stop schedulers. - if self.SchedIDTargetQueue then - self.scheduler:Stop(self.SchedIDTargetQueue) - end if self.SchedIDCheckShooting then self.scheduler:Stop(self.SchedIDCheckShooting) end @@ -1443,9 +1524,9 @@ function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) end ---- Go through queue of assigned tasks. +--- Go through queue of assigned tasks and trigger OpenFire event. -- @param #ARTY self -function ARTY:_TargetQueue() +function ARTY:_openfireontarget() self:F2() -- Debug info @@ -1453,7 +1534,7 @@ function ARTY:_TargetQueue() -- No targets assigned at the moment. if #self.targets==0 then - self:T3(ARTY.id..string.format("Group %s, no targets assigned at the moment. No need for _TargetQueue.", self.Controllable:GetName())) + self:T3(ARTY.id..string.format("Group %s, no targets assigned at the moment. No need for _OpenFire.", self.Controllable:GetName())) return end @@ -1471,7 +1552,7 @@ function ARTY:_TargetQueue() self:T(ARTY.id..string.format("Engaging timed target %s. Prio=%d, engaged=%d, time=%s, tnow=%s",_target.name,_target.prio,_target.engaged,_clock,_Cnow)) -- Call OpenFire event. - self:OpenFire(_target) + self:__OpenFire(1, _target) end end @@ -1491,9 +1572,8 @@ function ARTY:_TargetQueue() self:T(ARTY.id..string.format("Engaging target %s. Prio = %d, engaged = %d", _target.name, _target.prio, _target.engaged)) -- Call OpenFire event. - self:OpenFire(_target) - - break + self:__OpenFire(1, _target) + end end @@ -1548,8 +1628,11 @@ end --- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. -- @param #ARTY self --- @param Wrapper.Controllable#CONTROLLABLE controllable --- @return Number of ALL shells left from the whole group. +-- @param Wrapper.Controllable#CONTROLLABLE controllable Controllable for which the ammo is counted. +-- @return #number Total amount of ammo the whole group has left. +-- @return #number Number of shells the group has left. +-- @return #number Number of rockets the group has left. +-- @return #number Number of missiles the group has left. function ARTY:_GetAmmo(controllable) self:F2(controllable) @@ -1570,7 +1653,7 @@ function ARTY:_GetAmmo(controllable) if unit and unit:IsAlive() then local ammotable=unit:GetAmmo() - self:T({ammotable=ammotable}) + self:T2({ammotable=ammotable}) local name=unit:GetName() @@ -1768,6 +1851,7 @@ end -- @param #number tnumber Number of weapon type ARTY.WeaponType.XXX -- @return #number tnumber of weapon type. function ARTY:_WeaponTypeName(tnumber) + self:F2(tnumber) local name="unknown" if tnumber==ARTY.WeaponType.Auto then name="Auto (Cannon, Rockets, Missiles)" @@ -1823,7 +1907,6 @@ function ARTY:_SecondsToClock(seconds) if seconds==nil then return nil - --return "00:00:00" end -- Seconds @@ -1833,14 +1916,13 @@ function ARTY:_SecondsToClock(seconds) local _seconds=seconds%(60*60*24) if seconds <= 0 then - return "00:00:00" + return nil else local hours = string.format("%02.f", math.floor(_seconds/3600)) local mins = string.format("%02.f", math.floor(_seconds/60 - (hours*60))) local secs = string.format("%02.f", math.floor(_seconds - hours*3600 - mins *60)) - local days = string.format("%d", seconds/(60*60*24)) + local days = string.format("%d", seconds/(60*60*24)) return hours..":"..mins..":"..secs.."+"..days - --return hours, mins, secs end end @@ -1908,7 +1990,6 @@ function ARTY:_Move(group, ToCoord, Speed, OnRoad) -- Current coordinates of group. local cpini=group:GetCoordinate() - cpini:SmokeWhite() -- Distance between current and final point. local dist=cpini:Get2DDistance(ToCoord) @@ -1916,24 +1997,46 @@ function ARTY:_Move(group, ToCoord, Speed, OnRoad) -- Waypoint and task arrays. local path={} local task={} - + -- First waypoint is the current position of the group. path[#path+1]=cpini:WaypointGround(Speed, formation) - task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, 0, false) + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) + + -- Route group on road if requested. + if OnRoad then + + --path[#path+1]=cpini:GetClosestPointToRoad():WaypointGround(Speed, "On road") + --task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) + local _first=cpini:GetClosestPointToRoad() + local _last=ToCoord:GetClosestPointToRoad() + local _onroad=_first:GetPathOnRoad(_last) + + -- Points on road. + for i=1,#_onroad do + path[#path+1]=_onroad[i]:WaypointGround(Speed, "On road") + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) + end + + --path[#path+1]=ToCoord:GetClosestPointToRoad():WaypointGround(Speed, "On road") + --task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) + end + + -- Last waypoint at ToCoord. path[#path+1]=ToCoord:WaypointGround(Speed, formation) - task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, 1, true) + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, true) + -- Init waypoints of the group. local Waypoints={} -- New points are added to the default route. - for i,p in ipairs(path) do + for i=1,#path do table.insert(Waypoints, i, path[i]) end -- Set task for all waypoints. - for i,wp in ipairs(Waypoints) do + for i=1,#Waypoints do group:SetTaskWaypoint(Waypoints[i], task[i]) end @@ -1952,15 +2055,15 @@ function ARTY._PassingWaypoint(group, arty, i, final) -- Debug message. local text=string.format("Group %s passing waypoint %d (final=%s)", group:GetName(), i, tostring(final)) - local pos=group:GetCoordinate() - local MarkerID=pos:MarkToAll(string.format("Reached Waypoint %d of group %s", i, group:GetName())) - pos:SmokeRed() + --local pos=group:GetCoordinate() + --local MarkerID=pos:MarkToAll(string.format("Reached Waypoint %d of group %s", i, group:GetName())) + --pos:SmokeRed() MESSAGE:New(text,10):ToAll() env.info(ARTY.id..text) -- Move --> Moving --> Arrived --> CombatReady. - if final then + if final and arty.Controllable:GetName()==group:GetName() then arty:Arrived() end From 0cf4b8845e83d7ebc16ff8b68bda01bd447fb561 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Sat, 5 May 2018 08:44:16 +0200 Subject: [PATCH 16/31] ARTY v0.8.0 WIP version. Not functional. --- .../Moose/Functional/Artillery.lua | 698 +++++++++++------- 1 file changed, 411 insertions(+), 287 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 41e6e1f29..aaf1a77b9 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -60,10 +60,10 @@ -- @field #number IniGroupStrength Inital number of units in the ARTY group. -- @field #boolean IsArtillery If true, ARTY group has attribute "Artillery". -- @field #number Speed Max speed of ARTY group. --- @field Wrapper.Unit#UNIT RearmingUnit Unit designated to rearm the ARTY group. --- @field Wrapper.Point#COORDINATE RearmingUnitCoord Initial coordinates of the rearming unit. After rearming complete, the unit will return to this position. --- @field Wrapper.Point#COORDINATE RearmingPlaceCoord Coordinates of the rearming place. If the place is more than 100 m away from the ARTY group, the group will go there. --- @field Wrapper.Point#COORDINATE InitialCoord Initial coordinates of the ARTY group. +-- @field Wrapper.Group#GROUP RearmingGroup Unit designated to rearm the ARTY group. +-- @field Core.Point#COORDINATE RearmingGroupCoord Initial coordinates of the rearming unit. After rearming complete, the unit will return to this position. +-- @field Core.Point#COORDINATE RearmingPlaceCoord Coordinates of the rearming place. If the place is more than 100 m away from the ARTY group, the group will go there. +-- @field Core.Point#COORDINATE InitialCoord Initial coordinates of the ARTY group. -- @field #boolean report Arty group sends messages about their current state or target to its coaliton. -- @field #table ammoshells Table holding names of the shell types which are included when counting the ammo. Default is {"weapons.shells"} which include most shells. -- @field #table ammorockets Table holding names of the rocket types which are included when counting the ammo. Default is {"weapons.nurs"} which includes most unguided rockets. @@ -96,10 +96,10 @@ -- When the ARTY group runs out of ammunition, the event **Winchester** is triggered and the group enters the state **OutOfAmmo**. -- In this state, the group is unable to engage further targets. -- --- With the @{#ARTY.SetRearmingUnit}(*unit*) command, a special unit can be defined to rearm the ARTY group. If this unit has been assigned and the group has entered the state +-- With the @{#ARTY.SetRearmingGroup}(*group*) command, a special group can be defined to rearm the ARTY group. If this unit has been assigned and the group has entered the state -- **OutOfAmmo** the event **Rearm** is triggered followed by a transition to the state **Rearming**. --- If the rearming unit is less than 100 meters away from the ARTY group, the rearming process starts. If the rearming unit is more than 100 meters away from the ARTY unit, the --- rearming unit is routed to a point 20 to 100 m from the ARTY group. +-- If the rearming group is less than 100 meters away from the ARTY group, the rearming process starts. If the rearming group is more than 100 meters away from the ARTY unit, the +-- rearming group is routed to a point 20 to 100 m from the ARTY group. -- -- Once the rearming is complete, the **Rearmed** event is triggered and the group enters the state **CombatReady**. At this point targeted can be engaged again. -- @@ -200,7 +200,7 @@ -- * @{#ARTY.RemoveTarget}(*name*) deletes the target with *name* from the target queue. -- * @{#ARTY.SetMaxFiringRange}(*range*) defines the maximum firing range. Targets further away than this distance are not engaged. -- * @{#ARTY.SetMinFiringRange}(*range*) defines the minimum firing range. Targets closer than this distance are not engaged. --- * @{#ARTY.SetRearmingUnit}(*unit*) sets the unit resposible for rearming of the ARTY group once it is out of ammo. +-- * @{#ARTY.SetRearmingGroup}(*group*) sets the group resposible for rearming of the ARTY group once it is out of ammo. -- * @{#ARTY.SetReportON}() and @{#ARTY.SetReportOFF}() can be used to enable/disable status reports of the ARTY group send to all coalition members. -- * @{#ARTY.SetWaitForShotTime}(*waittime*) sets the time after which a target is deleted from the queue if no shooting event occured after the target engagement started. -- Default is 300 seconds. Note that this can for example happen, when the assigned target is out of range. @@ -239,8 +239,8 @@ ARTY={ DisplayName=nil, IniGroupStrength=0, IsArtillery=nil, - RearmingUnit=nil, - RearmingUnitCoord=nil, + RearmingGroup=nil, + RearmingGroupCoord=nil, RearmingPlaceCoord=nil, InitialCoord=nil, report=true, @@ -269,7 +269,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #number version -ARTY.version="0.7.0" +ARTY.version="0.8.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -287,6 +287,10 @@ ARTY.version="0.7.0" -- DONE: Improve assigned time for engagement. Next day? -- DONE: Improve documentation. -- DONE: Add pseudo user transitions. OnAfter... +-- TODO: Make reaming unit a group. +-- TODO: Adjust documenation again. +-- TODO: Add command move to make arty group move. +-- TODO: remove schedulers for status event. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -350,18 +354,35 @@ function ARTY:New(group) -- Initial group strength. self.IniGroupStrength=#group:GetUnits() - -- Transitions - self:AddTransition("*", "Start", "CombatReady") - self:AddTransition("CombatReady", "OpenFire", "Firing") - self:AddTransition("CombatReady", "Winchester", "OutOfAmmo") - self:AddTransition("Firing", "OpenFire", "Firing") - self:AddTransition("Firing", "CeaseFire", "CombatReady") - self:AddTransition("OutOfAmmo", "Rearm", "Rearming") - self:AddTransition("Rearming", "Rearmed", "CombatReady") - self:AddTransition("CombatReady", "Move", "Moving") - self:AddTransition("Moving", "Arrived", "CombatReady") - self:AddTransition("*", "NewTarget", "*") - self:AddTransition("*", "Dead", "*") + -- Transitions: + -- Entry + self:AddTransition("*", "Start", "CombatReady") + + -- Blue branch. + self:AddTransition("CombatReady", "OpenFire", "Firing") + self:AddTransition("Firing", "OpenFire", "Firing") + self:AddTransition("Firing", "CeaseFire", "CombatReady") + --self:AddTransition("CombatReady", "CeaseFire", "CombatReady") -- not in diagram yet. + + -- Violett branch. + self:AddTransition("Firing", "Winchester", "OutOfAmmo") + + -- Red branch. + self:AddTransition("OutOfAmmo", "Rearm", "Rearming") + self:AddTransition("Rearming", "Rearmed", "Rearmed") + + -- Green branch. + self:AddTransition("*", "Move", "Moving") + self:AddTransition("Moving", "Arrived", "Arrived") + + self:AddTransition("*", "CombatReady", "CombatReady") + + -- Yellow branch. + self:AddTransition("*", "NewTarget", "*") + -- Not in diagram. + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Dead", "*") + --- User function for OnBefore "OpenFire" event. -- @function [parent=#ARTY] OnBeforeOpenFire @@ -608,12 +629,12 @@ function ARTY:SetWaitForShotTime(waittime) self.WaitForShotTime=waittime or 300 end ---- Assign a unit which is responsible for rearming the ARTY group. If the unit is too far away from the ARTY group it will be guided towards the ARTY group. +--- Assign a group, which is responsible for rearming the ARTY group. If the group is too far away from the ARTY group it will be guided towards the ARTY group. -- @param #ARTY self --- @param Wrapper.Unit#UNIT unit Unit that is supposed to rearm the ARTY group. -function ARTY:SetRearmingUnit(unit) - self:F({unit=unit}) - self.RearmingUnit=unit +-- @param Wrapper.Group#GROUP unit Unit that is supposed to rearm the ARTY group. +function ARTY:SetRearmingGroup(group) + self:F({group=group}) + self.RearmingGroup=group end --- Defines the rearming place of the ARTY group. If the place is too far away from the ARTY group it will be routed to the place. @@ -747,8 +768,8 @@ function ARTY:onafterStart(Controllable, From, Event, To) text=text..string.format("Number of shells = %d\n", self.Nshells0) text=text..string.format("Number of rockets = %d\n", self.Nrockets0) text=text..string.format("Number of missiles = %d\n", self.Nmissiles0) - if self.RearmingUnit then - text=text..string.format("Reaming unit = %s\n", self.RearmingUnit:GetName()) + if self.RearmingGroup then + text=text..string.format("Reaming group = %s\n", self.RearmingGroup:GetName()) end if self.RearmingPlaceCoord then local dist=self.InitialCoord:Get2DDistance(self.RearmingPlaceCoord) @@ -780,14 +801,16 @@ function ARTY:onafterStart(Controllable, From, Event, To) -- Add event handler. self:HandleEvent(EVENTS.Shot, self._OnEventShot) self:HandleEvent(EVENTS.Dead, self._OnEventDead) + + self:__Status(5) -- Start scheduler to monitor if ARTY group started firing within a certain time. self.SchedIDCheckShooting=self.scheduler:Schedule(self, ARTY._CheckShootingStarted, {self}, 60, 60) -- Start cheduler for status reports. - if self.Debug then - self.SchedIDStatusReport=self.scheduler:Schedule(self, ARTY._StatusReport, {self}, 30, 30) - end +-- if self.Debug then +-- self.SchedIDStatusReport=self.scheduler:Schedule(self, ARTY._StatusReport, {self}, 30, 30) +-- end end @@ -868,8 +891,6 @@ function ARTY:_OnEventShot(EventData) if _nammo==0 then self:T(ARTY.id..string.format("Group %s completely out of ammo.", self.Controllable:GetName())) - -- Cease fire first. - self:CeaseFire(self.currentTarget) self:Winchester() -- Current target is deallocated ==> return @@ -966,6 +987,242 @@ function ARTY:onafterNewTarget(Controllable, From, Event, To, target) self:T(ARTY.id..text) end +--- After "Status" event. Report status of group. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterStatus(Controllable, From, Event, To) + self:_EventFromTo("onafterStatus", Event, From, To) + + if self.Debug then + self:_StatusReport() + end + + local _engage=true + + if self:is("OutOfAmmo") then + + -- Coordinate of ARTY unit. + local coordARTY=self.Controllable:GetCoordinate() + + -- Coordinate of rearming group. + local coordRARM=nil + if self.RearmingGroup then + -- Coordinate of the rearming unit. + coordRARM=self.RearmingGroup:GetCoordinate() + -- Remember the coordinates of the rearming unit. After rearming it will go back to this position. + self.RearmingGroupCoord=coordRARM + end + + if self.RearmingGroup and self.RearmingPlaceCoord and self.Speed>0 then + + -- CASE 1: Rearming unit and ARTY group meet at rearming place. + + -- Distances. + local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) + local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord) + + -- Route ARTY group to rearming place. + if dA>100 then + --self.Controllable:RouteGroundOnRoad(self.RearmingPlaceCoord, self.Speed, 1) + --self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) + self:Move(self.RearmingPlaceCoord, false) + end + + -- Route Rearming unit to rearming place + if dR>100 then + self.RearmingGroup:RouteGroundOnRoad(self.RearmingPlaceCoord, 50, 1) + --self:_Move(self.RearmingGroup, self.RearmingPlaceCoord, 50, true) + end + + elseif self.RearmingGroup then + + -- CASE 2: Rearming unit drives to ARTY group. + + -- Send message. + local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingGroup:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + + -- Distance between ARTY group and rearming unit. + local distance=coordARTY:Get2DDistance(coordRARM) + + -- If distance is larger than 100 m, the Rearming unit is routed to the ARTY group. + if distance > 100 then + -- Random point 20-100 m away from unit. + local vec2=coordARTY:GetRandomVec2InRadius(20, 100) + local pops=COORDINATE:NewFromVec2(vec2) + + -- Route unit to ARTY group. + self.RearmingGroup:RouteGroundOnRoad(pops, 50, 1) + end + + elseif self.RearmingPlaceCoord then + + -- CASE 3: ARTY drives to rearming place. + + -- Distance. + local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) + + -- Route ARTY group to rearming place. + if dA>100 then + --self.Controllable:RouteGroundOnRoad(self.RearmingPlaceCoord, self.Speed, 1) + --self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) + self:Move(self.RearmingPlaceCoord, false) + end + + end + + _engage=false + + end + + if self:is("Moving") then + _engage=false + end + + if self:is("Rearming") then + _engage=false + end + + if self:is("Rearmed") then + local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) + if distance > 100 then + self:Move(self.InitialCoord, false) + _engage=false + else + self:CombatReady() + end + end + + + if self:is("Arrived") then + + end + + -- Engage targets. + if _engage then + + -- Get a timed target if it is due to be attacked. + local _timedTarget=self:_CheckTimedTargets() + local _normalTarget=self:_CheckNormalTargets() + + -- Engage target. + if _timedTarget then + if self.currentTarget then + self:CeaseFire() + end + self:OpenFire(_timedTarget) + elseif _normalTarget then + self:OpenFire(_normalTarget) + end + + end + + -- Call status again in 5 sec. + self:__Status(5) +end + +--- Check all timed targets and return the target which should be attacked next. +-- @param #ARTY self +-- @return #table Target which is due to be attacked now. +function ARTY:_CheckTimedTargets() + + -- Current time. + local Tnow=timer.getAbsTime() + + -- Sort Targets wrt time. + self:_SortTargetQueueTime() + + for i=1,#self.targets do + local _target=self.targets[i] + + -- Check if target has an attack time which has already passed. + -- Also check that target is not under fire already and that it is in range. + if _target.time and _target.time>=Tnow and _target.underfire==false and self:_TargetInRange(_target) then + + -- Check if group currently has a target and whether its priorty is lower than the timed target. + if self.currentTarget then + if self.currentTarget.prio > _target.prio then + -- Current target under attack but has lower priority than this target. + self:T(ARTY.id..string.format("Group %s current target %s has lower prio than new target %s with attack time.", self.Controllable:GetName(), self.currentTarget.name, target.name)) + return _target + end + else + -- No current target. + return _target + end + end + end + + return nil +end + +--- Check all normal (untimed) targets and return the target with the highest priority which has been engaged the fewest times. +-- @param #ARTY self +-- @return #table Target which is due to be attacked now or nil if no target could be found. +function ARTY:_CheckNormalTargets() + + -- Sort targets w.r.t. prio and number times engaged already. + self:_SortTargetQueuePrio() + + -- Loop over all sorted targets. + for i=1,#self.targets do + local _target=self.targets[i] + + -- Check that target no time, is not under fire currently and in range. + if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged and self:_TargetInRange(_target) then + + -- Debug info. + self:T(ARTY.id..string.format("Engaging target %s. Prio = %d, engaged = %d", _target.name, _target.prio, _target.engaged)) + + return _target + end + end + + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Enter "CombatReady" state. Route the group back if necessary. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onenterCombatReady(Controllable, From, Event, To) + + env.info(string.format("FF: onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) + +--[[ + if From=="Rearming" and Event=="Rearmed" then + env.info("FF: Comabatready after Rearmed") + + -- Distance to initial position. + local dist=Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) + + if dist>100 then + -- Route group back to its original position, when rearming was at another place. + self:T(ARTY.id..string.format("%s is routed back to its initial position. Distance = %d m.", Controllable:GetName(), dist)) + self:__Move(30, self.InitialCoord, true) + end + + else + + -- Update target queue and open fire. + env.info("FF: Comabatready ==> _openfireontarget.") + self:_openfireontarget() + + end +]] + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Before "OpenFire" event. Checks if group already has a target. Checks for valid min/max range and removes the target if necessary. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -976,20 +1233,7 @@ end -- @return #boolean If true, proceed to onafterOpenfire. function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onbeforeOpenFire", Event, From, To) - - -- If this target has an attack time and it's prio is higher than the current task, we allow the transition. - if target.time~=nil and self.currentTarget~=nil and self.currentTarget.prio > target.prio then - -- Debug info. - self:T(ARTY.id..string.format("Group %s current target %s has lower prio than new target %s with attack time.", self.Controllable:GetName(), self.currentTarget.name, target.name)) - - -- Stop firing on current target. - self:CeaseFire(self.currentTarget) - - -- Alow transition to onafterOpenfire. - return true - end - - + -- Check that group has no current target already. if self.currentTarget then -- Debug info. @@ -999,25 +1243,9 @@ function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) return false end --- Distance to target - local range=Controllable:GetCoordinate():Get2DDistance(target.coord) - - -- Check that distance to target is within range. - if rangeself.maxrange then - - -- Debug output. - local text - if rangeself.maxrange then - text=string.format("%s, target is out of range. Distance of %d km is greater than max range of %d km.", Controllable:GetName(), range/1000, self.maxrange/1000) - end - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - - -- Remove target. - self:RemoveTarget(target.name) - + -- Check if target is in range. + local _inrange=self:_TargetInRange(target) + if not _inrange then -- Deny transition. return false end @@ -1064,6 +1292,36 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) end +--- Go through queue of assigned tasks and trigger OpenFire event. +-- @param #ARTY self +function ARTY:_openfireontarget() + self:F2() + + -- Debug info + self:T2(ARTY.id..string.format("Group %s, number of targets = %d", self.Controllable:GetName(), #self.targets)) + + -- No targets assigned at the moment. + if #self.targets==0 then + self:T3(ARTY.id..string.format("Group %s, no targets assigned at the moment. No need for _OpenFire.", self.Controllable:GetName())) + return + end + + -- Check timed targets first. + local _target=self:_CheckTimedTargets() + if _target then + self:__OpenFire(1, _target) + return + end + + -- Check normal targets + local _target=self:_CheckNormalTargets() + if _target then + self:__OpenFire(1, _target) + return + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- After "CeaseFire" event. Clears task of the group and removes the target if max engagement was reached. @@ -1075,85 +1333,48 @@ end -- @param #table target Array holding the target info. function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) self:_EventFromTo("onafterCeaseFire", Event, From, To) - - -- Send message. - local text=string.format("%s, ceasing fire on target %s.", Controllable:GetName(), target.name) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) - - -- Get target array index. - local id=self:_GetTargetByName(target.name) - -- Increase engaged counter - if id then - -- Target was actually engaged. (Could happen that engagement was aborted while group was still aiming.) - if self.Nshots>0 then - self.targets[id].engaged=self.targets[id].engaged+1 - -- Clear the attack time. - self.targets[id].time=nil + if target then + + -- Send message. + local text=string.format("%s, ceasing fire on target %s.", Controllable:GetName(), target.name) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) + + -- Get target array index. + local id=self:_GetTargetByName(target.name) + + -- Increase engaged counter + if id then + -- Target was actually engaged. (Could happen that engagement was aborted while group was still aiming.) + if self.Nshots>0 then + self.targets[id].engaged=self.targets[id].engaged+1 + -- Clear the attack time. + self.targets[id].time=nil + end + -- Target is not under fire any more. + self.targets[id].underfire=false end - -- Target is not under fire any more. - self.targets[id].underfire=false - end - - -- Clear tasks. - self.Controllable:ClearTasks() + -- If number of engagements has been reached, the target is removed. + if target.engaged >= target.maxengage then + self:RemoveTarget(target.name) + end + + -- Clear tasks. + self.Controllable:ClearTasks() + + end + -- Set number of shots to zero. self.Nshots=0 - - -- If number of engagements has been reached, the target is removed. - if target.engaged >= target.maxengage then - self:RemoveTarget(target.name) - end -- ARTY group has no current target any more. self.currentTarget=nil end ---- Enter "CombatReady" state. Route the group back if necessary. --- @param #ARTY self --- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function ARTY:onenterCombatReady(Controllable, From, Event, To) - - env.info(string.format("FF: onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) - - if From=="Rearming" and Event=="Rearmed" then - env.info("FF: Comabatready after Rearmed") - - -- Distance to initial position. - local dist=Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) - - if dist>100 then - -- Route group back to its original position, when rearming was at another place. - self:T(ARTY.id..string.format("%s is routed back to its initial position. Distance = %d m.", Controllable:GetName(), dist)) - self:__Move(30, self.InitialCoord, true) - end - - else - -- Update target queue and open fire. - env.info("FF: Comabatready ==> _openfireontarget.") - self:_openfireontarget() - end - -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Before "Winchester" event. Cease fire on current target. --- @param #ARTY self --- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @return #boolean If true, proceed to onafterWinchester. -function ARTY:onbeforeWinchester(Controllable, From, Event, To) - - return true -end --- After "Winchester" event. Group is out of ammo. Trigger "Rearm" event. -- @param #ARTY self @@ -1170,7 +1391,7 @@ function ARTY:onafterWinchester(Controllable, From, Event, To) MESSAGE:New(text, 30):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) -- Init rearming if possible. - self:Rearm() + --self:Rearm() end @@ -1187,7 +1408,7 @@ function ARTY:onbeforeRearm(Controllable, From, Event, To) self:_EventFromTo("onbeforeRearm", Event, From, To) -- Check if a reaming unit or rearming place was specified. - if self.RearmingUnit and self.RearmingUnit:IsAlive() then + if self.RearmingGroup and self.RearmingGroup:IsAlive() then return true elseif self.RearmingPlaceCoord then return true @@ -1206,75 +1427,6 @@ end function ARTY:onafterRearm(Controllable, From, Event, To) self:_EventFromTo("onafterRearm", Event, From, To) - -- Coordinate of ARTY unit. - local coordARTY=self.Controllable:GetCoordinate() - - -- Coordinate of rearming unit. - local coordRARM=nil - if self.RearmingUnit then - -- Coordinate of the rearming unit. - coordRARM=self.RearmingUnit:GetCoordinate() - -- Remember the coordinates of the rearming unit. After rearming it will go back to this position. - self.RearmingUnitCoord=coordRARM - end - - if self.RearmingUnit and self.RearmingPlaceCoord and self.Speed>0 then - - -- CASE 1: Rearming unit and ARTY group meet at rearming place. - - -- Distances. - local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) - local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord) - - -- Route ARTY group to rearming place. - if dA>100 then - --self.Controllable:RouteGroundOnRoad(self.RearmingPlaceCoord, self.Speed, 1) - self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) - end - - -- Route Rearming unit to rearming place - if dR>100 then - self.RearmingUnit:RouteGroundOnRoad(self.RearmingPlaceCoord, 50, 1) - --self:_Move(self.RearmingUnit, self.RearmingPlaceCoord, 50, true) - end - - elseif self.RearmingUnit then - - -- CASE 2: Rearming unit drives to ARTY group. - - -- Send message. - local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingUnit:GetName()) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - - -- Distance between ARTY group and rearming unit. - local distance=coordARTY:Get2DDistance(coordRARM) - - -- If distance is larger than 100 m, the Rearming unit is routed to the ARTY group. - if distance > 100 then - -- Random point 20-100 m away from unit. - local vec2=coordARTY:GetRandomVec2InRadius(20, 100) - local pops=COORDINATE:NewFromVec2(vec2) - - -- Route unit to ARTY group. - self.RearmingUnit:RouteGroundOnRoad(pops, 50, 1) - end - - elseif self.RearmingPlaceCoord then - - -- CASE 3: ARTY drives to rearming place. - - -- Distance. - local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) - - -- Route ARTY group to rearming place. - if dA>100 then - --self.Controllable:RouteGroundOnRoad(self.RearmingPlaceCoord, self.Speed, 1) - self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) - end - - end - -- Start scheduler to monitor ammo count until rearming is complete. self.SchedIDCheckRearmed=self.scheduler:Schedule(self, ARTY._CheckRearmed, {self}, 20, 20) end @@ -1307,10 +1459,10 @@ function ARTY:onafterRearmed(Controllable, From, Event, To) end -- Route unit back to where it came from (if distance is > 100 m). - if self.RearmingUnit and self.RearmingUnit:IsAlive() then - local d=self.RearmingUnit:GetCoordinate():Get2DDistance(self.RearmingUnitCoord) + if self.RearmingGroup and self.RearmingGroup:IsAlive() then + local d=self.RearmingGroup:GetCoordinate():Get2DDistance(self.RearmingGroupCoord) if d>100 then - self.RearmingUnit:RouteGroundOnRoad(self.RearmingUnitCoord, 50, 1) + self.RearmingGroup:RouteGroundOnRoad(self.RearmingGroupCoord, 50, 1) end end @@ -1349,7 +1501,6 @@ function ARTY:_CheckRearmed() end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Before "Move" event. Check if a unit to rearm the ARTY group has been defined. @@ -1416,7 +1567,6 @@ function ARTY:onafterArrived(Controllable, From, Event, To) end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- After "Dead" event, when a unit has died. When all units of a group are dead trigger "Stop" event. @@ -1450,24 +1600,6 @@ function ARTY:onafterDead(Controllable, From, Event, To) end ---- Before "Stop" event. Cease fire on current target. --- @param #ARTY self --- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @return #boolean If true, proceed to onafterStop. -function ARTY:onbeforeStop(Controllable, From, Event, To) - self:_EventFromTo("onbeforeStop", Event, From, To) - - -- Cease Fire on current target. - if self.currentTarget then - self:CeaseFire(self.currentTarget) - end - - return true -end - --- After "Stop" event. Stop schedulers and unhandle events. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -1479,8 +1611,15 @@ function ARTY:onafterStop(Controllable, From, Event, To) -- Debug info. self:T(ARTY.id..string.format("Stopping ARTY FSM for group %s.", Controllable:GetName())) + + -- Cease Fire on current target. + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + -- Remove all targets. --self:RemoveAllTargets() + -- Stop schedulers. if self.SchedIDCheckShooting then self.scheduler:Stop(self.SchedIDCheckShooting) @@ -1488,6 +1627,7 @@ function ARTY:onafterStop(Controllable, From, Event, To) if self.SchedIDCheckRearmed then self.scheduler:Stop(self.SchedIDCheckRearmed) end + -- Unhandle event. self:UnHandleEvent(EVENTS.Shot) self:UnHandleEvent(EVENTS.Dead) @@ -1524,61 +1664,6 @@ function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) end ---- Go through queue of assigned tasks and trigger OpenFire event. --- @param #ARTY self -function ARTY:_openfireontarget() - self:F2() - - -- Debug info - self:T(ARTY.id..string.format("Group %s, number of targets = %d", self.Controllable:GetName(), #self.targets)) - - -- No targets assigned at the moment. - if #self.targets==0 then - self:T3(ARTY.id..string.format("Group %s, no targets assigned at the moment. No need for _OpenFire.", self.Controllable:GetName())) - return - end - - -- First check if there is a target with a certain time for attack. - for i=1,#self.targets do - local _target=self.targets[i] - if _target and _target.time then - if timer.getAbsTime() >= _target.time and _target.underfire==false then - - -- Clock time format. - local _clock=self:_SecondsToClock(_target.time) - local _Cnow=self:_SecondsToClock(timer.getAbsTime()) - - -- Debug info. - self:T(ARTY.id..string.format("Engaging timed target %s. Prio=%d, engaged=%d, time=%s, tnow=%s",_target.name,_target.prio,_target.engaged,_clock,_Cnow)) - - -- Call OpenFire event. - self:__OpenFire(1, _target) - - end - end - end - - -- Sort targets w.r.t. prio and number times engaged already. - self:_SortTargetQueuePrio() - - -- Loop over all sorted targets. - for i=1,#self.targets do - - local _target=self.targets[i] - - if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged then - - -- Debug info. - self:T(ARTY.id..string.format("Engaging target %s. Prio = %d, engaged = %d", _target.name, _target.prio, _target.engaged)) - - -- Call OpenFire event. - self:__OpenFire(1, _target) - - end - end - -end - --- Sort targets with respect to priority and number of times it was already engaged. -- @param #ARTY self @@ -1594,7 +1679,9 @@ function ARTY:_SortTargetQueuePrio() -- Debug output. self:T2(ARTY.id.."Sorted targets wrt prio and number of engagements:") for i=1,#self.targets do - self:T2(ARTY.id..string.format("Target %s, prio=%d, engaged=%d", self.targets[i].name, self.targets[i].prio, self.targets[i].engaged)) + local _target=self.targets[i] + local _clock=self:_SecondsToClock(_target.time) + self:T2(ARTY.id..string.format("Target %s, prio=%d, engaged=%d, time=%s", _target.name, _target.prio, _target.engaged, tostring(_clock))) end end @@ -1621,7 +1708,9 @@ function ARTY:_SortTargetQueueTime() -- Debug output. self:T2(ARTY.id.."Sorted targets wrt time:") for i=1,#self.targets do - self:T2(ARTY.id..string.format("Target %s, prio=%d, engaged=%d", self.targets[i].name, self.targets[i].prio, self.targets[i].engaged)) + local _target=self.targets[i] + local _clock=self:_SecondsToClock(_target.time) + self:T2(ARTY.id..string.format("Target %s, prio=%d, engaged=%d, time=%s", _target.name, _target.prio, _target.engaged, tostring(_clock))) end end @@ -1846,6 +1935,42 @@ function ARTY:_CheckTargetName(name) return newname end +--- Check if target is in range. +-- @param #ARTY self +-- @param #table target Target table. +-- @return #boolean True if target is in range, false otherwise. +function ARTY:_TargetInRange(target) + self:F3(target) + + -- Distance between ARTY group and target. + local _dist=self.Controllable:GetCoordinate():Get2DDistance(target.coord) + + -- Assume we are in range. + local _inrange=true + local text="" + + if _dist < self.minrange then + _inrange=false + text=string.format("%s, target is out of range. Distance of %d km is below min range of %d km.", self.Controllable:GetName(), _dist/1000, self.minrange/1000) + elseif _dist > self.maxrange then + _inrange=false + text=string.format("%s, target is out of range. Distance of %d km is greater than max range of %d km.", self.Controllable:GetName(), _dist/1000, self.maxrange/1000) + end + + -- Debug output. + if not _inrange then + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + end + + -- Remove target if ARTY group cannot move. No change to be ever in range. + if self.Speed==0 then + self:RemoveTarget(target.name) + end + + return _inrange +end + --- Get the weapon type name, which should be used to attack the target. -- @param #ARTY self -- @param #number tnumber Number of weapon type ARTY.WeaponType.XXX @@ -1880,7 +2005,6 @@ function ARTY:_EventFromTo(BA, Event, From, To) self:T3(ARTY.id..text) end - --- Split string. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua -- @param #ARTY self -- @param #string str Sting to split. From 57c5ab1ecd50e5035abecd192291f27480322452 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Sat, 5 May 2018 21:45:07 +0200 Subject: [PATCH 17/31] ARTY v0.8.1 Improvements in FSM states. Still not quite functional. --- .../Moose/Functional/Artillery.lua | 508 ++++++------------ 1 file changed, 156 insertions(+), 352 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index aaf1a77b9..3ea6059c6 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -269,7 +269,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #number version -ARTY.version="0.8.0" +ARTY.version="0.8.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -339,8 +339,8 @@ function ARTY:New(group) self:T3({id=id, desc=desc}) end - -- Set speed to 1/2 of maximum in km/h. - self.Speed=self.DCSdesc.speedMax*3.6 * 0.5 + -- Set speed to 0.7 of maximum in km/h. + self.Speed=self.DCSdesc.speedMax*3.6 * 0.7 -- Displayed name (similar to type name below) self.DisplayName=self.DCSdesc.displayName @@ -353,199 +353,38 @@ function ARTY:New(group) -- Initial group strength. self.IniGroupStrength=#group:GetUnits() - - -- Transitions: - -- Entry + + --------------- + -- Transitions: + --------------- + + -- Entry. self:AddTransition("*", "Start", "CombatReady") -- Blue branch. self:AddTransition("CombatReady", "OpenFire", "Firing") - self:AddTransition("Firing", "OpenFire", "Firing") self:AddTransition("Firing", "CeaseFire", "CombatReady") - --self:AddTransition("CombatReady", "CeaseFire", "CombatReady") -- not in diagram yet. -- Violett branch. - self:AddTransition("Firing", "Winchester", "OutOfAmmo") + self:AddTransition("CombatReady", "Winchester", "OutOfAmmo") -- Red branch. - self:AddTransition("OutOfAmmo", "Rearm", "Rearming") - self:AddTransition("Rearming", "Rearmed", "Rearmed") + self:AddTransition({"CombatReady", "OutOfAmmo"}, "Rearm", "Rearming") + self:AddTransition("Rearming", "Move", "Rearming") + self:AddTransition("Rearming", "Rearmed", "Rearmed") -- Green branch. self:AddTransition("*", "Move", "Moving") self:AddTransition("Moving", "Arrived", "Arrived") - self:AddTransition("*", "CombatReady", "CombatReady") - -- Yellow branch. self:AddTransition("*", "NewTarget", "*") + -- Not in diagram. + self:AddTransition("*", "CombatReady", "CombatReady") self:AddTransition("*", "Status", "*") self:AddTransition("*", "Dead", "*") - - --- User function for OnBefore "OpenFire" event. - -- @function [parent=#ARTY] OnBeforeOpenFire - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param #table target Array holding the target info. - -- @return #boolean If true, allow transition to OnAfterOpenFire. - - --- User function for OnAfter "OpenFire" event. - -- @function [parent=#ARTY] OnAfterOpenFire - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param #table target Array holding the target info. - - - --- User function for OnBefore "CeaseFire" event. - -- @function [parent=#ARTY] OnBeforeCeaseFire - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param #table target Array holding the target info. - -- @return #boolean If true, allow transition to OnAfterCeaseFire. - - --- User function for OnAfter "CeaseFire" event. - -- @function [parent=#ARTY] OnAfterCeaseFire - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param #table target Array holding the target info. - - - --- User function for OnBefore "Winchester" event. - -- @function [parent=#ARTY] OnBeforeWinchester - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @return #boolean If true, allow transition to OnAfterWinchester. - - --- User function for OnAfter "Winchester" event. - -- @function [parent=#ARTY] OnAfterWinchester - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - - - --- User function for OnBefore "Rearm" event. - -- @function [parent=#ARTY] OnBeforeRearm - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @return #boolean If true, allow transition to OnAfterRearm. - - --- User function for OnAfter "Rearm" event. - -- @function [parent=#ARTY] OnAfterRearm - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - - - --- User function for OnBefore "Rearmed" event. - -- @function [parent=#ARTY] OnBeforeRearmed - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @return #boolean If true, allow transition to OnAfterRearmed. - - --- User function for OnAfter "Rearmed" event. - -- @function [parent=#ARTY] OnAfterRearmed - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - - - --- User function for OnBefore "Start" event. - -- @function [parent=#ARTY] OnBeforeStart - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @return #boolean If true, allow transition to OnAfterStart. - - --- User function for OnAfter "Start" event. - -- @function [parent=#ARTY] OnAfterStart - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - - - --- User function for OnBefore "Dead" event. - -- @function [parent=#ARTY] OnBeforeDead - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @return #boolean If true, allow transition to OnAfterDead. - - --- User function for OnAfter "Dead" event. - -- @function [parent=#ARTY] OnAfterDead - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - - - --- User function for OnEnter "CombatReady" state. - -- @function [parent=#ARTY] OnEnterCombatReady - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - - --- User function for OnEnter "Firing" state. - -- @function [parent=#ARTY] OnEnterFiring - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - - --- User function for OnEnter "OutOfAmmo" state. - -- @function [parent=#ARTY] OnEnterOutOfAmmo - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - - --- User function for OnEnter "Rearming" state. - -- @function [parent=#ARTY] OnEnterRearming - -- @param #ARTY self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - return self end @@ -598,13 +437,12 @@ function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, w local _clock=self:_SecondsToClock(_target.time) -- Debug info. - self:T(ARTY.id..string.format("Added target %s, prio=%d, radius=%d, nshells=%d, maxengage=%d, time=%s, weapontype=%d", name, prio, radius, nshells, maxengage, tostring(_clock), weapontype)) + self:T(ARTY.id..string.format("Added target %s", self:_TargetInfo(_target))) -- Trigger new target event. self:NewTarget(_target) end - --- Set minimum firing range. Targets closer than this distance are not engaged. -- @param #ARTY self -- @param #number range Min range in kilometers. Default is 0 km. @@ -891,6 +729,7 @@ function ARTY:_OnEventShot(EventData) if _nammo==0 then self:T(ARTY.id..string.format("Group %s completely out of ammo.", self.Controllable:GetName())) + self:CeaseFire(self.currentTarget) self:Winchester() -- Current target is deallocated ==> return @@ -970,23 +809,6 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- After "NewTarget" event. --- @param #ARTY self --- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param #table target Array holding the target info. --- @return #boolean If true, proceed to onafterOpenfire. -function ARTY:onafterNewTarget(Controllable, From, Event, To, target) - self:_EventFromTo("onafterNewTarget", Event, From, To) - - -- Debug message. - local text=string.format("Adding new target %s.", target.name) - MESSAGE:New(text, 30):ToAllIf(self.Debug) - self:T(ARTY.id..text) -end - --- After "Status" event. Report status of group. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -1000,98 +822,27 @@ function ARTY:onafterStatus(Controllable, From, Event, To) self:_StatusReport() end - local _engage=true - if self:is("OutOfAmmo") then - - -- Coordinate of ARTY unit. - local coordARTY=self.Controllable:GetCoordinate() - - -- Coordinate of rearming group. - local coordRARM=nil - if self.RearmingGroup then - -- Coordinate of the rearming unit. - coordRARM=self.RearmingGroup:GetCoordinate() - -- Remember the coordinates of the rearming unit. After rearming it will go back to this position. - self.RearmingGroupCoord=coordRARM - end - - if self.RearmingGroup and self.RearmingPlaceCoord and self.Speed>0 then - - -- CASE 1: Rearming unit and ARTY group meet at rearming place. - - -- Distances. - local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) - local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord) - - -- Route ARTY group to rearming place. - if dA>100 then - --self.Controllable:RouteGroundOnRoad(self.RearmingPlaceCoord, self.Speed, 1) - --self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) - self:Move(self.RearmingPlaceCoord, false) - end - - -- Route Rearming unit to rearming place - if dR>100 then - self.RearmingGroup:RouteGroundOnRoad(self.RearmingPlaceCoord, 50, 1) - --self:_Move(self.RearmingGroup, self.RearmingPlaceCoord, 50, true) - end - - elseif self.RearmingGroup then - - -- CASE 2: Rearming unit drives to ARTY group. - - -- Send message. - local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingGroup:GetName()) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - - -- Distance between ARTY group and rearming unit. - local distance=coordARTY:Get2DDistance(coordRARM) - - -- If distance is larger than 100 m, the Rearming unit is routed to the ARTY group. - if distance > 100 then - -- Random point 20-100 m away from unit. - local vec2=coordARTY:GetRandomVec2InRadius(20, 100) - local pops=COORDINATE:NewFromVec2(vec2) - - -- Route unit to ARTY group. - self.RearmingGroup:RouteGroundOnRoad(pops, 50, 1) - end - - elseif self.RearmingPlaceCoord then - - -- CASE 3: ARTY drives to rearming place. - - -- Distance. - local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) - - -- Route ARTY group to rearming place. - if dA>100 then - --self.Controllable:RouteGroundOnRoad(self.RearmingPlaceCoord, self.Speed, 1) - --self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) - self:Move(self.RearmingPlaceCoord, false) - end - - end - - _engage=false - + self:Rearm() end if self:is("Moving") then - _engage=false + --self.Controllable:GetVelocityKMH() end if self:is("Rearming") then - _engage=false + local _rearmed=self:_CheckRearmed() + env.info("FF: Rearming. _rearmed = ", tostring(_rearmed)) + if _rearmed then + self:Rearmed() + end end if self:is("Rearmed") then local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) + env.info("FF: Rearmed. Distance ARTY to InitalCoord = ", distance) if distance > 100 then - self:Move(self.InitialCoord, false) - _engage=false + --self:Move(self.InitialCoord, false) else self:CombatReady() end @@ -1099,11 +850,11 @@ function ARTY:onafterStatus(Controllable, From, Event, To) if self:is("Arrived") then - + self:CombatReady() end -- Engage targets. - if _engage then + if self:is("CombatReady") then -- Get a timed target if it is due to be attacked. local _timedTarget=self:_CheckTimedTargets() @@ -1112,7 +863,7 @@ function ARTY:onafterStatus(Controllable, From, Event, To) -- Engage target. if _timedTarget then if self.currentTarget then - self:CeaseFire() + self:CeaseFire(self.currentTarget) end self:OpenFire(_timedTarget) elseif _normalTarget then @@ -1147,7 +898,7 @@ function ARTY:_CheckTimedTargets() if self.currentTarget then if self.currentTarget.prio > _target.prio then -- Current target under attack but has lower priority than this target. - self:T(ARTY.id..string.format("Group %s current target %s has lower prio than new target %s with attack time.", self.Controllable:GetName(), self.currentTarget.name, target.name)) + self:T(ARTY.id..string.format("Group %s current target %s has lower prio than new target %s with attack time.", self.Controllable:GetName(), self.currentTarget.name, _target.name)) return _target end else @@ -1197,28 +948,6 @@ function ARTY:onenterCombatReady(Controllable, From, Event, To) env.info(string.format("FF: onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) ---[[ - if From=="Rearming" and Event=="Rearmed" then - env.info("FF: Comabatready after Rearmed") - - -- Distance to initial position. - local dist=Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) - - if dist>100 then - -- Route group back to its original position, when rearming was at another place. - self:T(ARTY.id..string.format("%s is routed back to its initial position. Distance = %d m.", Controllable:GetName(), dist)) - self:__Move(30, self.InitialCoord, true) - end - - else - - -- Update target queue and open fire. - env.info("FF: Comabatready ==> _openfireontarget.") - self:_openfireontarget() - - end -]] - end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1236,16 +965,16 @@ function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) -- Check that group has no current target already. if self.currentTarget then - -- Debug info. - self:T2(ARTY.id..string.format("Group %s already has a target %s.", self.Controllable:GetName(), self.currentTarget.name)) - + -- This should not happen. Some earlier check failed. + self:E(ARTY.id..string.format("ERROR: Group %s already has a target %s!", self.Controllable:GetName(), self.currentTarget.name)) -- Deny transition. return false end -- Check if target is in range. - local _inrange=self:_TargetInRange(target) - if not _inrange then + if not self:_TargetInRange(target) then + -- This should not happen. Some earlier check failed. + self:E(ARTY.id..string.format("ERROR: Group %s, target %s is out of range!", self.Controllable:GetName(), self.currentTarget.name)) -- Deny transition. return false end @@ -1292,36 +1021,6 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) end ---- Go through queue of assigned tasks and trigger OpenFire event. --- @param #ARTY self -function ARTY:_openfireontarget() - self:F2() - - -- Debug info - self:T2(ARTY.id..string.format("Group %s, number of targets = %d", self.Controllable:GetName(), #self.targets)) - - -- No targets assigned at the moment. - if #self.targets==0 then - self:T3(ARTY.id..string.format("Group %s, no targets assigned at the moment. No need for _OpenFire.", self.Controllable:GetName())) - return - end - - -- Check timed targets first. - local _target=self:_CheckTimedTargets() - if _target then - self:__OpenFire(1, _target) - return - end - - -- Check normal targets - local _target=self:_CheckNormalTargets() - if _target then - self:__OpenFire(1, _target) - return - end - -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- After "CeaseFire" event. Clears task of the group and removes the target if max engagement was reached. @@ -1427,10 +1126,74 @@ end function ARTY:onafterRearm(Controllable, From, Event, To) self:_EventFromTo("onafterRearm", Event, From, To) - -- Start scheduler to monitor ammo count until rearming is complete. - self.SchedIDCheckRearmed=self.scheduler:Schedule(self, ARTY._CheckRearmed, {self}, 20, 20) + -- Coordinate of ARTY unit. + local coordARTY=self.Controllable:GetCoordinate() + + -- Coordinate of rearming group. + local coordRARM=nil + if self.RearmingGroup then + -- Coordinate of the rearming unit. + coordRARM=self.RearmingGroup:GetCoordinate() + -- Remember the coordinates of the rearming unit. After rearming it will go back to this position. + self.RearmingGroupCoord=coordRARM + end + + if self.RearmingGroup and self.RearmingPlaceCoord and self.Speed>0 then + + -- CASE 1: Rearming unit and ARTY group meet at rearming place. + + -- Distances. + local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) + local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord) + + -- Route ARTY group to rearming place. + if dA>100 then + --self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) + self:Move(self:_VicinityCoord(self.RearmingPlaceCoord, 20, 50), false) + end + + -- Route Rearming unit to rearming place + if dR>100 then + self:_Move(self.RearmingGroup, self:_VicinityCoord(self.RearmingPlaceCoord, 20, 50), 50, false) + end + + elseif self.RearmingGroup then + + -- CASE 2: Rearming unit drives to ARTY group. + + -- Send message. + local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingGroup:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + + -- Distance between ARTY group and rearming unit. + local distance=coordARTY:Get2DDistance(coordRARM) + + -- If distance is larger than 100 m, the Rearming unit is routed to the ARTY group. + if distance > 100 then + + -- Route unit to ARTY group. + self:_Move(self.RearmingGroup, self:_VicinityCoord(coordARTY), 50, false) + end + + elseif self.RearmingPlaceCoord then + + -- CASE 3: ARTY drives to rearming place. + + -- Distance. + local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) + + -- Route ARTY group to rearming place. + if dA>100 then + --self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) + self:Move(self:_VicinityCoord(self.RearmingPlaceCoord), false) + end + + end + end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- After "Rearmed" event. Send message if reporting is on and stop the scheduler. @@ -1447,29 +1210,25 @@ function ARTY:onafterRearmed(Controllable, From, Event, To) self:T(ARTY.id..text) MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - -- Stop scheduler. - if self.SchedIDCheckRearmed then - self.scheduler:Stop(self.SchedIDCheckRearmed) - end - -- Route ARTY group backto where it came from (if distance is > 100 m). local d1=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) if d1>100 then - self.Controllable:RouteGroundOnRoad(self.InitialCoord, self.Speed, 5) + self:Move(self.InitialCoord, false) end -- Route unit back to where it came from (if distance is > 100 m). if self.RearmingGroup and self.RearmingGroup:IsAlive() then local d=self.RearmingGroup:GetCoordinate():Get2DDistance(self.RearmingGroupCoord) if d>100 then - self.RearmingGroup:RouteGroundOnRoad(self.RearmingGroupCoord, 50, 1) + self:_Move(self.RearmingGroup, self.RearmingGroupCoord, 50, false) end end end ---- Check if ARTY group is rearmed. +--- Check if ARTY group is rearmed, i.e. has its full amount of ammo. -- @param #ARTY self +-- @return #boolean True if rearming is complete, false otherwise. function ARTY:_CheckRearmed() self:F2() @@ -1490,13 +1249,17 @@ function ARTY:_CheckRearmed() local _rearmpc=nammo/self.FullAmmo*100 -- Send message. - local text=string.format("%s, rearming %d %% complete.", self.Controllable:GetName(), _rearmpc) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) - + if _rearmpc>1 then + local text=string.format("%s, rearming %d %% complete.", self.Controllable:GetName(), _rearmpc) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + end + -- Rearming --> Rearmed --> CombatReady if nammo==self.FullAmmo then - self:Rearmed() + return true + else + return false end end @@ -1569,6 +1332,23 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- After "NewTarget" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target info. +-- @return #boolean If true, proceed to onafterOpenfire. +function ARTY:onafterNewTarget(Controllable, From, Event, To, target) + self:_EventFromTo("onafterNewTarget", Event, From, To) + + -- Debug message. + local text=string.format("Adding new target %s.", target.name) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T(ARTY.id..text) +end + --- After "Dead" event, when a unit has died. When all units of a group are dead trigger "Stop" event. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -1625,7 +1405,7 @@ function ARTY:onafterStop(Controllable, From, Event, To) self.scheduler:Stop(self.SchedIDCheckShooting) end if self.SchedIDCheckRearmed then - self.scheduler:Stop(self.SchedIDCheckRearmed) + --self.scheduler:Stop(self.SchedIDCheckRearmed) end -- Unhandle event. @@ -1994,6 +1774,22 @@ function ARTY:_WeaponTypeName(tnumber) return name end +--- After "Rearmed" event. Send message if reporting is on and stop the scheduler. +-- @param #ARTY self +-- @param Core.Point#COORDINATE coord Center coordinate. +-- @param #number rmin (Optional) Minimum distance in meters from center coordinate. Default 20 m. +-- @param #number rmax (Optional) Maximum distance in meters from center coordinate. Default 100 m. +-- @return Core.Point#COORDINATE Random coordinate in a certain distance from center coordinate. +function ARTY:_VicinityCoord(coord, rmin, rmax) + self:F2({coord=coord, rmin=rmin, rmax=rmax}) + rmin=rmin or 20 + rmax=rmax or 100 + -- Random point. + local vec2=coord:GetRandomVec2InRadius(rmin, rmax) + local pops=COORDINATE:NewFromVec2(vec2) + return pops +end + --- Print event-from-to string to DCS log file. -- @param #ARTY self -- @param #string BA Before/after info. @@ -2022,6 +1818,14 @@ function ARTY:_split(str, sep) return result end +--- Returns the target info as formatted string. +-- @param #ARTY self +-- @return #string name, prio, radius, nshells, engaged, maxengage, time, weapontype +function ARTY:_TargetInfo(target) + local clock=tostring(self:_SecondsToClock(target.time)) + return string.format("%s, prio=%d, radius=%d, nshells=%d, engaged=%d maxengage=%d, weapontype=%d, time=%s", target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, target.weapontype, clock) +end + --- Convert time in seconds to hours, minutes and seconds. -- @param #ARTY self -- @param #number seconds Time in seconds. From f33856cddd82bd8282c8010aed9f44b38e347419 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Sat, 5 May 2018 22:51:43 +0200 Subject: [PATCH 18/31] ARTY v0.8.2 Fixes. Stil WIP --- .../Moose/Functional/Artillery.lua | 56 +++++++++---------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 3ea6059c6..94c24cff8 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -269,7 +269,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #number version -ARTY.version="0.8.1" +ARTY.version="0.8.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -618,7 +618,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) for _, target in pairs(self.targets) do local _clock=self:_SecondsToClock(target.time) local _weapon=self:_WeaponTypeName(target.weapontype) - text=text..string.format("- %s, prio=%3d, radius=%5d, nshells=%4d, maxengage=%3d, time=%11s, weapon=%s\n", target.name, target.prio, target.radius, target.nshells, target.maxengage, tostring(_clock), _weapon) + text=text..string.format("- %s\n", self:_TargetInfo(target)) end text=text..string.format("******************************************************\n") text=text..string.format("Shell types:\n") @@ -676,10 +676,7 @@ function ARTY:_StatusReport() text=text..string.format("Nshots curr. Target = %d\n", self.Nshots) text=text..string.format("Targets:\n") for _, target in pairs(self.targets) do - local _clock=self:_SecondsToClock(target.time) - local _weapon=self:_WeaponTypeName(target.weapontype) - text=text..string.format("- %s, prio=%3d, radius=%5d, nshells=%4d, engaged=%3d, maxengage=%3d, weapon=%s, time=%s\n", - target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, _weapon, tostring(_clock)) + text=text..string.format("- %s\n", self:_TargetInfo(target)) end text=text..string.format("******************************************************") env.info(ARTY.id..text) @@ -890,19 +887,19 @@ function ARTY:_CheckTimedTargets() for i=1,#self.targets do local _target=self.targets[i] - -- Check if target has an attack time which has already passed. - -- Also check that target is not under fire already and that it is in range. - if _target.time and _target.time>=Tnow and _target.underfire==false and self:_TargetInRange(_target) then + -- Check if target has an attack time which has already passed. Also check that target is not under fire already and that it is in range. + if _target.time and Tnow>=_target.time and _target.underfire==false and self:_TargetInRange(_target) then -- Check if group currently has a target and whether its priorty is lower than the timed target. if self.currentTarget then if self.currentTarget.prio > _target.prio then -- Current target under attack but has lower priority than this target. - self:T(ARTY.id..string.format("Group %s current target %s has lower prio than new target %s with attack time.", self.Controllable:GetName(), self.currentTarget.name, _target.name)) + self:T(ARTY.id..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInRange(_target))) return _target end else -- No current target. + self:T(ARTY.id..string.format("Found TIMED target %s.", self:_TargetInfo(_target))) return _target end end @@ -927,7 +924,7 @@ function ARTY:_CheckNormalTargets() if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged and self:_TargetInRange(_target) then -- Debug info. - self:T(ARTY.id..string.format("Engaging target %s. Prio = %d, engaged = %d", _target.name, _target.prio, _target.engaged)) + self:T(ARTY.id..string.format("Found NORMAL target %s", self:_TargetInfo(_target))) return _target end @@ -945,7 +942,9 @@ end -- @param #string Event Event. -- @param #string To To state. function ARTY:onenterCombatReady(Controllable, From, Event, To) - + self:_EventFromTo("onenterCombatReady", Event, From, To) + + -- Debug info env.info(string.format("FF: onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) end @@ -1088,9 +1087,6 @@ function ARTY:onafterWinchester(Controllable, From, Event, To) local text=string.format("%s, winchester.", Controllable:GetName()) self:T(ARTY.id..text) MESSAGE:New(text, 30):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - - -- Init rearming if possible. - --self:Rearm() end @@ -1823,7 +1819,10 @@ end -- @return #string name, prio, radius, nshells, engaged, maxengage, time, weapontype function ARTY:_TargetInfo(target) local clock=tostring(self:_SecondsToClock(target.time)) - return string.format("%s, prio=%d, radius=%d, nshells=%d, engaged=%d maxengage=%d, weapontype=%d, time=%s", target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, target.weapontype, clock) + local weapon=self:_WeaponTypeName(target.weapontype) + local _underfire=tostring(target.underfire) + return string.format("%s, prio=%d, radius=%d, nshells=%d, engaged=%d/%d, weapontype=%s, time=%s, underfire=%s", + target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, weapon, clock,_underfire) end --- Convert time in seconds to hours, minutes and seconds. @@ -1932,10 +1931,7 @@ function ARTY:_Move(group, ToCoord, Speed, OnRoad) -- Route group on road if requested. if OnRoad then - - --path[#path+1]=cpini:GetClosestPointToRoad():WaypointGround(Speed, "On road") - --task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) - + local _first=cpini:GetClosestPointToRoad() local _last=ToCoord:GetClosestPointToRoad() local _onroad=_first:GetPathOnRoad(_last) @@ -1946,8 +1942,6 @@ function ARTY:_Move(group, ToCoord, Speed, OnRoad) task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) end - --path[#path+1]=ToCoord:GetClosestPointToRoad():WaypointGround(Speed, "On road") - --task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) end -- Last waypoint at ToCoord. @@ -1981,16 +1975,18 @@ end function ARTY._PassingWaypoint(group, arty, i, final) -- Debug message. - local text=string.format("Group %s passing waypoint %d (final=%s)", group:GetName(), i, tostring(final)) - - --local pos=group:GetCoordinate() - --local MarkerID=pos:MarkToAll(string.format("Reached Waypoint %d of group %s", i, group:GetName())) - --pos:SmokeRed() - - MESSAGE:New(text,10):ToAll() + local text=string.format("%s, passing waypoint %d.", group:GetName(), i) + if final then + text=string.format("%s, arrived at destination.", group:GetName()) + end env.info(ARTY.id..text) + if final then + MESSAGE:New(text, 10):ToCoalitionIf(group:GetCoalition(), arty.Debug or arty.report) + else + MESSAGE:New(text, 10):ToAllIf(arty.Debug) + end - -- Move --> Moving --> Arrived --> CombatReady. + -- Arrived event. if final and arty.Controllable:GetName()==group:GetName() then arty:Arrived() end From ba944444daa44b0bdffa1466be97704f597d1812 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Sat, 5 May 2018 23:59:02 +0200 Subject: [PATCH 19/31] ARTY v0.8.3 Various improvements. Still WIP. --- .../Moose/Functional/Artillery.lua | 211 +++++++++--------- 1 file changed, 105 insertions(+), 106 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 94c24cff8..6a8b5d1d5 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -269,7 +269,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #number version -ARTY.version="0.8.2" +ARTY.version="0.8.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -287,7 +287,7 @@ ARTY.version="0.8.2" -- DONE: Improve assigned time for engagement. Next day? -- DONE: Improve documentation. -- DONE: Add pseudo user transitions. OnAfter... --- TODO: Make reaming unit a group. +-- DONE: Make reaming unit a group. -- TODO: Adjust documenation again. -- TODO: Add command move to make arty group move. -- TODO: remove schedulers for status event. @@ -384,12 +384,15 @@ function ARTY:New(group) self:AddTransition("*", "CombatReady", "CombatReady") self:AddTransition("*", "Status", "*") self:AddTransition("*", "Dead", "*") + + -- Unknown transitons. To be checked if adding these causes problems. + self:AddTransition("Rearming", "Arrived", "Rearming") return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Assign target coordinates to the ARTY group. Only the first parameter, i.e. the coordinate of the target is mandatory. The remaining parameters are optional and can be used to fine tune the engagement. @@ -563,19 +566,8 @@ function ARTY:SetMissileTypes(tableofnames) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Start Event ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Before "Start" event. Initialized ROE and alarm state. Starts the event handler. --- @param #ARTY self --- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function ARTY:onbeforeStart(Controllable, From, Event, To) - self:_EventFromTo("onbeforeStart", Event, From, To) - - env.info("FF: onbeforeStart") -end --- After "Start" event. Initialized ROE and alarm state. Starts the event handler. -- @param #ARTY self @@ -643,12 +635,8 @@ function ARTY:onafterStart(Controllable, From, Event, To) self:__Status(5) -- Start scheduler to monitor if ARTY group started firing within a certain time. + --TODO: move this to status checks self.SchedIDCheckShooting=self.scheduler:Schedule(self, ARTY._CheckShootingStarted, {self}, 60, 60) - - -- Start cheduler for status reports. --- if self.Debug then --- self.SchedIDStatusReport=self.scheduler:Schedule(self, ARTY._StatusReport, {self}, 30, 30) --- end end @@ -684,7 +672,7 @@ function ARTY:_StatusReport() end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Handling ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Eventhandler for shot event. @@ -803,7 +791,7 @@ function ARTY:_OnEventDead(EventData) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events and States ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- After "Status" event. Report status of group. @@ -815,29 +803,36 @@ end function ARTY:onafterStatus(Controllable, From, Event, To) self:_EventFromTo("onafterStatus", Event, From, To) + -- Debug current status info. if self.Debug then self:_StatusReport() end + -- Group is out of ammo. if self:is("OutOfAmmo") then + env.info(string.format("FF: OutOfAmmo. ==> Rearm")) self:Rearm() end + -- Group is out of moving. if self:is("Moving") then - --self.Controllable:GetVelocityKMH() + local _speed=self.Controllable:GetVelocityKMH() + env.info(string.format("FF: Moving. Velocity = %d km/h", _speed)) end + -- Group is rearming. if self:is("Rearming") then local _rearmed=self:_CheckRearmed() - env.info("FF: Rearming. _rearmed = ", tostring(_rearmed)) + env.info(string.format("FF: Rearming. _rearmed = %s", tostring(_rearmed))) if _rearmed then self:Rearmed() end end + -- Group finished rearming. if self:is("Rearmed") then local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) - env.info("FF: Rearmed. Distance ARTY to InitalCoord = ", distance) + env.info(string.format("FF: Rearmed. Distance ARTY to InitalCoord = %d", distance)) if distance > 100 then --self:Move(self.InitialCoord, false) else @@ -845,25 +840,31 @@ function ARTY:onafterStatus(Controllable, From, Event, To) end end - + -- Group arrived at destination. if self:is("Arrived") then + env.info(string.format("FF: Arrived. ==> CombatReady")) self:CombatReady() end - -- Engage targets. + -- Group is combat ready. if self:is("CombatReady") then + env.info(string.format("FF: Combatready. Looking for targets.")) - -- Get a timed target if it is due to be attacked. + -- Get a valid timed target if it is due to be attacked. local _timedTarget=self:_CheckTimedTargets() + -- Get a valid normal target (one that is not timed). local _normalTarget=self:_CheckNormalTargets() -- Engage target. if _timedTarget then + -- Cease fire on current target first. if self.currentTarget then self:CeaseFire(self.currentTarget) end + -- Open fire on timed target. self:OpenFire(_timedTarget) elseif _normalTarget then + -- Open fire on normal target. self:OpenFire(_normalTarget) end @@ -873,66 +874,6 @@ function ARTY:onafterStatus(Controllable, From, Event, To) self:__Status(5) end ---- Check all timed targets and return the target which should be attacked next. --- @param #ARTY self --- @return #table Target which is due to be attacked now. -function ARTY:_CheckTimedTargets() - - -- Current time. - local Tnow=timer.getAbsTime() - - -- Sort Targets wrt time. - self:_SortTargetQueueTime() - - for i=1,#self.targets do - local _target=self.targets[i] - - -- Check if target has an attack time which has already passed. Also check that target is not under fire already and that it is in range. - if _target.time and Tnow>=_target.time and _target.underfire==false and self:_TargetInRange(_target) then - - -- Check if group currently has a target and whether its priorty is lower than the timed target. - if self.currentTarget then - if self.currentTarget.prio > _target.prio then - -- Current target under attack but has lower priority than this target. - self:T(ARTY.id..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInRange(_target))) - return _target - end - else - -- No current target. - self:T(ARTY.id..string.format("Found TIMED target %s.", self:_TargetInfo(_target))) - return _target - end - end - end - - return nil -end - ---- Check all normal (untimed) targets and return the target with the highest priority which has been engaged the fewest times. --- @param #ARTY self --- @return #table Target which is due to be attacked now or nil if no target could be found. -function ARTY:_CheckNormalTargets() - - -- Sort targets w.r.t. prio and number times engaged already. - self:_SortTargetQueuePrio() - - -- Loop over all sorted targets. - for i=1,#self.targets do - local _target=self.targets[i] - - -- Check that target no time, is not under fire currently and in range. - if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged and self:_TargetInRange(_target) then - - -- Debug info. - self:T(ARTY.id..string.format("Found NORMAL target %s", self:_TargetInfo(_target))) - - return _target - end - end - - return nil -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Enter "CombatReady" state. Route the group back if necessary. @@ -943,10 +884,8 @@ end -- @param #string To To state. function ARTY:onenterCombatReady(Controllable, From, Event, To) self:_EventFromTo("onenterCombatReady", Event, From, To) - -- Debug info - env.info(string.format("FF: onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) - + self:T(string.format("FF: onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1189,7 +1128,6 @@ function ARTY:onafterRearm(Controllable, From, Event, To) end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- After "Rearmed" event. Send message if reporting is on and stop the scheduler. @@ -1206,7 +1144,7 @@ function ARTY:onafterRearmed(Controllable, From, Event, To) self:T(ARTY.id..text) MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - -- Route ARTY group backto where it came from (if distance is > 100 m). + -- Route ARTY group back to where it came from (if distance is > 100 m). local d1=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) if d1>100 then self:Move(self.InitialCoord, false) @@ -1420,7 +1358,7 @@ end -- @param #number nshells Number of shells to fire. -- @param #number weapontype Type of weapon to use. function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) - self:E({coord=coord, radius=radius, nshells=nshells}) + self:F({coord=coord, radius=radius, nshells=nshells}) -- Controllable. local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE @@ -1436,7 +1374,6 @@ function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) -- Execute task. group:SetTask(fire) - --group:PushTask(fire) end @@ -1491,6 +1428,66 @@ function ARTY:_SortTargetQueueTime() end +--- Check all timed targets and return the target which should be attacked next. +-- @param #ARTY self +-- @return #table Target which is due to be attacked now. +function ARTY:_CheckTimedTargets() + + -- Current time. + local Tnow=timer.getAbsTime() + + -- Sort Targets wrt time. + self:_SortTargetQueueTime() + + for i=1,#self.targets do + local _target=self.targets[i] + + -- Check if target has an attack time which has already passed. Also check that target is not under fire already and that it is in range. + if _target.time and Tnow>=_target.time and _target.underfire==false and self:_TargetInRange(_target) then + + -- Check if group currently has a target and whether its priorty is lower than the timed target. + if self.currentTarget then + if self.currentTarget.prio > _target.prio then + -- Current target under attack but has lower priority than this target. + self:T(ARTY.id..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInRange(_target))) + return _target + end + else + -- No current target. + self:T(ARTY.id..string.format("Found TIMED target %s.", self:_TargetInfo(_target))) + return _target + end + end + end + + return nil +end + +--- Check all normal (untimed) targets and return the target with the highest priority which has been engaged the fewest times. +-- @param #ARTY self +-- @return #table Target which is due to be attacked now or nil if no target could be found. +function ARTY:_CheckNormalTargets() + + -- Sort targets w.r.t. prio and number times engaged already. + self:_SortTargetQueuePrio() + + -- Loop over all sorted targets. + for i=1,#self.targets do + local _target=self.targets[i] + + -- Check that target no time, is not under fire currently and in range. + if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged and self:_TargetInRange(_target) then + + -- Debug info. + self:T(ARTY.id..string.format("Found NORMAL target %s", self:_TargetInfo(_target))) + + return _target + end + end + + return nil +end + --- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE controllable Controllable for which the ammo is counted. @@ -1561,8 +1558,7 @@ function ARTY:_GetAmmo(controllable) _gotmissile=true end end - - + -- We are specifically looking for shells or rockets here. if _gotshell then @@ -1755,17 +1751,17 @@ function ARTY:_WeaponTypeName(tnumber) self:F2(tnumber) local name="unknown" if tnumber==ARTY.WeaponType.Auto then - name="Auto (Cannon, Rockets, Missiles)" + name="Auto" -- (Cannon, Rockets, Missiles) elseif tnumber==ARTY.WeaponType.Cannon then - name="Cannon" + name="Cannons" elseif tnumber==ARTY.WeaponType.Rockets then name="Rockets" elseif tnumber==ARTY.WeaponType.UnguidedAny then - name="Any Unguided Weapon (Cannon or Rockets)" + name="Unguided Weapons" -- (Cannon or Rockets) elseif tnumber==ARTY.WeaponType.CruiseMissile then - name="Cruise Missile" + name="Cruise Missiles" elseif tnumber==ARTY.WeaponType.GuidedMissile then - name="Guided Missile" + name="Guided Missiles" end return name end @@ -1774,15 +1770,18 @@ end -- @param #ARTY self -- @param Core.Point#COORDINATE coord Center coordinate. -- @param #number rmin (Optional) Minimum distance in meters from center coordinate. Default 20 m. --- @param #number rmax (Optional) Maximum distance in meters from center coordinate. Default 100 m. +-- @param #number rmax (Optional) Maximum distance in meters from center coordinate. Default 80 m. -- @return Core.Point#COORDINATE Random coordinate in a certain distance from center coordinate. function ARTY:_VicinityCoord(coord, rmin, rmax) self:F2({coord=coord, rmin=rmin, rmax=rmax}) + -- Set default if necessary. rmin=rmin or 20 - rmax=rmax or 100 - -- Random point. - local vec2=coord:GetRandomVec2InRadius(rmin, rmax) + rmax=rmax or 80 + -- Random point withing range. + local vec2=coord:GetRandomVec2InRadius(rmax, rmin) local pops=COORDINATE:NewFromVec2(vec2) + -- Debug info. + self:T(ARTY.id..string.format("Vicinity distance = %d (rmin=%d, rmax=%d)", pops:Get2DDistance(coord), rmin, rmax)) return pops end From c36579f88a1e96a33a2c65110d9c8828755171eb Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Sun, 6 May 2018 12:05:23 +0200 Subject: [PATCH 20/31] ARTY v0.8.4 Several improvments and fixes. --- .../Moose/Functional/Artillery.lua | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 6a8b5d1d5..410b083c1 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -269,7 +269,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #number version -ARTY.version="0.8.3" +ARTY.version="0.8.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -407,10 +407,10 @@ end -- @param #string name (Optional) Name of the target. Default is LL DMS coordinate of the target. If the name was already given, the numbering "#01", "#02",... is appended automatically. -- @return #string Name of the target. Can be used for further reference, e.g. deleting the target from the list. -- @usage paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) --- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 10, 300, 10, 1, "08:02:00", ARTY.WeaponType.Auto, "Red Targets 1") +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 10, 300, 10, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1") -- paladin:Start() function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, weapontype, name) - self:T({coord=coord, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage, time=time, weapontype=weapontype, name=name}) + self:F({coord=coord, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage, time=time, weapontype=weapontype, name=name}) -- Set default values. nshells=nshells or 5 @@ -436,12 +436,6 @@ function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, w -- Add to table. table.insert(self.targets, _target) - -- Clock. - local _clock=self:_SecondsToClock(_target.time) - - -- Debug info. - self:T(ARTY.id..string.format("Added target %s", self:_TargetInfo(_target))) - -- Trigger new target event. self:NewTarget(_target) end @@ -632,12 +626,8 @@ function ARTY:onafterStart(Controllable, From, Event, To) self:HandleEvent(EVENTS.Shot, self._OnEventShot) self:HandleEvent(EVENTS.Dead, self._OnEventDead) + -- Start checking status. self:__Status(5) - - -- Start scheduler to monitor if ARTY group started firing within a certain time. - --TODO: move this to status checks - self.SchedIDCheckShooting=self.scheduler:Schedule(self, ARTY._CheckShootingStarted, {self}, 60, 60) - end --- After "Start" event. Initialized ROE and alarm state. Starts the event handler. @@ -648,8 +638,8 @@ function ARTY:_StatusReport() local Nammo, Nshells, Nrockets, Nmissiles=self:_GetAmmo(self.Controllable) local Tnow=timer.getTime() - local text=string.format("\n******************************************************\n") - text=text..string.format("Status of ARTY = %s\n", self.Controllable:GetName()) + local text=string.format("\n******************* STATUS ***************************\n") + text=text..string.format("ARTY group = %s\n", self.Controllable:GetName()) text=text..string.format("FSM state = %s\n", self:GetState()) text=text..string.format("Total ammo count = %d\n", Nammo) text=text..string.format("Number of shells = %d\n", Nshells) @@ -846,28 +836,41 @@ function ARTY:onafterStatus(Controllable, From, Event, To) self:CombatReady() end - -- Group is combat ready. - if self:is("CombatReady") then - env.info(string.format("FF: Combatready. Looking for targets.")) - - -- Get a valid timed target if it is due to be attacked. - local _timedTarget=self:_CheckTimedTargets() - -- Get a valid normal target (one that is not timed). - local _normalTarget=self:_CheckNormalTargets() + -- Group is firing on target. + if self:is("Firing") then + -- Check that firing started after ~5 min. If not, target is removed. + self:_CheckShootingStarted() + end + + + -- Get a valid timed target if it is due to be attacked. + local _timedTarget=self:_CheckTimedTargets() + + -- Get a valid normal target (one that is not timed). + local _normalTarget=self:_CheckNormalTargets() + + + -- Group is combat ready or firing but we have a high prio timed target. + if self:is("CombatReady") or (self:is("Firing") and _timedTarget) then + env.info(string.format("FF: Combatready or firing and high prio timed target.")) -- Engage target. if _timedTarget then + -- Cease fire on current target first. if self.currentTarget then self:CeaseFire(self.currentTarget) end + -- Open fire on timed target. self:OpenFire(_timedTarget) + elseif _normalTarget then + -- Open fire on normal target. self:OpenFire(_normalTarget) + end - end -- Call status again in 5 sec. @@ -1023,7 +1026,7 @@ function ARTY:onafterWinchester(Controllable, From, Event, To) self:_EventFromTo("onafterWinchester", Event, From, To) -- Send message. - local text=string.format("%s, winchester.", Controllable:GetName()) + local text=string.format("%s, winchester!", Controllable:GetName()) self:T(ARTY.id..text) MESSAGE:New(text, 30):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) @@ -1130,7 +1133,7 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- After "Rearmed" event. Send message if reporting is on and stop the scheduler. +--- After "Rearmed" event. Send ARTY and rearming group back to their inital positions. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. @@ -1189,7 +1192,7 @@ function ARTY:_CheckRearmed() MESSAGE:New(text, 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) end - -- Rearming --> Rearmed --> CombatReady + -- Return if ammo is full. if nammo==self.FullAmmo then return true else @@ -1314,7 +1317,7 @@ function ARTY:onafterDead(Controllable, From, Event, To) end ---- After "Stop" event. Stop schedulers and unhandle events. +--- After "Stop" event. Unhandle events and cease fire on current target. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. @@ -1334,14 +1337,6 @@ function ARTY:onafterStop(Controllable, From, Event, To) -- Remove all targets. --self:RemoveAllTargets() - -- Stop schedulers. - if self.SchedIDCheckShooting then - self.scheduler:Stop(self.SchedIDCheckShooting) - end - if self.SchedIDCheckRearmed then - --self.scheduler:Stop(self.SchedIDCheckRearmed) - end - -- Unhandle event. self:UnHandleEvent(EVENTS.Shot) self:UnHandleEvent(EVENTS.Dead) @@ -1361,7 +1356,7 @@ function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) self:F({coord=coord, radius=radius, nshells=nshells}) -- Controllable. - local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + local group=self.Controllable --Wrapper.Group#GROUP -- Set ROE to weapon free. group:OptionROEOpenFire() @@ -1393,14 +1388,14 @@ function ARTY:_SortTargetQueuePrio() self:T2(ARTY.id.."Sorted targets wrt prio and number of engagements:") for i=1,#self.targets do local _target=self.targets[i] - local _clock=self:_SecondsToClock(_target.time) - self:T2(ARTY.id..string.format("Target %s, prio=%d, engaged=%d, time=%s", _target.name, _target.prio, _target.engaged, tostring(_clock))) + self:T2(ARTY.id..string.format("Target %s", self:_TargetInfo(_target))) end end ---- Sort targets with respect to engage time. +--- Sort array with respect to time. Array elements must have a .time entry. -- @param #ARTY self -function ARTY:_SortTargetQueueTime() +-- @param #table queue Array to sort. Should have elemnt .time. +function ARTY:_SortQueueTime(queue) self:F2() -- Sort targets w.r.t attack time. @@ -1416,14 +1411,15 @@ function ARTY:_SortTargetQueueTime() end return a.time < b.time end - table.sort(self.targets, _sort) + table.sort(queue, _sort) -- Debug output. - self:T2(ARTY.id.."Sorted targets wrt time:") - for i=1,#self.targets do - local _target=self.targets[i] - local _clock=self:_SecondsToClock(_target.time) - self:T2(ARTY.id..string.format("Target %s, prio=%d, engaged=%d, time=%s", _target.name, _target.prio, _target.engaged, tostring(_clock))) + self:T(ARTY.id.."Sorted queue wrt time:") + for i=1,#queue do + local _queue=queue[i] + local _time=tostring(_queue.time) + local _clock=tostring(self:_SecondsToClock(_queue.time)) + self:T(ARTY.id..string.format("%s: time=%s, clock=%s", _queue.name, _time, _clock)) end end @@ -1437,11 +1433,14 @@ function ARTY:_CheckTimedTargets() local Tnow=timer.getAbsTime() -- Sort Targets wrt time. - self:_SortTargetQueueTime() + self:_SortQueueTime(self.targets) for i=1,#self.targets do local _target=self.targets[i] + -- Debug info. + self:T3(ARTY.id..string.format("Check TIMED target %d: %s", i, self:_TargetInfo(_target))) + -- Check if target has an attack time which has already passed. Also check that target is not under fire already and that it is in range. if _target.time and Tnow>=_target.time and _target.underfire==false and self:_TargetInRange(_target) then @@ -1449,7 +1448,7 @@ function ARTY:_CheckTimedTargets() if self.currentTarget then if self.currentTarget.prio > _target.prio then -- Current target under attack but has lower priority than this target. - self:T(ARTY.id..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInRange(_target))) + self:T(ARTY.id..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInfo(_target))) return _target end else @@ -1474,6 +1473,9 @@ function ARTY:_CheckNormalTargets() -- Loop over all sorted targets. for i=1,#self.targets do local _target=self.targets[i] + + -- Debug info. + self:T3(ARTY.id..string.format("Check NORMAL target %d: %s", i, self:_TargetInfo(_target))) -- Check that target no time, is not under fire currently and in range. if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged and self:_TargetInRange(_target) then @@ -1802,14 +1804,12 @@ end -- @param #string sep Speparator for split. -- @return #table Split text. function ARTY:_split(str, sep) - self:F3({str=str, sep=sep}) - + self:F3({str=str, sep=sep}) local result = {} local regex = ("([^%s]+)"):format(sep) for each in str:gmatch(regex) do table.insert(result, each) end - return result end From a2c12dc05eec4304ef00d5fe608487e0b0dd73bc Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Sun, 6 May 2018 12:51:51 +0200 Subject: [PATCH 21/31] ARTY v0.8.5 Improved GetAmmo function. Display of ammo table. Removed all schedulers. --- .../Moose/Functional/Artillery.lua | 90 +++++++++---------- 1 file changed, 43 insertions(+), 47 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 410b083c1..19b401e65 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -14,7 +14,9 @@ -- * Multiple targets can be assigned. No restriction on number of targets. -- * Targets can be given a priority. Engagement of targets is executed a according to their priority. -- * Engagements can be scheduled, i.e. will be executed at a certain time of the day. --- * Special weapon types can be selected. +-- * Special weapon types can be selected for each attack, e.g. cruise missiles for Naval units. +-- * Automatic rearming once the artillery is out of ammo. +-- * Finite state machine implementation. User can interact when certain events occur. -- -- ==== -- @@ -49,11 +51,7 @@ -- @field #number Nrockets0 Initial amount of rockets of the whole group. -- @field #number Nmissiles0 Initial amount of missiles of the whole group. -- @field #number FullAmmo Full amount of all ammunition taking the number of alive units into account. --- @field Core.Scheduler#SCHEDULER scheduler Scheduler object handling various timed functions. --- @field #number SchedIDCheckRearmed Scheduler ID responsible for checking whether rearming of the ARTY group is complete. --- @field #number SchedIDCheckShooting Scheduler ID for checking whether a group startet firing within a certain time after the fire at point task was assigned. -- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. Default is 300 seconds. --- @field #number SchedIDStatusReport Scheduler ID for status report messages. The scheduler is only launched in debug mode. -- @field #table DCSdesc DCS descriptors of the ARTY group. -- @field #string Type Type of the ARTY group. -- @field #string DisplayName Extended type name of the ARTY group. @@ -229,9 +227,6 @@ ARTY={ Nrockets0=0, Nmissiles0=0, FullAmmo=0, - scheduler=nil, - SchedIDCheckRearmed=nil, - SchedIDCheckShooting=nil, WaitForShotTime=300, SchedIDStatusReport=nil, DCSdesc=nil, @@ -269,7 +264,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #number version -ARTY.version="0.8.4" +ARTY.version="0.8.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -290,7 +285,7 @@ ARTY.version="0.8.4" -- DONE: Make reaming unit a group. -- TODO: Adjust documenation again. -- TODO: Add command move to make arty group move. --- TODO: remove schedulers for status event. +-- DONE: remove schedulers for status event. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -325,9 +320,6 @@ function ARTY:New(group) -- Set the initial coordinates of the ARTY group. self.InitialCoord=group:GetCoordinate() - -- Create scheduler object. - self.scheduler=SCHEDULER:New(self) - -- Get DCS descriptors of group. local DCSgroup=Group.getByName(group:GetName()) local DCSunit=DCSgroup:getUnit(1) @@ -577,7 +569,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) MESSAGE:New(text, 10):ToAllIf(self.Debug) -- Get Ammo. - self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0=self:_GetAmmo(self.Controllable) + self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0=self:GetAmmo(self.Controllable, self.Debug) local text=string.format("\n******************************************************\n") text=text..string.format("Arty group = %s\n", Controllable:GetName()) @@ -635,7 +627,7 @@ end function ARTY:_StatusReport() -- Get Ammo. - local Nammo, Nshells, Nrockets, Nmissiles=self:_GetAmmo(self.Controllable) + local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo(self.Controllable) local Tnow=timer.getTime() local text=string.format("\n******************* STATUS ***************************\n") @@ -699,7 +691,7 @@ function ARTY:_OnEventShot(EventData) MESSAGE:New(text, 5):ToAllIf(self.Debug) -- Get current ammo. - local _nammo,_nshells,_nrockets,_nmissiles=self:_GetAmmo(self.Controllable) + local _nammo,_nshells,_nrockets,_nmissiles=self:GetAmmo(self.Controllable) if _nammo==0 then @@ -1170,7 +1162,7 @@ function ARTY:_CheckRearmed() self:F2() -- Get current ammo. - local nammo,nshells,nrockets,nmissiles=self:_GetAmmo(self.Controllable) + local nammo,nshells,nrockets,nmissiles=self:GetAmmo(self.Controllable) -- Number of units still alive. local units=self.Controllable:GetUnits() @@ -1385,10 +1377,10 @@ function ARTY:_SortTargetQueuePrio() table.sort(self.targets, _sort) -- Debug output. - self:T2(ARTY.id.."Sorted targets wrt prio and number of engagements:") + self:T3(ARTY.id.."Sorted targets wrt prio and number of engagements:") for i=1,#self.targets do local _target=self.targets[i] - self:T2(ARTY.id..string.format("Target %s", self:_TargetInfo(_target))) + self:T3(ARTY.id..string.format("Target %s", self:_TargetInfo(_target))) end end @@ -1396,7 +1388,7 @@ end -- @param #ARTY self -- @param #table queue Array to sort. Should have elemnt .time. function ARTY:_SortQueueTime(queue) - self:F2() + self:F3({queue=queue}) -- Sort targets w.r.t attack time. local function _sort(a, b) @@ -1414,12 +1406,12 @@ function ARTY:_SortQueueTime(queue) table.sort(queue, _sort) -- Debug output. - self:T(ARTY.id.."Sorted queue wrt time:") + self:T3(ARTY.id.."Sorted queue wrt time:") for i=1,#queue do local _queue=queue[i] local _time=tostring(_queue.time) local _clock=tostring(self:_SecondsToClock(_queue.time)) - self:T(ARTY.id..string.format("%s: time=%s, clock=%s", _queue.name, _time, _clock)) + self:T3(ARTY.id..string.format("%s: time=%s, clock=%s", _queue.name, _time, _clock)) end end @@ -1428,7 +1420,8 @@ end -- @param #ARTY self -- @return #table Target which is due to be attacked now. function ARTY:_CheckTimedTargets() - + self:F3() + -- Current time. local Tnow=timer.getAbsTime() @@ -1448,12 +1441,12 @@ function ARTY:_CheckTimedTargets() if self.currentTarget then if self.currentTarget.prio > _target.prio then -- Current target under attack but has lower priority than this target. - self:T(ARTY.id..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInfo(_target))) + self:T2(ARTY.id..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInfo(_target))) return _target end else -- No current target. - self:T(ARTY.id..string.format("Found TIMED target %s.", self:_TargetInfo(_target))) + self:T2(ARTY.id..string.format("Found TIMED target %s.", self:_TargetInfo(_target))) return _target end end @@ -1481,7 +1474,7 @@ function ARTY:_CheckNormalTargets() if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged and self:_TargetInRange(_target) then -- Debug info. - self:T(ARTY.id..string.format("Found NORMAL target %s", self:_TargetInfo(_target))) + self:T2(ARTY.id..string.format("Found NORMAL target %s", self:_TargetInfo(_target))) return _target end @@ -1493,12 +1486,17 @@ end --- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE controllable Controllable for which the ammo is counted. +-- @param #boolean display Display ammo table as message to all. Default false. -- @return #number Total amount of ammo the whole group has left. -- @return #number Number of shells the group has left. -- @return #number Number of rockets the group has left. -- @return #number Number of missiles the group has left. -function ARTY:_GetAmmo(controllable) - self:F2(controllable) +function ARTY:GetAmmo(controllable, display) + self:F3({controllable=controllable, display=display}) + + if display==nil then + display=false + end -- Init counter. local nammo=0 @@ -1515,18 +1513,19 @@ function ARTY:_GetAmmo(controllable) for _,unit in pairs(units) do if unit and unit:IsAlive() then + + -- Output. + local text=string.format("ARTY group %s - unit %s:\n", self.Controllable:GetName(), unit:GetName()) + -- Get ammo table. local ammotable=unit:GetAmmo() - self:T2({ammotable=ammotable}) - - local name=unit:GetName() - + if ammotable ~= nil then local weapons=#ammotable self:T2(ARTY.id..string.format("Number of weapons %d.", weapons)) - self:T2(ammotable) + self:T2({ammotable=ammotable}) -- Loop over all weapons. for w=1,weapons do @@ -1568,9 +1567,7 @@ function ARTY:_GetAmmo(controllable) nshells=nshells+Nammo -- Debug info. - local text=string.format("Unit %s has %d shells of type %s", name, Nammo, Tammo) - self:T2(ARTY.id..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug and not self.report) + text=text..string.format("- %d shells of type %s\n", Nammo, Tammo) elseif _gotrocket then @@ -1578,9 +1575,7 @@ function ARTY:_GetAmmo(controllable) nrockets=nrockets+Nammo -- Debug info. - local text=string.format("Unit %s has %d rockets of type %s", name, Nammo, Tammo) - self:T2(ARTY.id..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug and not self.report) + text=text..string.format("- %d rockets of type %s\n", Nammo, Tammo) elseif _gotmissile then @@ -1588,21 +1583,22 @@ function ARTY:_GetAmmo(controllable) nmissiles=nmissiles+Nammo -- Debug info. - local text=string.format("Unit %s has %d missiles of type %s", name, Nammo, Tammo) - self:T2(ARTY.id..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug and not self.report) - + text=text..string.format("- %d missiles of type %s\n", name, Nammo, Tammo) + else -- Debug info. - local text=string.format("Unit %s has %d ammo of type %s", name, Nammo, Tammo) - self:T2(ARTY.id..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug and not self.report) + text=text..string.format("- %d unknown ammo of type %s\n", Nammo, Tammo) end end end + + self:T2(ARTY.id..text) + MESSAGE:New(text, 10):ToAllIf(display) + + end end @@ -1768,7 +1764,7 @@ function ARTY:_WeaponTypeName(tnumber) return name end ---- After "Rearmed" event. Send message if reporting is on and stop the scheduler. +--- Find a random coordinate in the vicinity of another coordinate. -- @param #ARTY self -- @param Core.Point#COORDINATE coord Center coordinate. -- @param #number rmin (Optional) Minimum distance in meters from center coordinate. Default 20 m. From 16d4a65569724ffec23fdc5b38ed47aee0b390d6 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Sun, 6 May 2018 15:13:36 +0200 Subject: [PATCH 22/31] ARTY v0.8.6 Added user function for rearming properties. Minor bug fixes. --- .../Moose/Functional/Artillery.lua | 136 +++++++++++++----- 1 file changed, 97 insertions(+), 39 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 19b401e65..1b2d15fdf 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -44,30 +44,36 @@ -- @type ARTY -- @field #string ClassName Name of the class. -- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. --- @field #table targets Targets assigned. +-- @field #table targets All targets assigned. +-- @field #table moves All moves assigned. -- @field #table currentTarget Holds the current target, if there is one assigned. -- @field #number Nammo0 Initial amount total ammunition (shells+rockets+missiles) of the whole group. -- @field #number Nshells0 Initial amount of shells of the whole group. -- @field #number Nrockets0 Initial amount of rockets of the whole group. -- @field #number Nmissiles0 Initial amount of missiles of the whole group. -- @field #number FullAmmo Full amount of all ammunition taking the number of alive units into account. +-- @field #number StatusInterval Update interval in seconds between status updates. Default 10 seconds. -- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. Default is 300 seconds. -- @field #table DCSdesc DCS descriptors of the ARTY group. -- @field #string Type Type of the ARTY group. -- @field #string DisplayName Extended type name of the ARTY group. -- @field #number IniGroupStrength Inital number of units in the ARTY group. -- @field #boolean IsArtillery If true, ARTY group has attribute "Artillery". --- @field #number Speed Max speed of ARTY group. +-- @field #number Speed Maximum speed of ARTY group in km/h. +-- @field #number RearmingDistance Safe distance in meters between ARTY group and rearming group or place at which rearming is possible. Default 100 m. -- @field Wrapper.Group#GROUP RearmingGroup Unit designated to rearm the ARTY group. +-- @field #number RearmingGroupSpeed Speed in km/h the rearming unit moves at. Default 50 km/h. +-- @field #boolean RearmingGroupOnRoad If true, rearming group will move to ARTY group or rearming place using mainly roads. Default false. -- @field Core.Point#COORDINATE RearmingGroupCoord Initial coordinates of the rearming unit. After rearming complete, the unit will return to this position. -- @field Core.Point#COORDINATE RearmingPlaceCoord Coordinates of the rearming place. If the place is more than 100 m away from the ARTY group, the group will go there. +-- @field #boolean RearmingArtyOnRoad If true, ARTY group will move to rearming place using mainly roads. Default false. -- @field Core.Point#COORDINATE InitialCoord Initial coordinates of the ARTY group. -- @field #boolean report Arty group sends messages about their current state or target to its coaliton. -- @field #table ammoshells Table holding names of the shell types which are included when counting the ammo. Default is {"weapons.shells"} which include most shells. -- @field #table ammorockets Table holding names of the rocket types which are included when counting the ammo. Default is {"weapons.nurs"} which includes most unguided rockets. -- @field #table ammomissiles Table holding names of the missile types which are included when counting the ammo. Default is {"weapons.missiles"} which includes some guided missiles. -- @field #number Nshots Number of shots fired on current target. --- @field #number minrange Minimum firing range in kilometers. Targets closer than this distance are not engaged. Default 0 km. +-- @field #number minrange Minimum firing range in kilometers. Targets closer than this distance are not engaged. Default 0.5 km. -- @field #number maxrange Maximum firing range in kilometers. Targets further away than this distance are not engaged. Default 10000 km. -- @extends Core.Fsm#FSM_CONTROLLABLE @@ -221,29 +227,34 @@ ARTY={ ClassName = "ARTY", Debug = true, targets = {}, + moves = {}, currentTarget = nil, Nammo0=0, Nshells0=0, Nrockets0=0, Nmissiles0=0, FullAmmo=0, + StatusInterval=10, WaitForShotTime=300, - SchedIDStatusReport=nil, DCSdesc=nil, Type=nil, DisplayName=nil, IniGroupStrength=0, IsArtillery=nil, + RearmingDistance=100, RearmingGroup=nil, + RearmingGroupSpeed=50, + RearmingGroupOnRoad=false, RearmingGroupCoord=nil, RearmingPlaceCoord=nil, + RearmingArtyOnRoad=false, InitialCoord=nil, report=true, ammoshells={"weapons.shells"}, ammorockets={"weapons.nurs"}, ammomissiles={"weapons.missiles"}, Nshots=0, - minrange=0, + minrange=500, maxrange=1000000, } @@ -264,7 +275,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #number version -ARTY.version="0.8.5" +ARTY.version="0.8.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -434,10 +445,10 @@ end --- Set minimum firing range. Targets closer than this distance are not engaged. -- @param #ARTY self --- @param #number range Min range in kilometers. Default is 0 km. +-- @param #number range Min range in kilometers. Default is 0.5 km. function ARTY:SetMinFiringRange(range) self:F({range=range}) - self.minrange=range or 0 + self.minrange=range*1000 or 500 end --- Set maximum firing range. Targets further away than this distance are not engaged. @@ -448,6 +459,14 @@ function ARTY:SetMaxFiringRange(range) self.maxrange=range*1000 or 1000*1000 end +--- Set time interval between status updates. During the status check, new events are triggered. +-- @param #ARTY self +-- @param #number interval Time interval in seconds. Default 10 seconds. +function ARTY:SetStatusInterval(interval) + self:F({interval=interval}) + self.StatusInterval=interval or 10 +end + --- Set time how it is waited a unit the first shot event happens. If no shot is fired after this time, the task to fire is aborted and the target removed. -- @param #ARTY self -- @param #number waittime Time in seconds. Default 300 seconds. @@ -456,14 +475,52 @@ function ARTY:SetWaitForShotTime(waittime) self.WaitForShotTime=waittime or 300 end +--- Define the safe distance between ARTY group and rearming unit or rearming place at which rearming process is possible. +-- @param #ARTY self +-- @param #number distance Safe distance in meters. Default is 100 m. +function ARTY:SetRearmingDistance(distance) + self:F({distance=distance}) + self.RearmingDistance=distance or 100 +end + --- Assign a group, which is responsible for rearming the ARTY group. If the group is too far away from the ARTY group it will be guided towards the ARTY group. -- @param #ARTY self --- @param Wrapper.Group#GROUP unit Unit that is supposed to rearm the ARTY group. +-- @param Wrapper.Group#GROUP group Group that is supposed to rearm the ARTY group. function ARTY:SetRearmingGroup(group) self:F({group=group}) self.RearmingGroup=group end +--- Set the speed the rearming group moves at towards the ARTY group or the rearming place. +-- @param #ARTY self +-- @param #number speed Speed in km/h. Default 50 km/h. +function ARTY:SetRearmingGroupSpeed(speed) + self:F({speed=speed}) + self.RearmingGroupSpeed=speed or 50 +end + +--- Define if rearming group uses mainly roads to drive to the ARTY group or rearming place. +-- @param #ARTY self +-- @param #boolean onroad If true, rearming group uses mainly roads. If false, it drives directly to the ARTY group or rearming place. +function ARTY:SetRearmingGroupOnRoad(onroad) + self:F({onroad=onroad}) + if onroad==nil then + onroad=true + end + self.RearmingGroupOnRoad=onroad +end + +--- Define if ARTY group uses mainly roads to drive to the rearming place. +-- @param #ARTY self +-- @param #boolean onroad If true, ARTY group uses mainly roads. If false, it drives directly to the rearming place. +function ARTY:SetRearmingArtyOnRoad(onroad) + self:F({onroad=onroad}) + if onroad==nil then + onroad=true + end + self.RearmingArtyOnRoad=onroad +end + --- Defines the rearming place of the ARTY group. If the place is too far away from the ARTY group it will be routed to the place. -- @param #ARTY self -- @param Wrapper.Point#COORDINATE coord Coordinates of the rearming place. @@ -584,18 +641,22 @@ function ARTY:onafterStart(Controllable, From, Event, To) text=text..string.format("Number of shells = %d\n", self.Nshells0) text=text..string.format("Number of rockets = %d\n", self.Nrockets0) text=text..string.format("Number of missiles = %d\n", self.Nmissiles0) + if self.RearmingGroup or self.RearmingPlaceCoord then + text=text..string.format("Rearming safe dist. = %d m\n", self.RearmingDistance) + end if self.RearmingGroup then - text=text..string.format("Reaming group = %s\n", self.RearmingGroup:GetName()) + text=text..string.format("Rearming group = %s\n", self.RearmingGroup:GetName()) + text=text..string.format("Rearming group speed= %d km/h\n", self.RearmingGroupSpeed) + text=text..string.format("Rearming group roads= %s\n", tostring(self.RearmingGroupOnRoad)) end if self.RearmingPlaceCoord then - local dist=self.InitialCoord:Get2DDistance(self.RearmingPlaceCoord) - text=text..string.format("Reaming coord dist. = %d m\n", dist) + local dist=self.InitialCoord:Get2DDistance(self.RearmingPlaceCoord) + text=text..string.format("Rearming coord dist = %d m\n", dist) + text=text..string.format("Rearming ARTY roads = %s\n", tostring(self.RearmingArtyOnRoad)) end text=text..string.format("******************************************************\n") text=text..string.format("Targets:\n") for _, target in pairs(self.targets) do - local _clock=self:_SecondsToClock(target.time) - local _weapon=self:_WeaponTypeName(target.weapontype) text=text..string.format("- %s\n", self:_TargetInfo(target)) end text=text..string.format("******************************************************\n") @@ -619,7 +680,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) self:HandleEvent(EVENTS.Dead, self._OnEventDead) -- Start checking status. - self:__Status(5) + self:__Status(self.StatusInterval) end --- After "Start" event. Initialized ROE and alarm state. Starts the event handler. @@ -798,8 +859,9 @@ function ARTY:onafterStatus(Controllable, From, Event, To) -- Group is out of moving. if self:is("Moving") then - local _speed=self.Controllable:GetVelocityKMH() - env.info(string.format("FF: Moving. Velocity = %d km/h", _speed)) + --local _speed=self.Controllable:GetVelocityKMH() + --env.info(string.format("FF: Moving. Velocity = %d km/h", _speed)) + env.info(string.format("FF: Moving")) end -- Group is rearming. @@ -815,9 +877,7 @@ function ARTY:onafterStatus(Controllable, From, Event, To) if self:is("Rearmed") then local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) env.info(string.format("FF: Rearmed. Distance ARTY to InitalCoord = %d", distance)) - if distance > 100 then - --self:Move(self.InitialCoord, false) - else + if distance <= self.RearmingDistance then self:CombatReady() end end @@ -841,7 +901,6 @@ function ARTY:onafterStatus(Controllable, From, Event, To) -- Get a valid normal target (one that is not timed). local _normalTarget=self:_CheckNormalTargets() - -- Group is combat ready or firing but we have a high prio timed target. if self:is("CombatReady") or (self:is("Firing") and _timedTarget) then env.info(string.format("FF: Combatready or firing and high prio timed target.")) @@ -865,8 +924,8 @@ function ARTY:onafterStatus(Controllable, From, Event, To) end end - -- Call status again in 5 sec. - self:__Status(5) + -- Call status again in ~10 sec. + self:__Status(self.StatusInterval) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1077,14 +1136,13 @@ function ARTY:onafterRearm(Controllable, From, Event, To) local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord) -- Route ARTY group to rearming place. - if dA>100 then - --self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) - self:Move(self:_VicinityCoord(self.RearmingPlaceCoord, 20, 50), false) + if dA > self.RearmingDistance then + self:Move(self:_VicinityCoord(self.RearmingPlaceCoord, self.RearmingDistance/4, self.RearmingDistance/2), self.RearmingArtyOnRoad) end - -- Route Rearming unit to rearming place - if dR>100 then - self:_Move(self.RearmingGroup, self:_VicinityCoord(self.RearmingPlaceCoord, 20, 50), 50, false) + -- Route Rearming group to rearming place. + if dR > self.RearmingDistance then + self:_Move(self.RearmingGroup, self:_VicinityCoord(self.RearmingPlaceCoord, self.RearmingDistance/4, self.RearmingDistance/2), self.RearmingGroupSpeed, self.RearmingGroupOnRoad) end elseif self.RearmingGroup then @@ -1099,11 +1157,11 @@ function ARTY:onafterRearm(Controllable, From, Event, To) -- Distance between ARTY group and rearming unit. local distance=coordARTY:Get2DDistance(coordRARM) - -- If distance is larger than 100 m, the Rearming unit is routed to the ARTY group. - if distance > 100 then + -- If distance is larger than ~100 m, the Rearming unit is routed to the ARTY group. + if distance > self.RearmingDistance then - -- Route unit to ARTY group. - self:_Move(self.RearmingGroup, self:_VicinityCoord(coordARTY), 50, false) + -- Route rearming group to ARTY group. + self:_Move(self.RearmingGroup, self:_VicinityCoord(coordARTY), self.RearmingGroupSpeed, self.RearmingGroupOnRoad) end elseif self.RearmingPlaceCoord then @@ -1114,7 +1172,7 @@ function ARTY:onafterRearm(Controllable, From, Event, To) local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) -- Route ARTY group to rearming place. - if dA>100 then + if dA > self.RearmingDistance then --self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) self:Move(self:_VicinityCoord(self.RearmingPlaceCoord), false) end @@ -1141,15 +1199,15 @@ function ARTY:onafterRearmed(Controllable, From, Event, To) -- Route ARTY group back to where it came from (if distance is > 100 m). local d1=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) - if d1>100 then + if d1 > self.RearmingDistance then self:Move(self.InitialCoord, false) end -- Route unit back to where it came from (if distance is > 100 m). if self.RearmingGroup and self.RearmingGroup:IsAlive() then local d=self.RearmingGroup:GetCoordinate():Get2DDistance(self.RearmingGroupCoord) - if d>100 then - self:_Move(self.RearmingGroup, self.RearmingGroupCoord, 50, false) + if d > self.RearmingDistance then + self:_Move(self.RearmingGroup, self.RearmingGroupCoord, self.RearmingGroupSpeed, self.RearmingGroupOnRoad) end end @@ -1721,10 +1779,10 @@ function ARTY:_TargetInRange(target) if _dist < self.minrange then _inrange=false - text=string.format("%s, target is out of range. Distance of %d km is below min range of %d km.", self.Controllable:GetName(), _dist/1000, self.minrange/1000) + text=string.format("%s, target is out of range. Distance of %.1f km is below min range of %.1f km.", self.Controllable:GetName(), _dist/1000, self.minrange/1000) elseif _dist > self.maxrange then _inrange=false - text=string.format("%s, target is out of range. Distance of %d km is greater than max range of %d km.", self.Controllable:GetName(), _dist/1000, self.maxrange/1000) + text=string.format("%s, target is out of range. Distance of %.1f km is greater than max range of %.1f km.", self.Controllable:GetName(), _dist/1000, self.maxrange/1000) end -- Debug output. From d9222c23cb9ab419b9a716e6891d18f104fb3e6f Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Sun, 6 May 2018 17:03:31 +0200 Subject: [PATCH 23/31] ARTY v0.8.7 Added first version of move implementation. Moves are not executed yet. --- .../Moose/Functional/Artillery.lua | 139 +++++++++++++++--- 1 file changed, 117 insertions(+), 22 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 1b2d15fdf..36a847036 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -59,7 +59,8 @@ -- @field #string DisplayName Extended type name of the ARTY group. -- @field #number IniGroupStrength Inital number of units in the ARTY group. -- @field #boolean IsArtillery If true, ARTY group has attribute "Artillery". --- @field #number Speed Maximum speed of ARTY group in km/h. +-- @field #number SpeedMax Maximum speed of ARTY group in km/h. This is determined from the DCS descriptor table. +-- @field #number Speed Default speed in km/h the ARTY group moves at. Maximum speed possible is 80% of maximum speed the group can do. -- @field #number RearmingDistance Safe distance in meters between ARTY group and rearming group or place at which rearming is possible. Default 100 m. -- @field Wrapper.Group#GROUP RearmingGroup Unit designated to rearm the ARTY group. -- @field #number RearmingGroupSpeed Speed in km/h the rearming unit moves at. Default 50 km/h. @@ -275,7 +276,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #number version -ARTY.version="0.8.6" +ARTY.version="0.8.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -297,6 +298,7 @@ ARTY.version="0.8.6" -- TODO: Adjust documenation again. -- TODO: Add command move to make arty group move. -- DONE: remove schedulers for status event. +-- TODO: Improve handling of special weapons. When winchester? ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -342,8 +344,11 @@ function ARTY:New(group) self:T3({id=id, desc=desc}) end - -- Set speed to 0.7 of maximum in km/h. - self.Speed=self.DCSdesc.speedMax*3.6 * 0.7 + -- Maximum speed in km/h. + self.SpeedMax=self.DCSdesc.speedMax*3.6 + + -- Set speed to 0.7 of maximum. + self.Speed=self.SpeedMax * 0.7 -- Displayed name (similar to type name below) self.DisplayName=self.DCSdesc.displayName @@ -386,6 +391,7 @@ function ARTY:New(group) -- Not in diagram. self:AddTransition("*", "CombatReady", "CombatReady") self:AddTransition("*", "Status", "*") + self:AddTransition("*", "NewMove", "*") self:AddTransition("*", "Dead", "*") -- Unknown transitons. To be checked if adding these causes problems. @@ -428,7 +434,7 @@ function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, w local _name=name or coord:ToStringLLDMS() -- Check if the name has already been used for another target. If so, the function returns a new unique name. - _name=self:_CheckTargetName(_name) + _name=self:_CheckName(self.targets, _name) -- Time in seconds. local _time=self:_ClockToSeconds(time) @@ -441,6 +447,53 @@ function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, w -- Trigger new target event. self:NewTarget(_target) + + return _name +end + +--- Assign coordinate to where the ARTY group should move. +-- @param #ARTY self +-- @param Wrapper.Point#COORDINATE coord Coordinates of the target. +-- @param #string time (Optional) Day time at which the group should start moving. Passed as a string in format "08:13:45". +-- @param #number speed (Optinal) Speed in km/h the group should move at. Default 50 km/h. +-- @param #boolean onroad (Optional) If true, group will mainly use roads. Default off, i.e. go directly towards the specified coordinate. +-- @param #boolean cancel (Optional) If true, cancel any running attack when move should begin. Default is false. +-- @param #string name (Optional) Name of the coordinate. Default is LL DMS string of the coordinate. If the name was already given, the numbering "#01", "#02",... is appended automatically. +-- @return #string Name of the move. Can be used for further reference, e.g. deleting the move from the list. +function ARTY:AssignMoveCoord(coord, time, speed, onroad, cancel, name) + self:F({coord=coord, time=time, speed=speed, onroad=onroad, cancel=cancel, name=name}) + + -- Name of the target. + local _name=name or coord:ToStringLLDMS() + + -- Check if the name has already been used for another target. If so, the function returns a new unique name. + _name=self:_CheckName(self.moves, _name) + + -- Default is current time if no time was specified. + time=time or self:_SecondsToClock(timer.getAbsTime()) + + -- Default speed is 50 km/h. + speed=speed or 50 + + -- Default is off road. + if onroad==nil then + onroad=false + end + + -- Default is not to cancel a running attack. + if cancel==nil then + cancel=false + end + + -- Time in seconds. + local _time=self:_ClockToSeconds(time) + + -- Prepare move array. + local _move={name=_name, coord=coord, time=_time, speed=speed, onroad=onroad, cancel=cancel} + + -- Add to table. + table.insert(self.moves, _move) + end --- Set minimum firing range. Targets closer than this distance are not engaged. @@ -634,7 +687,8 @@ function ARTY:onafterStart(Controllable, From, Event, To) text=text..string.format("Type = %s\n", self.Type) text=text..string.format("Display Name = %s\n", self.DisplayName) text=text..string.format("Number of units = %d\n", self.IniGroupStrength) - text=text..string.format("Max Speed = %d km/h\n", self.Speed) + text=text..string.format("Speed max = %d km/h\n", self.SpeedMax) + text=text..string.format("Speed default = %d km/h\n", self.Speed) text=text..string.format("Min range = %d km\n", self.minrange/1000) text=text..string.format("Max range = %d km\n", self.maxrange/1000) text=text..string.format("Total ammo count = %d\n", self.Nammo0) @@ -659,6 +713,12 @@ function ARTY:onafterStart(Controllable, From, Event, To) for _, target in pairs(self.targets) do text=text..string.format("- %s\n", self:_TargetInfo(target)) end + text=text..string.format("Moves:\n") + for i=1,#self.moves do + local _move=self.moves[i] + local _clock=tostring(self:_SecondsToClock(_move.time)) + text=text..string.format("- %s: time=%s, speed=%d, onroad=%s, cancel=%s\n", _move.name, _clock, _move.speed, tostring(_move.onroad), tostring(_move.cancel)) + end text=text..string.format("******************************************************\n") text=text..string.format("Shell types:\n") for _,_type in pairs(self.ammoshells) do @@ -1127,17 +1187,22 @@ function ARTY:onafterRearm(Controllable, From, Event, To) self.RearmingGroupCoord=coordRARM end - if self.RearmingGroup and self.RearmingPlaceCoord and self.Speed>0 then + if self.RearmingGroup and self.RearmingPlaceCoord and self.SpeedMax>0 then -- CASE 1: Rearming unit and ARTY group meet at rearming place. + -- Send message. + local text=string.format("%s, %s, request rearming at rearming place.", Controllable:GetName(), self.RearmingGroup:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + -- Distances. local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord) -- Route ARTY group to rearming place. if dA > self.RearmingDistance then - self:Move(self:_VicinityCoord(self.RearmingPlaceCoord, self.RearmingDistance/4, self.RearmingDistance/2), self.RearmingArtyOnRoad) + self:Move(self:_VicinityCoord(self.RearmingPlaceCoord, self.RearmingDistance/4, self.RearmingDistance/2), self.Speed, self.RearmingArtyOnRoad) end -- Route Rearming group to rearming place. @@ -1167,14 +1232,18 @@ function ARTY:onafterRearm(Controllable, From, Event, To) elseif self.RearmingPlaceCoord then -- CASE 3: ARTY drives to rearming place. + + -- Send message. + local text=string.format("%s, moving to rearming place.", Controllable:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) -- Distance. local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) -- Route ARTY group to rearming place. if dA > self.RearmingDistance then - --self:_Move(self.Controllable, self.RearmingPlaceCoord, self.Speed, true) - self:Move(self:_VicinityCoord(self.RearmingPlaceCoord), false) + self:Move(self:_VicinityCoord(self.RearmingPlaceCoord), self.Speed, self.RearmingArtyOnRoad) end end @@ -1200,7 +1269,7 @@ function ARTY:onafterRearmed(Controllable, From, Event, To) -- Route ARTY group back to where it came from (if distance is > 100 m). local d1=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) if d1 > self.RearmingDistance then - self:Move(self.InitialCoord, false) + self:Move(self.InitialCoord, self.Speed, self.RearmingArtyOnRoad) end -- Route unit back to where it came from (if distance is > 100 m). @@ -1266,7 +1335,7 @@ function ARTY:onbeforeMove(Controllable, From, Event, To, ToCoord, OnRoad) self:_EventFromTo("onbeforeMove", Event, From, To) -- Check if group can actually move... - if self.Speed==0 then + if self.SpeedMax==0 then return false end @@ -1285,16 +1354,20 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param Wrapper.Point#COORDINATE ToCoord Coordinate to which the ARTY group should move. +-- @param #number Speed Speed in km/h at which the grou p should move. -- @param #boolean OnRoad If true group should move on road mainly. -function ARTY:onafterMove(Controllable, From, Event, To, ToCoord, OnRoad) +function ARTY:onafterMove(Controllable, From, Event, To, ToCoord, Speed, OnRoad) self:_EventFromTo("onafterMove", Event, From, To) -- Set alarm state to green and ROE to weapon hold. self.Controllable:OptionAlarmStateGreen() self.Controllable:OptionROEHoldFire() + + -- Take care of max speed. + local _Speed=math.min(Speed, self.SpeedMax) -- Route group to coodinate. - self:_Move(self.Controllable, ToCoord, self.Speed, OnRoad) + self:_Move(self.Controllable, ToCoord, _Speed, OnRoad) end @@ -1325,7 +1398,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #table target Array holding the target info. +-- @param #table target Array holding the target parameters. -- @return #boolean If true, proceed to onafterOpenfire. function ARTY:onafterNewTarget(Controllable, From, Event, To, target) self:_EventFromTo("onafterNewTarget", Event, From, To) @@ -1336,6 +1409,24 @@ function ARTY:onafterNewTarget(Controllable, From, Event, To, target) self:T(ARTY.id..text) end +--- After "NewMove" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table move Array holding the move parameters. +-- @return #boolean If true, proceed to onafterOpenfire. +function ARTY:onafterNewMove(Controllable, From, Event, To, move) + self:_EventFromTo("onafterNewTarget", Event, From, To) + + -- Debug message. + local text=string.format("Adding new move %s.", move.name) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T(ARTY.id..text) +end + + --- After "Dead" event, when a unit has died. When all units of a group are dead trigger "Stop" event. -- @param #ARTY self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -1724,12 +1815,13 @@ function ARTY:_GetTargetByName(name) end ---- Get the weapon type name, which should be used to attack the target. +--- Check if a name is unique. If not, a new unique name is created by adding a running index #01, #02, ... -- @param #ARTY self --- @param #string name Desired target name. +-- @param #table givennames Table with entries of already given names. Must contain a .name item. +-- @param #string name Desired name. -- @return #string Unique name, which is not already given for another target. -function ARTY:_CheckTargetName(name) - self:F2(name) +function ARTY:_CheckName(givennames, name) + self:F2({givennames=givennames, name=name}) local newname=name local counter=1 @@ -1739,12 +1831,12 @@ function ARTY:_CheckTargetName(name) local unique=true -- Loop over all targets already defined. - for _,_target in pairs(self.targets) do + for _,_target in pairs(givennames) do -- Target name. - local _targetname=_target.name + local _givenname=givennames.name - if _targetname==newname then + if _givenname==newname then -- Define new name = "name #01" newname=string.format("%s #%02d", name, counter) @@ -1968,6 +2060,9 @@ function ARTY:_Move(group, ToCoord, Speed, OnRoad) -- Set formation. local formation = "Off road" + -- Default speed is 30 km/h. + Speed=Speed or 30 + -- Current coordinates of group. local cpini=group:GetCoordinate() From 3157a63b7e817d23c684167d7a00b667985d5ffc Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Sun, 6 May 2018 20:40:14 +0200 Subject: [PATCH 24/31] ARTY v0.8.8 Added working implementation for moves. Other minor adjustments. --- .../Moose/Functional/Artillery.lua | 190 +++++++++++++++--- 1 file changed, 157 insertions(+), 33 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 36a847036..ee6ab8178 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -47,6 +47,7 @@ -- @field #table targets All targets assigned. -- @field #table moves All moves assigned. -- @field #table currentTarget Holds the current target, if there is one assigned. +-- @field #table currentMove Holds the current commanded move, if there is one assigned. -- @field #number Nammo0 Initial amount total ammunition (shells+rockets+missiles) of the whole group. -- @field #number Nshells0 Initial amount of shells of the whole group. -- @field #number Nrockets0 Initial amount of rockets of the whole group. @@ -225,11 +226,12 @@ -- -- @field #ARTY ARTY={ - ClassName = "ARTY", - Debug = true, - targets = {}, - moves = {}, - currentTarget = nil, + ClassName="ARTY", + Debug=true, + targets={}, + moves={}, + currentTarget=nil, + currentMove=nil, Nammo0=0, Nshells0=0, Nrockets0=0, @@ -276,7 +278,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #number version -ARTY.version="0.8.7" +ARTY.version="0.8.8" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -296,7 +298,7 @@ ARTY.version="0.8.7" -- DONE: Add pseudo user transitions. OnAfter... -- DONE: Make reaming unit a group. -- TODO: Adjust documenation again. --- TODO: Add command move to make arty group move. +-- DONE: Add command move to make arty group move. -- DONE: remove schedulers for status event. -- TODO: Improve handling of special weapons. When winchester? @@ -606,12 +608,12 @@ function ARTY:SetDebugOFF() self.Debug=false end ---- Delete target from target list. +--- Delete a target from target list. -- @param #ARTY self -- @param #string name Name of the target. function ARTY:RemoveTarget(name) self:F2(name) - local id=self:_GetTargetByName(name) + local id=self:_GetTargetIndexByName(name) if id then self:T(ARTY.id..string.format("Group %s: Removing target %s (id=%d).", self.Controllable:GetName(), name, id)) table.remove(self.targets, id) @@ -619,6 +621,19 @@ function ARTY:RemoveTarget(name) self:T(ARTY.id..string.format("Group %s: Number of targets = %d.", self.Controllable:GetName(), #self.targets)) end +--- Delete a move from move list. +-- @param #ARTY self +-- @param #string name Name of the target. +function ARTY:RemoveMove(name) + self:F2(name) + local id=self:_GetMoveIndexByName(name) + if id then + self:T(ARTY.id..string.format("Group %s: Removing move %s (id=%d).", self.Controllable:GetName(), name, id)) + table.remove(self.moves, id) + end + self:T(ARTY.id..string.format("Group %s: Number of moves = %d.", self.Controllable:GetName(), #self.moves)) +end + --- Delete ALL targets from current target list. -- @param #ARTY self function ARTY:RemoveAllTargets() @@ -679,7 +694,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) MESSAGE:New(text, 10):ToAllIf(self.Debug) -- Get Ammo. - self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0=self:GetAmmo(self.Controllable, self.Debug) + self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0=self:GetAmmo(self.Debug) local text=string.format("\n******************************************************\n") text=text..string.format("Arty group = %s\n", Controllable:GetName()) @@ -715,9 +730,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) end text=text..string.format("Moves:\n") for i=1,#self.moves do - local _move=self.moves[i] - local _clock=tostring(self:_SecondsToClock(_move.time)) - text=text..string.format("- %s: time=%s, speed=%d, onroad=%s, cancel=%s\n", _move.name, _clock, _move.speed, tostring(_move.onroad), tostring(_move.cancel)) + text=text..string.format("- %s\n", self:_MoveInfo(self.moves[i])) end text=text..string.format("******************************************************\n") text=text..string.format("Shell types:\n") @@ -748,7 +761,7 @@ end function ARTY:_StatusReport() -- Get Ammo. - local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo(self.Controllable) + local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() local Tnow=timer.getTime() local text=string.format("\n******************* STATUS ***************************\n") @@ -766,8 +779,12 @@ function ARTY:_StatusReport() end text=text..string.format("Nshots curr. Target = %d\n", self.Nshots) text=text..string.format("Targets:\n") - for _, target in pairs(self.targets) do - text=text..string.format("- %s\n", self:_TargetInfo(target)) + for i=1,#self.targets do + text=text..string.format("- %s\n", self:_TargetInfo(self.targets[i])) + end + text=text..string.format("Moves:\n") + for i=1,#self.moves do + text=text..string.format("- %s\n", self:_MoveInfo(self.moves[i])) end text=text..string.format("******************************************************") env.info(ARTY.id..text) @@ -812,7 +829,7 @@ function ARTY:_OnEventShot(EventData) MESSAGE:New(text, 5):ToAllIf(self.Debug) -- Get current ammo. - local _nammo,_nshells,_nrockets,_nmissiles=self:GetAmmo(self.Controllable) + local _nammo,_nshells,_nrockets,_nmissiles=self:GetAmmo() if _nammo==0 then @@ -960,9 +977,18 @@ function ARTY:onafterStatus(Controllable, From, Event, To) -- Get a valid normal target (one that is not timed). local _normalTarget=self:_CheckNormalTargets() + + -- Get a commaned move to another location. + local _move=self:_CheckMoves() -- Group is combat ready or firing but we have a high prio timed target. - if self:is("CombatReady") or (self:is("Firing") and _timedTarget) then + if (self:is("CombatReady") or self:is("Firing")) and _move then + + -- Command to move. + self.currentMove=_move + self:Move(_move.coord, _move.speed, _move.onroad) + + elseif self:is("CombatReady") or (self:is("Firing") and _timedTarget) then env.info(string.format("FF: Combatready or firing and high prio timed target.")) -- Engage target. @@ -1048,7 +1074,7 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) --_coord:MarkToAll("Arty Target") -- Get target array index. - local id=self:_GetTargetByName(target.name) + local id=self:_GetTargetIndexByName(target.name) -- Target is now under fire and has been engaged once more. if id then @@ -1062,9 +1088,36 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) -- Distance to target local range=Controllable:GetCoordinate():Get2DDistance(target.coord) + + -- Get ammo. + local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() + local nfire=Nammo + local _type="shots" + if self.WeaponType==ARTY.WeaponType.Auto then + nfire=Nammo + _type="shots" + elseif self.WeaponType==ARTY.WeaponType.Cannon then + nfire=Nshells + _type="shells" + elseif self.WeaponType==ARTY.WeaponType.Rockets then + nfire=Nrockets + _type="rockets" + elseif self.WeaponType==ARTY.WeaponType.UnguidedAny then + nfire=Nshells+Nrockets + _type="shells or rockets" + elseif self.WeaponType==ARTY.WeaponType.GuidedMissile then + nfire=Nmissiles + _type="missiles" + elseif self.WeaponType==ARTY.WeaponType.CruiseMissile then + nfire=Nmissiles + _type="cruise missiles" + end + + -- Adjust if less than requested ammo is left. + local _n=math.min(target.nshells, nfire) -- Send message. - local text=string.format("%s, opening fire on target %s with %s shells. Distance %.1f km.", Controllable:GetName(), target.name, target.nshells, range/1000) + local text=string.format("%s, opening fire on target %s with %d %s. Distance %.1f km.", Controllable:GetName(), target.name, _n, _type, range/1000) self:T(ARTY.id..text) MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) @@ -1093,7 +1146,7 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) -- Get target array index. - local id=self:_GetTargetByName(target.name) + local id=self:_GetTargetIndexByName(target.name) -- Increase engaged counter if id then @@ -1289,7 +1342,7 @@ function ARTY:_CheckRearmed() self:F2() -- Get current ammo. - local nammo,nshells,nrockets,nmissiles=self:GetAmmo(self.Controllable) + local nammo,nshells,nrockets,nmissiles=self:GetAmmo() -- Number of units still alive. local units=self.Controllable:GetUnits() @@ -1353,7 +1406,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Wrapper.Point#COORDINATE ToCoord Coordinate to which the ARTY group should move. +-- @param Core.Point#COORDINATE ToCoord Coordinate to which the ARTY group should move. -- @param #number Speed Speed in km/h at which the grou p should move. -- @param #boolean OnRoad If true group should move on road mainly. function ARTY:onafterMove(Controllable, From, Event, To, ToCoord, Speed, OnRoad) @@ -1365,6 +1418,11 @@ function ARTY:onafterMove(Controllable, From, Event, To, ToCoord, Speed, OnRoad) -- Take care of max speed. local _Speed=math.min(Speed, self.SpeedMax) + + -- Smoke coordinate + if self.Debug then + ToCoord:SmokeRed() + end -- Route group to coodinate. self:_Move(self.Controllable, ToCoord, _Speed, OnRoad) @@ -1388,6 +1446,12 @@ function ARTY:onafterArrived(Controllable, From, Event, To) self:T(ARTY.id..text) MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + -- Remove executed move from queue. + if self.currentMove then + self:RemoveMove(self.currentMove.name) + self.currentMove=nil + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1604,6 +1668,36 @@ function ARTY:_CheckTimedTargets() return nil end +--- Check all moves and return the one which should be executed next. +-- @param #ARTY self +-- @return #table Move which is due. +function ARTY:_CheckMoves() + self:F3() + + -- Current time. + local Tnow=timer.getAbsTime() + + -- Sort Targets wrt time. + self:_SortQueueTime(self.moves) + + -- Check if we are currently firing. + local firing=false + if self.currentTarget then + firing=true + end + + for i=1,#self.moves do + local _move=self.moves[i] + + -- Check if time for move is reached. + if Tnow >= _move.time and (firing==false or _move.cancel) then + return _move + end + end + + return nil +end + --- Check all normal (untimed) targets and return the target with the highest priority which has been engaged the fewest times. -- @param #ARTY self -- @return #table Target which is due to be attacked now or nil if no target could be found. @@ -1634,15 +1728,15 @@ end --- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. -- @param #ARTY self --- @param Wrapper.Controllable#CONTROLLABLE controllable Controllable for which the ammo is counted. -- @param #boolean display Display ammo table as message to all. Default false. -- @return #number Total amount of ammo the whole group has left. -- @return #number Number of shells the group has left. -- @return #number Number of rockets the group has left. -- @return #number Number of missiles the group has left. -function ARTY:GetAmmo(controllable, display) - self:F3({controllable=controllable, display=display}) +function ARTY:GetAmmo(display) + self:F3({display=display}) + -- Default is display false. if display==nil then display=false end @@ -1654,7 +1748,7 @@ function ARTY:GetAmmo(controllable, display) local nmissiles=0 -- Get all units. - local units=controllable:GetUnits() + local units=self.Controllable:GetUnits() if units==nil then return nammo, nshells, nrockets, nmissiles end @@ -1744,10 +1838,10 @@ function ARTY:GetAmmo(controllable, display) end end + -- Debug text and send message. self:T2(ARTY.id..text) MESSAGE:New(text, 10):ToAllIf(display) - - + end end @@ -1795,11 +1889,11 @@ function ARTY:_CheckShootingStarted() end end ---- Get a target by its name. +--- Get the index of a target by its name. -- @param #ARTY self -- @param #string name Name of target. -- @return #number Arrayindex of target. -function ARTY:_GetTargetByName(name) +function ARTY:_GetTargetIndexByName(name) self:F2(name) for i=1,#self.targets do @@ -1814,6 +1908,26 @@ function ARTY:_GetTargetByName(name) return nil end +--- Get the index of a move by its name. +-- @param #ARTY self +-- @param #string name Name of move. +-- @return #number Arrayindex of move. +function ARTY:_GetMoveIndexByName(name) + self:F2(name) + + for i=1,#self.moves do + local movename=self.moves[i].name + if movename==name then + self:T2(ARTY.id..string.format("Found move with name %s. Index = %d", name, i)) + return i + end + end + + self:E(ARTY.id..string.format("ERROR: Move with name %s could not be found!", name)) + return nil +end + + --- Check if a name is unique. If not, a new unique name is created by adding a running index #01, #02, ... -- @param #ARTY self @@ -1959,17 +2073,27 @@ function ARTY:_split(str, sep) return result end ---- Returns the target info as formatted string. +--- Returns the target parameters as formatted string. -- @param #ARTY self -- @return #string name, prio, radius, nshells, engaged, maxengage, time, weapontype function ARTY:_TargetInfo(target) local clock=tostring(self:_SecondsToClock(target.time)) local weapon=self:_WeaponTypeName(target.weapontype) local _underfire=tostring(target.underfire) - return string.format("%s, prio=%d, radius=%d, nshells=%d, engaged=%d/%d, weapontype=%s, time=%s, underfire=%s", + return string.format("%s: prio=%d, radius=%d, nshells=%d, engaged=%d/%d, weapontype=%s, time=%s, underfire=%s", target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, weapon, clock,_underfire) end +--- Returns a formatted string with information about all move parameters. +-- @param #ARTY self +-- @param #table move Move table item. +-- @return #string Info string. +function ARTY:_MoveInfo(move) + self:F3(move) + local _clock=self:_SecondsToClock(move.time) + return string.format("%s: time=%s, speed=%d, onroad=%s, cancel=%s", move.name, _clock, move.speed, tostring(move.onroad), tostring(move.cancel)) +end + --- Convert time in seconds to hours, minutes and seconds. -- @param #ARTY self -- @param #number seconds Time in seconds. From 6554fa55d16dfc85352b4bf1f39f90d26346aab4 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Mon, 7 May 2018 00:20:19 +0200 Subject: [PATCH 25/31] ARTY v0.8.8 Minor Changes. --- Moose Development/Moose/Functional/Artillery.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index ee6ab8178..227f67413 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -763,9 +763,11 @@ function ARTY:_StatusReport() -- Get Ammo. local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() local Tnow=timer.getTime() + local Clock=self:_SecondsToClock(timer.getAbsTime()) local text=string.format("\n******************* STATUS ***************************\n") text=text..string.format("ARTY group = %s\n", self.Controllable:GetName()) + text=text..string.format("Clock = %s\n", Clock) text=text..string.format("FSM state = %s\n", self:GetState()) text=text..string.format("Total ammo count = %d\n", Nammo) text=text..string.format("Number of shells = %d\n", Nshells) @@ -1469,7 +1471,7 @@ function ARTY:onafterNewTarget(Controllable, From, Event, To, target) -- Debug message. local text=string.format("Adding new target %s.", target.name) - MESSAGE:New(text, 30):ToAllIf(self.Debug) + --MESSAGE:New(text, 30):ToAllIf(self.Debug) self:T(ARTY.id..text) end From 92c3b530cce1792c3e0ceadbcfda81079c2db2b2 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Mon, 7 May 2018 17:33:48 +0200 Subject: [PATCH 26/31] ARTY v0.8.9 Improved documentation. --- .../Moose/Functional/Artillery.lua | 123 ++++++++++++++++-- 1 file changed, 114 insertions(+), 9 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 227f67413..2edf7dd89 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -7,7 +7,7 @@ -- -- ==== -- --- The ARTY class can be used to easily assign targets for artillery units. +-- The ARTY class can be used to easily assign and manage targets for artillery units. -- -- ## Features: -- @@ -16,7 +16,8 @@ -- * Engagements can be scheduled, i.e. will be executed at a certain time of the day. -- * Special weapon types can be selected for each attack, e.g. cruise missiles for Naval units. -- * Automatic rearming once the artillery is out of ammo. --- * Finite state machine implementation. User can interact when certain events occur. +-- * New targets can be added during the mission, e.g. when they are detected by recon units. +-- * Finite state machine implementation. Mission designer can interact when certain events occur. -- -- ==== -- @@ -93,15 +94,17 @@ -- -- ![Process](..\Presentations\ARTY\ARTY_Process.png) -- +-- ### Blue Branch -- After the FMS process is started the ARTY group will be in the state **CombatReady**. Once a target is assigned the **OpenFire** event will be triggered and the group starts -- firing. At this point the group in in the state **Firing**. --- -- When the defined number of shots has been fired on the current target the event **CeaseFire** is triggered. The group will stop firing and go back to the state **CombatReady**. -- If another target is defined (or multiple engagements of the same target), the cycle starts anew. -- +-- ### Violet Branch -- When the ARTY group runs out of ammunition, the event **Winchester** is triggered and the group enters the state **OutOfAmmo**. -- In this state, the group is unable to engage further targets. -- +-- ### Red Branch -- With the @{#ARTY.SetRearmingGroup}(*group*) command, a special group can be defined to rearm the ARTY group. If this unit has been assigned and the group has entered the state -- **OutOfAmmo** the event **Rearm** is triggered followed by a transition to the state **Rearming**. -- If the rearming group is less than 100 meters away from the ARTY group, the rearming process starts. If the rearming group is more than 100 meters away from the ARTY unit, the @@ -109,6 +112,16 @@ -- -- Once the rearming is complete, the **Rearmed** event is triggered and the group enters the state **CombatReady**. At this point targeted can be engaged again. -- +-- ### Green Branch +-- The ARTY group can be ordered to change its position via the @{#ARTY.AssignMoveCoord}() function as described below. When the group receives the command to move +-- the event **Move** is triggered and the state changes to **Moving**. When the unit arrives to its destination the event **Arrived** is triggered and the group +-- becomes **CombatReady** again. +-- +-- Note, that the ARTY group will not open fire while it is in state **Moving**. This property differentiates artillery from tanks. +-- +-- ### Yellow Branch +-- When a new target is assigned via the @{#ARTY.AssignTargetCoord}() function (see below), the **NewTarget** event is triggered. +-- -- ## Assigning Targets -- Assigning targets is a central point of the ARTY class. Multiple targets can be assigned simultanioulsly and are put into a queue. -- Of course, targets can be added at any time during the mission. For example, once they are detected by a reconnaissance unit. @@ -135,9 +148,9 @@ -- or the same target should be attacked two or more times with different parameters a suffix "#01", "#02", "#03" is automatically appended to the specified name. -- -- ## Target Queue --- In case, multiple targets have been defined, it is important to understand how the target queue works. +-- In case multiple targets have been defined, it is important to understand how the target queue works. -- --- Here, the important parameters are the priority *prio*, the number of engagements *maxengage* and the scheduled *time* as described above. +-- Here, the essential parameters are the priority *prio*, the number of engagements *maxengage* and the scheduled *time* as described above. -- -- For example, we have assigned two targets one with *prio*=10 and the other with *prio*=50 and both targets should be engaged three times (*maxengage*=3). -- Let's first consider the case that none of the targets is scheduled to be executed at a certain time (*time*=nil). @@ -146,7 +159,7 @@ -- have been engaged equally often, the target with the higher priority is engaged again. This coninues until a target has engaged three times. -- Once the maximum number of engagements is reached, the target is deleted from the queue. -- --- In other works, the queue is first sorted with respect to the number of engagements and targets with the same number of engagements are sorted with +-- In other words, the queue is first sorted with respect to the number of engagements and targets with the same number of engagements are sorted with -- respect to their priority. -- -- ### Timed Engagements @@ -198,6 +211,43 @@ -- * @{#ARTY.WeaponType}.GuidedMissile: Any guided missiles are used during the attack. Corresponding ammo type are missiles and can be defined by @{#ARTY.SetMissileTypes}. -- * @{#ARTY.WeaponType}.CruiseMissile: Only cruise missiles are used during the attack. Corresponding ammo type are missiles and can be defined by @{#ARTY.SetMissileTypes}. -- +-- ## Assigning Moves +-- The ARTY group can be commanded to move. This is done by the @{#ARTY.AssignMoveCoord}(*coord*,*time*,*speed*,*onroad*,*cancel*,*name*) function. +-- With this multiple timed moves of the group can be scheduled easily. By default, these moves will only be executed if the group is state **CombatReady**. +-- +-- ### Parameters +-- +-- * *coord*: Coordinates where the group should move to given as @{Point#COORDINATE} object. +-- * *time*: The time when the move should be executed. This has to be given as a string in the format "hh:mm:ss" (hh=hours, mm=minutes, ss=seconds). +-- * *speed*: Speed of the group in km/h. +-- * *onroad*: If this parameter is set to true, the group uses mainly roads to get to the commanded coordinates. +-- * *cancel*: If set to true, any current engagement of targets is cancelled at the time the move should be executed. +-- * *name*: Can be used to set a user defined name of the move. By default the name is created from the LL DMS coordinates. +-- +-- ## Automatic Rearming +-- +-- If an ARTY group runs out of ammunition, it can be rearmed automatically. +-- +-- ### Rearming Group +-- The first way to activate the automatic rearming is to define a rearming group with the function @{#ARTY.SetRearmingGroup}(*group*). For the blue side, this +-- could be a M181 transport truck and for the red side an Ural-375 truck. +-- +-- Once the ARTY group is out of ammo and the **Rearm** event is triggered, the defined rearming truck will drive to the ARTY group. +-- So the rearming truck does not have to be placed nearby the artillery group. When the rearming is complete, the rearming truck will drive back to its original position. +-- +-- ### Rearming Place +-- The second alternative is to define a rearming place, e.g. a FRAP, airport or any other warehouse. This is done with the function @{#ARTY.SetRearmingPlace}(*coord*). +-- The parameter *coord* specifies the coordinate of the rearming place which should not be further away then 100 meters from the warehouse. +-- +-- When the **Rearm** event is triggered, the ARTY group will move to the rearming place. Of course, the group must be mobil. So for a mortar this rearming procedure would not work. +-- +-- After the rearming is complete, the ARTY group will move back to its original position and resume normal operations. +-- +-- ### Rearming Group **and** Rearming Place +-- If both a rearming group *and* a rearming place are specified like described above, both the ARTY group and the rearming truck will move to the rearming place and meet there. +-- +-- After the rearming is complete, both groups will move back to their original positions. +-- -- ## Fine Tuning -- -- The mission designer has a few options to tailor the ARTY object according to his needs. @@ -215,13 +265,66 @@ -- ## Examples -- -- ### Assigning Multiple Targets --- This basic example illustrates how to assign multiple targets. +-- This basic example illustrates how to assign multiple targets and defining a rearming group. +-- -- Creat a new ARTY object from a Paladin group. +-- paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) +-- +-- -- Define a rearming group. This is a Transport M818 truck. +-- paladin:SetRearmingGroup(GROUP:FindByName("Blue Ammo Truck")) +-- +-- -- Set the max firing range. A Paladin unit has a range of 20 km. +-- paladin:SetMaxFiringRange(20) +-- +-- -- Low priorty (90) target, will be engage last. Target is engaged two times. At each engagement five shots are fired. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 3"):GetCoordinate(), 90, nil, 5, 2) +-- -- Medium priorty (nil=50) target, will be engage second. Target is engaged two times. At each engagement ten shots are fired. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), nil, nil, 10, 2) +-- -- High priorty (10) target, will be engage first. Target is engaged three times. At each engagement twenty shots are fired. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 2"):GetCoordinate(), 10, nil, 20, 3) +-- +-- -- Start ARTY process. +-- paladin:Start() +-- **Note** +-- +-- * If a parameter should be set to its default value, it has to be set to *nil* if other non-default parameters follow. Parameters at the end can simply be skiped. +-- * In this example, the target coordinates are taken from groups placed in the mission edit using the COORDINATE:GetCoordinate() function. -- -- ### Scheduled Engagements --- This example shows how to execute an engagement at a certain time. +-- -- Mission starts at 8 o'clock. +-- -- Assign two scheduled targets. +-- +-- -- Create ARTY object from Paladin group. +-- paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) +-- +-- -- Assign target coordinates. Priority=50 (medium), radius=100 m, use 5 shells per engagement, engage 1 time at two past 8 o'clock. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 50, 100, 5, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1") +-- +-- -- Assign target coordinates. Priority=10 (high), radius=300 m, use 10 shells per engagement, engage 1 time at seven past 8 o'clock. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 2"):GetCoordinate(), 10, 300, 10, 1, "08:07:00", ARTY.WeaponType.Auto, "Target 2") +-- +-- -- Start ARTY process. +-- paladin:Start() -- -- ### Specific Weapons -- This example demonstrates how to use specific weapons during an engagement. +-- -- Define the Normandy as ARTY object. +-- normandy=ARTY:New(GROUP:FindByName("Normandy")) +-- +-- -- Add target: prio=50, radius=300 m, number of missiles=20, number of engagements=1, start time=08:05 hours, only use cruise missiles for this attack. +-- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"), 20, 300, 50, 1, "08:01:00", ARTY.WeaponType.CruiseMissile) +-- +-- -- Add target: prio=50, radius=300 m, number of shells=100, number of engagements=1, start time=08:15 hours, only use cannons during this attack. +-- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"), 50, 300, 100, 1, "08:15:00", ARTY.WeaponType.Cannon) +-- +-- -- Define shells that are counted to check whether the ship is out of ammo. +-- -- Note that this is necessary because the Normandy has a lot of other shell type weapons which cannot be used to engage ground targets in an artillery style manner. +-- normandy:SetShellTypes({"MK45_127"}) +-- +-- -- Define missile types that are counted. +-- normandy:SetMissileTypes({"BGM"}) +-- +-- -- Start ARTY process. +-- normandy:Start() -- -- -- @field #ARTY @@ -278,7 +381,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #number version -ARTY.version="0.8.8" +ARTY.version="0.8.9" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -301,6 +404,8 @@ ARTY.version="0.8.8" -- DONE: Add command move to make arty group move. -- DONE: remove schedulers for status event. -- TODO: Improve handling of special weapons. When winchester? +-- TODO: Handle rearming for ships. +-- TODO: Make coordinate after rearming general, i.e. also work after the group has moved to anonther location. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- From 9429ec66cabe53f2af998eb6797a430b15cabc07 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Mon, 7 May 2018 17:44:53 +0200 Subject: [PATCH 27/31] RAT fixed typo --- Moose Development/Moose/Functional/RAT.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index c6c422798..8196a67f5 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -4265,10 +4265,10 @@ end function RAT:_CommandImmortal(group, switch) -- Command structure for setting groups to invisible. - local SetInvisible = {id = 'SetImmortal', params = {value = switch}} + local SetImmortal = {id = 'SetImmortal', params = {value = switch}} -- Execute command. - group:SetCommand(SetInvisible) + group:SetCommand(SetImmortal) end --- Adds a parking spot at an airport when it has been used by a spawned RAT aircraft to the RAT parking data base. From 95d7b8250d1a4f2c2437c9d8f3f897ba76df6cd9 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Mon, 7 May 2018 23:32:57 +0200 Subject: [PATCH 28/31] Minor Changes. --- Moose Development/Moose/Functional/Artillery.lua | 6 +++--- Moose Development/Moose/Wrapper/Group.lua | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 2edf7dd89..973b6ccbf 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -398,12 +398,12 @@ ARTY.version="0.8.9" -- DONE: Abort firing task if no shooting event occured with 5(?) minutes. Something went wrong then. Min/max range for example. -- DONE: Improve assigned time for engagement. Next day? -- DONE: Improve documentation. --- DONE: Add pseudo user transitions. OnAfter... +-- TODO: Add pseudo user transitions. OnAfter... -- DONE: Make reaming unit a group. --- TODO: Adjust documenation again. +-- DONE: Write documenation. -- DONE: Add command move to make arty group move. -- DONE: remove schedulers for status event. --- TODO: Improve handling of special weapons. When winchester? +-- TODO: Improve handling of special weapons. When winchester if using selected weapons? -- TODO: Handle rearming for ships. -- TODO: Make coordinate after rearming general, i.e. also work after the group has moved to anonther location. diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index db2092e9e..c595a5dbb 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -264,7 +264,6 @@ function GROUP:Destroy( GenerateEvent ) if self:IsAir() then self:CreateEventCrash( timer.getTime(), UnitData ) else - env.info("FF create event dead") self:CreateEventDead( timer.getTime(), UnitData ) end end From 191fcb25beb680abb574c622ea09481cf4388c1b Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 9 May 2018 00:11:53 +0200 Subject: [PATCH 29/31] PseudoATC v0.7.0 Pseudo ATC improvements. Minor changes in RAT and Suppression. --- .../Moose/Functional/PseudoATC.lua | 407 ++++++++++++------ Moose Development/Moose/Functional/RAT.lua | 6 +- .../Moose/Functional/Suppression.lua | 46 +- 3 files changed, 320 insertions(+), 139 deletions(-) diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua index d70e9d069..10b28166e 100644 --- a/Moose Development/Moose/Functional/PseudoATC.lua +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -7,7 +7,7 @@ -- -- The pseudo ATC enhances the standard DCS ATC functions. -- --- In particular, a menu entry "Pseudo ATC" is created in the special F10 menu. +-- In particular, a menu entry "Pseudo ATC" is created in the "F10 Other..." radiomenu. -- -- ## Features -- @@ -17,18 +17,17 @@ -- * Report absolute bearing and range to nearest airports. -- * Report current altitude AGL of own aircraft. -- * Upon request, ATC reports altitude until touchdown. --- * Pressure temperature, wind data and BR for mission waypoints. +-- * Report weather (pressure temperature, wind) and BR at players mission waypoints. -- * Works with static and dynamic weather. --- * All maps supported (Caucasus, NTTR, Normandy, and all future maps). --- * Multiplayer ready (?) (I suppose yes, but I don't have a server to test or debug. Jumping from client to client works.) +-- * Player can select the unit system (metric or imperial) in which data is reported. +-- * All maps supported (Caucasus, NTTR, Normandy, Persion Gulf and all future maps). -- --- Pressure units: hPa (european aircraft), mmHg (russian aircraft), inHg (american aircraft). --- -- ==== -- -- # Demo Missions -- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- ### [ALL Demo Missions of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master) +-- ### [ALL Demo Missions of the latest deveopment branch](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop) -- -- ==== -- @@ -40,7 +39,7 @@ -- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** -- --- ### Contributions: **Sven van de Velde ([FlightControl](https://forums.eagle.ru/member.php?u=89536))** +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536)** -- -- ==== -- @module PseudoATC @@ -49,9 +48,10 @@ --- PSEUDOATC class -- @type PSEUDOATC -- @field #string ClassName Name of the Class. +-- @field #table player Table comprising each player info. -- @field #boolean Debug If true, print debug info to dcs.log file. --- @field #table player Table comprising the player info. -- @field #number mdur Duration in seconds how low messages to the player are displayed. +-- @field #number mrefresh Interval in seconds after which the F10 menu is refreshed. E.g. by the closest airports. -- @field #boolean eventsmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. -- @extends Core.Base#BASE @@ -60,19 +60,19 @@ -- -- ## Scripting: -- --- Scripting is almost trivial. Just add the following line to your script: +-- Scripting is almost trivial. Just add the following two lines to your script: -- --- PSEUDOATC:Start() +-- pseudoATC=PSEUDOATC:New() +-- pseudoATC:Start() -- -- -- @field #PSEUDOATC PSEUDOATC={ ClassName = "PSEUDOATC", - Debug=true, player={}, - maxairport=10, + Debug=false, mdur=30, - mrefresh=120, + mrefresh=60, eventsmoose=true, } @@ -92,38 +92,93 @@ PSEUDOATC.unit={ PSEUDOATC.id="PseudoATC | " --- PSEUDOATC version. --- @field #list -PSEUDOATC.version={ - version = "0.6.0", - print = true, -} +-- @field #number version +PSEUDOATC.version="0.7.0" +----------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO list +-- TODO: Add takeoff event. +-- TODO: Add user functions. + +----------------------------------------------------------------------------------------------------------------------------------------- --- PSEUDOATC contructor. Starts the PseudoATC. -- @param #PSEUDOATC self -- @return #PSEUDOATC Returns a PSEUDOATC object. -function PSEUDOATC:Start() +function PSEUDOATC:New() -- Inherit BASE. local self=BASE:Inherit(self, BASE:New()) -- #PSEUDOATC -- Debug info - self:E(PSEUDOATC.id..string.format("Creating PseudoATC object. PseudoATC version %s", PSEUDOATC.version.version)) + self:E(PSEUDOATC.id..string.format("PseudoATC version %s", PSEUDOATC.version)) + + -- Return object. + return self +end + +--- PSEUDOATC contructor. Starts the PseudoATC. +-- @param #PSEUDOATC self +-- @return #PSEUDOATC Returns a PSEUDOATC object. +function PSEUDOATC:Start() + self:F() + + -- Debug info + self:E(PSEUDOATC.id.."Starting PseudoATC") -- Handle events. if self.eventsmoose then - self:HandleEvent(EVENTS.Birth, self._OnBirth) + self:T(PSEUDOATC.id.."Events are handled by MOOSE.") + self:HandleEvent(EVENTS.Birth, self._OnBirth) + self:HandleEvent(EVENTS.Land, self._PlayerLanded) + self:HandleEvent(EVENTS.Takeoff, self._PlayerTakeOff) self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) - --self:HandleEvent(EVENTS.PilotDead, self._PlayerLeft) - self:HandleEvent(EVENTS.Land, self._PlayerLanded) - --self:HandleEvent(EVENTS.Takeoff, self._PlayerTakeoff) + self:HandleEvent(EVENTS.Crash, self._PlayerLeft) + self:HandleEvent(EVENTS.Ejection, self._PlayerLeft) + --self:HandleEvent(EVENTS.PilotDead, self._PlayerLeft) else + self:T(PSEUDOATC.id.."Events are handled by DCS.") -- Events are handled directly by DCS. world.addEventHandler(self) end - -- Return object. - return self +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions + +--- Debug mode on. Send messages to everone. +-- @param #PSEUDOATC self +function PSEUDOATC:DebugOn() + self.Debug=true +end + +--- Debug mode off. This is the default setting. +-- @param #PSEUDOATC self +function PSEUDOATC:DebugOff() + self.Debug=false +end + +--- Set message duration how long messages are displayed. +-- @param #PSEUDOATC self +-- @param #number duration Time in seconds. Default is 30 sec. +function PSEUDOATC:SetMessageDuration(duration) + self.mdur=duration or 30 +end + +--- Set time interval after which the F10 radio menu is refreshed. +-- @param #PSEUDOATC self +-- @param #number interval Interval in seconds. Default is every 60 sec. +function PSEUDOATC:SetMessageDuration(interval) + self.mrefresh=interval or 60 +end + +--- Enable/disable event handling by MOOSE or DCS. +-- @param #PSEUDOATC self +-- @param #boolean switch If true, events are handled by MOOSE (default). If fase, events are handled directly by DCS. +function PSEUDOATC:SetMessageDuration(switch) + self.eventsmoose=switch end ----------------------------------------------------------------------------------------------------------------------------------------- @@ -163,20 +218,23 @@ function PSEUDOATC:onEvent(Event) end -- Event info. - if self.Debug then - env.info(PSEUDOATC.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) - env.info(PSEUDOATC.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) - env.info(PSEUDOATC.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) - env.info(PSEUDOATC.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) - env.info(PSEUDOATC.id..string.format("EVENT: Place = %s" , tostring(EventData.PlaceName))) - env.info(PSEUDOATC.id..string.format("EVENT: SubPlace = %s" , tostring(EventData.SubPlaceName))) - end + self:T3(PSEUDOATC.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) + self:T3(PSEUDOATC.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) + self:T3(PSEUDOATC.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) + self:T3(PSEUDOATC.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) + self:T3(PSEUDOATC.id..string.format("EVENT: Place = %s" , tostring(EventData.PlaceName))) + self:T3(PSEUDOATC.id..string.format("EVENT: SubPlace = %s" , tostring(EventData.SubPlaceName))) -- Event birth. if Event.id == world.event.S_EVENT_BIRTH and _playername then self:_OnBirth(EventData) end + -- Event takeoff. + if Event.id == world.event.S_EVENT_TAKEOFF and _playername then + self:_PlayerTakeOff(EventData) + end + -- Event land. if Event.id == world.event.S_EVENT_LAND and _playername and EventData.Place then self:_PlayerLanded(EventData) @@ -186,6 +244,26 @@ function PSEUDOATC:onEvent(Event) if Event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT and _playername then self:_PlayerLeft(EventData) end + + -- Event crash ==> player left unit + if Event.id == world.event.S_EVENT_CRASH and _playername then + self:_PlayerLeft(EventData) + end + + -- Event eject ==> player left unit + if Event.id == world.event.S_EVENT_EJECTION and _playername then + self:_PlayerLeft(EventData) + end + + -- Event pilot dead ==> player left unit + if Event.id == world.event.S_EVENT_PILOT_DEAD and _playername then + self:_PlayerLeft(EventData) + end + + -- Event pilot dead ==> player left unit + if Event.id == world.event.S_EVENT_PILOT_DEAD and _playername then + self:_PlayerLeft(EventData) + end end @@ -248,6 +326,28 @@ function PSEUDOATC:_PlayerLanded(EventData) end end +--- Function called by MOOSE/DCS event handler when a player took off. +-- @param #PSEUDOATC self +-- @param Core.Event#EVENTDATA EventData +function PSEUDOATC:_PlayerTakeOff(EventData) + self:F({EventData=EventData}) + + -- Get unit, player and place. + local _unitName=EventData.IniUnitName + local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) + local _base=nil + local _baseName=nil + if EventData.place then + _base=EventData.place + _baseName=EventData.place:getName() + end + + -- Call take-off function. + if _unit and _playername and _base then + self:PlayerTakeOff(_unit, _baseName) + end +end + ----------------------------------------------------------------------------------------------------------------------------------------- -- Event Functions @@ -283,15 +383,15 @@ function PSEUDOATC:PlayerEntered(unit) -- Create main F10 menu, i.e. "F10/Pseudo ATC" self.player[GID].menu_main=missionCommands.addSubMenuForGroup(GID, "Pseudo ATC") - -- Create list of nearby airports. + -- Create/update list of nearby airports. self:LocalAirports(GID) - -- Create submenu My Positon. - self:MenuAircraft(GID) - - -- Create submenu airports. + -- Create submenu of local airports. self:MenuAirports(GID) + -- Create submenu Waypoints. + self:MenuWaypoints(GID) + -- Start scheduler to refresh the F10 menues. self.player[GID].scheduler, self.player[GID].schedulerid=SCHEDULER:New(nil, self.MenuRefresh, {self, GID}, self.mrefresh, self.mrefresh) @@ -319,7 +419,7 @@ function PSEUDOATC:PlayerLanded(unit, place) MESSAGE:New(text, 30):ToAllIf(self.Debug) -- Stop altitude reporting timer if its activated. - self:AltidudeStopTimer(id) + self:AltitudeTimerStop(id) -- Welcome message. if place then @@ -329,11 +429,40 @@ function PSEUDOATC:PlayerLanded(unit, place) end +--- Function called when a player took off. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT unit Unit of player which has landed. +-- @param #string place Name of the place the player landed at. +function PSEUDOATC:PlayerTakeOff(unit, place) + self:F2({unit=unit, place=place}) + + -- Gather some information. + local group=unit:GetGroup() + local id=group:GetID() + local PlayerName=self.player[id].playername + local Callsign=self.player[id].callsign + local UnitName=self.player[id].unitname + local GroupName=self.player[id].groupname + local CallSign=self.player[id].callsign + + -- Debug message. + local text=string.format("Player %s (%s) from group %s (ID %d) took off at %s", PlayerName, UnitName, GroupName, id, place) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + + -- Bye-Bye message. + if place then + local text=string.format("%s, %s, your are airborn. Have a save trip!", place, CallSign) + MESSAGE:New(text, self.mdur):ToGroup(group) + end + +end + --- Function called when a player leaves a unit or dies. -- @param #PSEUDOATC self -- @param Wrapper.Unit#UNIT unit Player unit which was left. function PSEUDOATC:PlayerLeft(unit) - self:F2({unit=unit}) + self:F({unit=unit}) -- Get id. local group=unit:GetGroup() @@ -348,10 +477,15 @@ function PSEUDOATC:PlayerLeft(unit) if self.player[id].schedulerid then self.player[id].scheduler:Stop(self.player[id].schedulerid) end - - -- Remove main menu. - missionCommands.removeItem(self.player[id].menu_main) + -- Stop scheduler for reporting alt if it runs. + self:AltitudeTimerStop(id) + + -- Remove main menu. + if self.player[id].menu_main then + missionCommands.removeItem(self.player[id].menu_main) + end + -- Remove player array. self.player[id]=nil end @@ -375,12 +509,13 @@ function PSEUDOATC:MenuRefresh(id) -- Create list of nearby airports. self:LocalAirports(id) - - -- Create submenu My Positon. - --self:MenuAircraft(id) - - -- Create submenu airports. + + -- Create submenu Local Airports. self:MenuAirports(id) + + -- Create submenu Waypoints etc. + self:MenuWaypoints(id) + end @@ -395,47 +530,49 @@ function PSEUDOATC:MenuClear(id) self:T(PSEUDOATC.id..text) MESSAGE:New(text,30):ToAllIf(self.Debug) - + -- Delete Airports menu. if self.player[id].menu_airports then missionCommands.removeItemForGroup(id, self.player[id].menu_airports) - --[[ - for name,item in pairs(self.player[id].menu_airports) do - - -- Debug message. - self:E(PSEUDOATC.id..string.format("Deleting menu item %s for ID %d", name, id)) - - -- Remove menu item. - missionCommands.removeItemForGroup(id, self.player[id].menu_airports[name]) - end - ]] + self.player[id].menu_airports=nil else self:T2(PSEUDOATC.id.."No airports to clear menus.") end - -- Remove - if self.player[id].menu_aircraft then - missionCommands.removeItemForGroup(id, self.player[id].menu_aircraft.main) + -- Delete waypoints menu. + if self.player[id].menu_waypoints then + missionCommands.removeItemForGroup(id, self.player[id].menu_waypoints) + self.player[id].menu_waypoints=nil end - self.player[id].menu_airports=nil - --self.player[id].menu_aircraft=nil + -- Delete report alt until touchdown menu command. + if self.player[id].menu_reportalt then + missionCommands.removeItemForGroup(id, self.player[id].menu_reportalt) + self.player[id].menu_reportalt=nil + end + + -- Delete request current alt menu command. + if self.player[id].menu_requesttalt then + missionCommands.removeItemForGroup(id, self.player[id].menu_requestalt) + self.player[id].menu_requestalt=nil + end + end ---- Create "F10/Pseudo ATC" menu items "Airport Data". +--- Create "F10/Pseudo ATC/Local Airports" menu item. -- @param #PSEUDOATC self -- @param #number id Group id of player unit for which menues are created. function PSEUDOATC:MenuAirports(id) self:F(id) -- Table for menu entries. - self.player[id].menu_airports=missionCommands.addSubMenuForGroup(id, "Airports", self.player[id].menu_main) + self.player[id].menu_airports=missionCommands.addSubMenuForGroup(id, "Local Airports", self.player[id].menu_main) local i=0 for _,airport in pairs(self.player[id].airports) do i=i+1 - if i>self.maxairport then - break -- Max X<10 airports due to 10 menu items restriction. + if i > 10 then + break -- Max 10 airports due to 10 menu items restriction. end local name=airport.name @@ -444,14 +581,9 @@ function PSEUDOATC:MenuAirports(id) --F10menu_ATC_airports[ID][name] = missionCommands.addSubMenuForGroup(ID, name, F10menu_ATC) local submenu=missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_airports) - --self.player[id].menu_airports[name]=submenu -- Create menu reporting commands missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, name) - --missionCommands.addCommandForGroup(id, "Request QFE", submenu, self.ReportPressure, self, id, "QFE", pos, name) - --missionCommands.addCommandForGroup(id, "Request QNH", submenu, self.ReportPressure, self, id, "QNH", pos, name) - --missionCommands.addCommandForGroup(id, "Request Wind", submenu, self.ReportWind, self, id, pos, name) - --missionCommands.addCommandForGroup(id, "Request Temperature", submenu, self.ReportTemperature, self, id, pos, name) missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, name) -- Debug message. @@ -459,32 +591,28 @@ function PSEUDOATC:MenuAirports(id) end end ---- Create F10/Pseudo ATC menu item "My Plane". +--- Create F10/Pseudo ATC/Waypoints menu items and misc items. -- @param #PSEUDOATC self -- @param #number id Group id of player unit for which menues are created. -function PSEUDOATC:MenuAircraft(id) +function PSEUDOATC:MenuWaypoints(id) self:F(id) - -- Table for menu entries. - --self.player[id].menu_aircraft={} - + -- Player unit and callsign. local unit=self.player[id].unit --Wrapper.Unit#UNIT local callsign=self.player[id].callsign local name=string.format("My Aircraft (%s)", callsign) -- Debug info. - self:T(PSEUDOATC.id..string.format("Creating menu item %s for ID %d", name,id)) - - -- F10/PseudoATC/My Aircraft (callsign) - --self.player[id].menu_aircraft.main = missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_main) - + self:T(PSEUDOATC.id..string.format("Creating waypoint menu for %s (ID %d).", name, id)) + if #self.player[id].waypoints>0 then - -- F10/PseudoATC/Waypoints + -- F10/PseudoATC/Waypoints self.player[id].menu_waypoints=missionCommands.addSubMenuForGroup(id, "Waypoints", self.player[id].menu_main) local j=0 for i, wp in pairs(self.player[id].waypoints) do + -- Increase counter j=j+1 @@ -501,23 +629,18 @@ function PSEUDOATC:MenuAircraft(id) -- Menu commands for each waypoint "F10/PseudoATC/My Aircraft (callsign)/Waypoints/Waypoint X/" missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, name) - --missionCommands.addCommandForGroup(id, "Request QFE", submenu, self.ReportPressure, self, id, "QFE", pos, pname) - --missionCommands.addCommandForGroup(id, "Request QNH", submenu, self.ReportPressure, self, id, "QNH", pos, pname) - --missionCommands.addCommandForGroup(id, "Request Wind", submenu, self.ReportWind, self, id, pos, pname) - --missionCommands.addCommandForGroup(id, "Request Temperature", submenu, self.ReportTemperature, self, id, pos, pname) missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, name) end end - missionCommands.addCommandForGroup(id, "Request current altitude AGL", self.player[id].menu_main, self.ReportHeight, self, id) - missionCommands.addCommandForGroup(id, "Report altitude until touchdown", self.player[id].menu_main, self.AltidudeStartTimer, self, id) - missionCommands.addCommandForGroup(id, "Quit reporting altitude", self.player[id].menu_main, self.AltidudeStopTimer, self, id) + self.player[id].menu_reportalt = missionCommands.addCommandForGroup(id, "Report alt until touchdown", self.player[id].menu_main, self.AltidudeTimerToggle, self, id) + self.player[id].menu_requestalt = missionCommands.addCommandForGroup(id, "Request altitude AGL", self.player[id].menu_main, self.ReportHeight, self, id) end ----------------------------------------------------------------------------------------------------------------------------------------- -- Reporting Functions ---- Weather Report. Report pressure QFE/QNH, temperature, wind at certain location +--- Weather Report. Report pressure QFE/QNH, temperature, wind at certain location. -- @param #PSEUDOATC self -- @param #number id Group id to which the report is delivered. -- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. @@ -533,7 +656,7 @@ function PSEUDOATC:ReportWeather(id, position, location) -- Get pressure in hPa. local Pqnh=position:GetPressure(0) -- Get pressure at sea level. local Pqfe=position:GetPressure() -- Get pressure at (land) height of position. - + -- Unit conversion. local _Pqnh=string.format("%.2f inHg", Pqnh * PSEUDOATC.unit.hPa2inHg) local _Pqfe=string.format("%.2f inHg", Pqfe * PSEUDOATC.unit.hPa2inHg) @@ -675,7 +798,7 @@ function PSEUDOATC:ReportWind(id, position, location) -- Formatted wind direction. local Ds = string.format('%03d°', Dir) - -- Settings. + -- Player settings. local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS -- Velocity in player units. @@ -732,7 +855,7 @@ end -- @param #number id Group id to the report is delivered. -- @param #number dt (Optional) Duration the message is displayed. -- @param #boolean _clear (Optional) Clear previouse messages. --- @return #number Altuitude above ground. +-- @return #number Altitude above ground. function PSEUDOATC:ReportHeight(id, dt, _clear) self:F({id=id, dt=dt}) @@ -743,43 +866,67 @@ function PSEUDOATC:ReportHeight(id, dt, _clear) -- Return height [m] above ground level. local function get_AGL(p) - local vec2={x=p.x,y=p.z} - local ground=land.getHeight(vec2) - local agl=p.y-ground + local agl=0 + if p then + local vec2={x=p.x,y=p.z} + local ground=land.getHeight(vec2) + local agl=p.y-ground + end return agl end -- Get height AGL. local unit=self.player[id].unit --Wrapper.Unit#UNIT - local position=unit:GetCoordinate() - local height=get_AGL(position) - local callsign=unit:GetCallsign() - -- Settings. - local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + if unit and unit:IsAlive() then - local Hs=string.format("%d m", height) - if settings:IsMetric() then - Hs=string.format("%d ft", height*PSEUDOATC.unit.meter2feet) + local position=unit:GetCoordinate() + local height=get_AGL(position) + local callsign=unit:GetCallsign() + + -- Settings. + local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + + local Hs=string.format("%d m", height) + if settings:IsMetric() then + Hs=string.format("%d ft", height*PSEUDOATC.unit.meter2feet) + end + + -- Message text. + local _text=string.format("%s: Your altitude is %s AGL.", callsign, Hs) + + -- Send message to player group. + --MESSAGE:New(text, dt):ToGroup(self.player[id].group) + self:_DisplayMessageToGroup(self.player[id].unit,_text, dt,_clear) + + -- Return height + return height end - -- Message text. - local _text=string.format("%s: Your altitude is %s AGL.", callsign, Hs) - - -- Send message to player group. - --MESSAGE:New(text, dt):ToGroup(self.player[id].group) - self:_DisplayMessageToGroup(self.player[id].unit,_text, dt,_clear) - - -- Return height - return height + return 0 end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Toggle report altitude reporting on/of. +-- @param #PSEUDOATC self. +-- @param #number id Group id of player unit. +function PSEUDOATC:AltidudeTimerToggle(id) + self:F(id) + + if self.player[id].altimerid then + -- If the timer is on, we turn it off. + self:AltitudeTimerStop(id) + else + -- If the timer is off, we turn it on. + self:AltitudeTimeStart(id) + end +end + --- Start altitude reporting scheduler. -- @param #PSEUDOATC self. -- @param #number id Group id of player unit. -function PSEUDOATC:AltidudeStartTimer(id) +function PSEUDOATC:AltitudeTimeStart(id) self:F(id) -- Debug info. @@ -787,13 +934,13 @@ function PSEUDOATC:AltidudeStartTimer(id) -- Start timer. --self.player[id].altimer=timer.scheduleFunction(self.ReportAltTouchdown, self, id, Tnow+2) - self.player[id].altimer, self.player[id].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, id, 0.1, true}, 1, 5) + self.player[id].altimer, self.player[id].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, id, 0.1, true}, 1, 3) end --- Stop/destroy DCS scheduler function for reporting altitude. -- @param #PSEUDOATC self. -- @param #number id Group id of player unit. -function PSEUDOATC:AltidudeStopTimer(id) +function PSEUDOATC:AltitudeTimerStop(id) -- Debug info. self:T(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", id)) @@ -863,14 +1010,24 @@ function PSEUDOATC:_GetPlayerUnitAndName(_unitName) self:F(_unitName) if _unitName ~= nil then + + -- Get DCS unit from its name. local DCSunit=Unit.getByName(_unitName) - local playername=DCSunit:getPlayerName() + if DCSunit then - - if DCSunit and playername then + -- Get the player name to make sure a player entered. + local playername=DCSunit:getPlayerName() local unit=UNIT:Find(DCSunit) - return unit, playername - end + + -- Debug output. + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + + if unit and playername then + -- Return MOOSE unit and player name + return unit, playername + end + + end end return nil,nil diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 8196a67f5..d5e78cdce 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -1280,7 +1280,7 @@ end --- Check if aircraft have accidentally been spawned on the runway. If so they will be removed immediatly. -- @param #RAT self --- @param #booblen switch If true, check is performed. If false, this check is omitted. +-- @param #boolean switch If true, check is performed. If false, this check is omitted. function RAT:CheckOnRunway(switch) self:F2(switch) if switch==nil then @@ -1291,7 +1291,7 @@ end --- Check if aircraft have accidentally been spawned on top of each other. If yes, they will be removed immediately. -- @param #RAT self --- @param #booblen switch If true, check is performed. If false, this check is omitted. +-- @param #boolean switch If true, check is performed. If false, this check is omitted. function RAT:CheckOnTop(switch) self:F2(switch) if switch==nil then @@ -1302,7 +1302,7 @@ end --- Put parking spot coordinates in a data base for future use of aircraft. -- @param #RAT self --- @param #booblen switch If true, parking spots are memorized. This is also the default setting. +-- @param #boolean switch If true, parking spots are memorized. This is also the default setting. function RAT:ParkingSpotDB(switch) self:F2(switch) if switch==nil then diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua index f3a4c4954..e9f42c626 100644 --- a/Moose Development/Moose/Functional/Suppression.lua +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -28,7 +28,7 @@ -- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** -- --- ### Contributions: **Sven van de Velde ([FlightControl](https://forums.eagle.ru/member.php?u=89536))** +-- ### Contributions: **[FlightControl](https://forums.eagle.ru/member.php?u=89536)** -- -- ==== -- @module Suppression @@ -45,7 +45,7 @@ -- @field #string Type Type of the group. -- @field #number SpeedMax Maximum speed of group in km/h. -- @field #boolean IsInfantry True if group has attribute Infantry. --- @field Core.Controllable#CONTROLLABLE Controllable Controllable of the FSM. Must be a ground group. +-- @field Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the FSM. Must be a ground group. -- @field #number Tsuppress_ave Average time in seconds a group gets suppressed. Actual value is sampled randomly from a Gaussian distribution. -- @field #number Tsuppress_min Minimum time in seconds the group gets suppressed. -- @field #number Tsuppress_max Maximum time in seconds the group gets suppressed. @@ -283,11 +283,16 @@ SUPPRESSION.MenuF10=nil -- @field #string id SUPPRESSION.id="SFX | " +--- PSEUDOATC version. +-- @field #number version +SUPPRESSION.version="0.7.0" + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---TODO: Figure out who was shooting and move away from him. ---TODO: Move behind a scenery building if there is one nearby. ---TODO: Retreat to a given zone or point. +--TODO list +--DONE: Figure out who was shooting and move away from him. +--DONE: Move behind a scenery building if there is one nearby. +--DONE: Retreat to a given zone or point. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -304,7 +309,7 @@ function SUPPRESSION:New(group) -- Check that group is present. if group then - self:T(SUPPRESSION.id.."Suppressive fire for group "..group:GetName()) + self:T(SUPPRESSION.id..string.format("SUPPRESSION version %s. Activating suppressive fire for group %s", SUPPRESSION.version, group:GetName())) else self:E(SUPPRESSION.id.."Suppressive fire: Requested group does not exist! (Has to be a MOOSE group.)") return nil @@ -312,7 +317,7 @@ function SUPPRESSION:New(group) -- Check that we actually have a GROUND group. if group:IsGround()==false then - self:E(SUPPRESSION.id.."SUPPRESSION fire group "..group:GetName().." has to be a GROUND group!") + self:E(SUPPRESSION.id..string.format("SUPPRESSION fire group %s has to be a GROUND group!", group:GetName())) return nil end @@ -326,7 +331,6 @@ function SUPPRESSION:New(group) -- Get max speed the group can do and convert to km/h. self.SpeedMax=self.DCSdesc.speedMaxOffRoad*3.6 - --self.SpeedMaxOffRoad=DCSdesc.speedMaxOffRoad -- Set speed to maximum. self.Speed=self.SpeedMax @@ -503,6 +507,7 @@ end -- @param #number Tmin (Optional) Minimum time [seconds] a group will be suppressed. Default is 5 seconds. -- @param #number Tmax (Optional) Maximum time a group will be suppressed. Default is 25 seconds. function SUPPRESSION:SetSuppressionTime(Tave, Tmin, Tmax) + self:F({Tave=Tave, Tmin=Tmin, Tmax=Tmax}) -- Minimum suppression time is input or default but at least 1 second. self.Tsuppress_min=Tmin or self.Tsuppress_min @@ -526,24 +531,28 @@ end -- @param #SUPPRESSION self -- @param Core.Zone#ZONE zone MOOSE zone object. function SUPPRESSION:SetRetreatZone(zone) + self:F({zone=zone}) self.RetreatZone=zone end --- Turn Debug mode on. Enables messages and more output to DCS log file. -- @param #SUPPRESSION self function SUPPRESSION:DebugOn() + self:F() self.Debug=true end --- Flare units when they are hit, die or recover from suppression. -- @param #SUPPRESSION self function SUPPRESSION:FlareOn() + self:F() self.flare=true end --- Smoke positions where units fall back to, hide or retreat. -- @param #SUPPRESSION self function SUPPRESSION:SmokeOn() + self:F() self.smoke=true end @@ -551,6 +560,7 @@ end -- @param #SUPPRESSION self -- @param #string formation Formation of the group. Default "Vee". function SUPPRESSION:SetFormation(formation) + self:F(formation) self.Formation=formation or "Vee" end @@ -558,6 +568,7 @@ end -- @param #SUPPRESSION self -- @param #number speed Speed in km/h of group. Default max speed the group can do. function SUPPRESSION:SetSpeed(speed) + self:F(speed) self.Speed=speed or self.SpeedMax self.Speed=math.min(self.Speed, self.SpeedMax) end @@ -566,6 +577,7 @@ end -- @param #SUPPRESSION self -- @param #boolean switch Enable=true or disable=false fall back of group. function SUPPRESSION:Fallback(switch) + self:F(switch) if switch==nil then switch=true end @@ -576,6 +588,7 @@ end -- @param #SUPPRESSION self -- @param #number distance Distance in meters. function SUPPRESSION:SetFallbackDistance(distance) + self:F(distance) self.FallbackDist=distance end @@ -583,6 +596,7 @@ end -- @param #SUPPRESSION self -- @param #number time Time in seconds. function SUPPRESSION:SetFallbackWait(time) + self:F(time) self.FallbackWait=time end @@ -590,6 +604,7 @@ end -- @param #SUPPRESSION self -- @param #boolean switch Enable=true or disable=false fall back of group. function SUPPRESSION:Takecover(switch) + self:F(switch) if switch==nil then switch=true end @@ -600,6 +615,7 @@ end -- @param #SUPPRESSION self -- @param #number time Time in seconds. function SUPPRESSION:SetTakecoverWait(time) + self:F(time) self.TakecoverWait=time end @@ -607,6 +623,7 @@ end -- @param #SUPPRESSION self -- @param #number range Search range in meters. function SUPPRESSION:SetTakecoverRange(range) + self:F(range) self.TakecoverRange=range end @@ -621,6 +638,7 @@ end -- @param #SUPPRESSION self -- @param #number probability Probability in percent. function SUPPRESSION:SetMinimumFleeProbability(probability) + self:F(probability) self.PminFlee=probability or 10 end @@ -628,6 +646,7 @@ end -- @param #SUPPRESSION self -- @param #number probability Probability in percent. function SUPPRESSION:SetMaximumFleeProbability(probability) + self:F(probability) self.PmaxFlee=probability or 90 end @@ -637,6 +656,7 @@ end -- @param #SUPPRESSION self -- @param #number damage Damage in percent. If group gets damaged above this value, the group will retreat. Default 50 %. function SUPPRESSION:SetRetreatDamage(damage) + self:F(damage) self.RetreatDamage=damage or 50 end @@ -644,6 +664,7 @@ end -- @param #SUPPRESSION self -- @param #number time Time in seconds. Default 7200 seconds = 2 hours. function SUPPRESSION:SetRetreatWait(time) + self:F(time) self.RetreatWait=time or 7200 end @@ -651,6 +672,7 @@ end -- @param #SUPPRESSION self -- @param #string alarmstate Alarm state. Possible "Auto", "Green", "Red". Default is "Auto". function SUPPRESSION:SetDefaultAlarmState(alarmstate) + self:F(alarmstate) if alarmstate:lower()=="auto" then self.DefaultAlarmState=SUPPRESSION.AlarmState.Auto elseif alarmstate:lower()=="green" then @@ -666,6 +688,7 @@ end -- @param #SUPPRESSION self -- @param #string roe ROE after suppression. Possible "Free", "Hold" or "Return". Default "Free". function SUPPRESSION:SetDefaultROE(roe) + self:F(roe) if roe:lower()=="free" then self.DefaultROE=SUPPRESSION.ROE.Free elseif roe:lower()=="hold" then @@ -681,6 +704,7 @@ end -- @param #SUPPRESSION self -- @param #boolean switch Enable=true or disable=false menu group. Default is true. function SUPPRESSION:MenuOn(switch) + self:F(switch) if switch==nil then switch=true end @@ -1298,7 +1322,7 @@ function SUPPRESSION:onEvent(Event) self:_OnEventHit(EventData) end - -- Event HIT + -- Event DEAD if Event.id == world.event.S_EVENT_DEAD then self:_OnEventDead(EventData) end @@ -1441,12 +1465,12 @@ end --- Make group run/drive to a certain point. We put in several intermediate waypoints because sometimes the group stops before it arrived at the desired point. --@param #SUPPRESSION self --@param Core.Point#COORDINATE fin Coordinate where we want to go. ---@param #number speed Speed of group. Default is 999. +--@param #number speed Speed of group. Default is 20. --@param #string formation Formation of group. Default is "Vee". --@param #number wait Time the group will wait/hold at final waypoint. Default is 30 seconds. function SUPPRESSION:_Run(fin, speed, formation, wait) - speed=speed or 999 + speed=speed or 20 formation=formation or "Vee" wait=wait or 30 From f9d7eea72115cf27cfa82109a9d1ce3e4105206e Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Wed, 9 May 2018 23:56:55 +0200 Subject: [PATCH 30/31] ARTY, PseudoATC, RANGE, RAT, SUPPRESSION ARTY v0.9.0: Added anti-ship missiles. Various fixes. PSEUDOATC v0.9.0: Added docu. Cleaned up code. Bug fixes. RANGE v1.1.1: Changed menu. RAT v2.2.2: Changed default setting to menu = off. Added user function to enable/disable menus. SUPPRESSION v0.9.0: Improvements. --- .../Moose/Functional/Artillery.lua | 98 +++-- .../Moose/Functional/PseudoATC.lua | 355 ++++++++---------- Moose Development/Moose/Functional/RAT.lua | 30 +- Moose Development/Moose/Functional/Range.lua | 5 +- .../Moose/Functional/Suppression.lua | 19 +- 5 files changed, 250 insertions(+), 257 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 973b6ccbf..418bb359a 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -23,19 +23,19 @@ -- -- # Demo Missions -- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- ### [MOOSE - ALL Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS) -- -- ==== -- -- # YouTube Channel -- --- ### [MOOSE YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) -- -- === -- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** -- --- ### Contributions: **[FlightControl](https://forums.eagle.ru/member.php?u=89536)** +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) -- -- ==== -- @module Arty @@ -311,10 +311,10 @@ -- normandy=ARTY:New(GROUP:FindByName("Normandy")) -- -- -- Add target: prio=50, radius=300 m, number of missiles=20, number of engagements=1, start time=08:05 hours, only use cruise missiles for this attack. --- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"), 20, 300, 50, 1, "08:01:00", ARTY.WeaponType.CruiseMissile) +-- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 20, 300, 50, 1, "08:01:00", ARTY.WeaponType.CruiseMissile) -- -- -- Add target: prio=50, radius=300 m, number of shells=100, number of engagements=1, start time=08:15 hours, only use cannons during this attack. --- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"), 50, 300, 100, 1, "08:15:00", ARTY.WeaponType.Cannon) +-- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 50, 300, 100, 1, "08:15:00", ARTY.WeaponType.Cannon) -- -- -- Define shells that are counted to check whether the ship is out of ammo. -- -- Note that this is necessary because the Normandy has a lot of other shell type weapons which cannot be used to engage ground targets in an artillery style manner. @@ -356,9 +356,12 @@ ARTY={ RearmingArtyOnRoad=false, InitialCoord=nil, report=true, - ammoshells={"weapons.shells"}, - ammorockets={"weapons.nurs"}, - ammomissiles={"weapons.missiles"}, + --ammoshells={"weapons.shells"}, + ammoshells={}, + --ammorockets={"weapons.nurs"}, + ammorockets={}, + --ammomissiles={"weapons.missiles"}, + ammomissiles={}, Nshots=0, minrange=500, maxrange=1000000, @@ -373,6 +376,7 @@ ARTY.WeaponType={ UnguidedAny=805339120, GuidedMissile=268402688, CruiseMissile=2097152, + AntiShipMissile=65536, } --- Some ID to identify who we are in output of the DCS.log file. @@ -381,7 +385,7 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #number version -ARTY.version="0.8.9" +ARTY.version="0.9.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -485,7 +489,6 @@ function ARTY:New(group) -- Red branch. self:AddTransition({"CombatReady", "OutOfAmmo"}, "Rearm", "Rearming") - self:AddTransition("Rearming", "Move", "Rearming") self:AddTransition("Rearming", "Rearmed", "Rearmed") -- Green branch. @@ -503,7 +506,8 @@ function ARTY:New(group) -- Unknown transitons. To be checked if adding these causes problems. self:AddTransition("Rearming", "Arrived", "Rearming") - + self:AddTransition("Rearming", "Move", "Rearming") + return self end @@ -968,15 +972,16 @@ function ARTY:_OnEventShot(EventData) elseif self.currentTarget.weapontype==ARTY.WeaponType.UnguidedAny and _nshells+_nrockets==0 then - self:T(ARTY.id.."Unguided weapon requested but shells and rockets empty.") + self:T(ARTY.id.."Unguided weapon requested but shells AND rockets empty.") self:CeaseFire(self.currentTarget) return - elseif (self.currentTarget.weapontype==ARTY.WeaponType.CruiseMissile or self.currentTarget.weapontype==ARTY.WeaponType.CruiseMissile) and _nmissiles==0 then + elseif (self.currentTarget.weapontype==ARTY.WeaponType.GuidedMissile or self.currentTarget.weapontype==ARTY.WeaponType.CruiseMissile or self.currentTarget.weapontype==ARTY.WeaponType.AntiShipMissile) and _nmissiles==0 then - self:T(ARTY.id.."Guided or Cruise missiles requested but all missiles empty.") + self:T(ARTY.id.."Guided, anti-ship or cruise missiles requested but all missiles empty.") self:CeaseFire(self.currentTarget) return + end -- Check if number of shots reached max. @@ -1214,11 +1219,14 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) _type="shells or rockets" elseif self.WeaponType==ARTY.WeaponType.GuidedMissile then nfire=Nmissiles - _type="missiles" + _type="guided missiles" elseif self.WeaponType==ARTY.WeaponType.CruiseMissile then nfire=Nmissiles _type="cruise missiles" - end + elseif self.WeaponType==ARTY.WeaponType.AntiShipMissile then + nfire=Nmissiles + _type="anti-ship missiles" + end -- Adjust if less than requested ammo is left. local _n=math.min(target.nshells, nfire) @@ -1877,6 +1885,11 @@ function ARTY:GetAmmo(display) self:T2(ARTY.id..string.format("Number of weapons %d.", weapons)) self:T2({ammotable=ammotable}) + self:T(ARTY.id.."Ammotable:") + for id,bla in pairs(ammotable) do + self:T({id=id, ammo=bla}) + end + -- Loop over all weapons. for w=1,weapons do @@ -1886,28 +1899,55 @@ function ARTY:GetAmmo(display) -- Typename of current weapon local Tammo=ammotable[w]["desc"]["typeName"] + -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3 + local Category=ammotable[w].desc.category + + local MissileCategory=nil + if Category==Weapon.Category.MISSILE then + MissileCategory=ammotable[w].desc.missileCategory + end + -- Check for correct shell type. local _gotshell=false - for _,_type in pairs(self.ammoshells) do - if string.match(Tammo, _type) then + if #self.ammoshells>0 then + -- User explicitly specified the valid type(s) of shells. + for _,_type in pairs(self.ammoshells) do + if string.match(Tammo, _type) then + _gotshell=true + end + end + else + if Category==Weapon.Category.SHELL then _gotshell=true end end -- Check for correct rocket type. local _gotrocket=false - for _,_type in pairs(self.ammorockets) do - if string.match(Tammo, _type) then - _gotrocket=true + if #self.ammorockets>0 then + for _,_type in pairs(self.ammorockets) do + if string.match(Tammo, _type) then + _gotrocket=true + end end + else + if Category==Weapon.Category.ROCKET then + _gotrocket=true + end end -- Check for correct missile type. local _gotmissile=false - for _,_type in pairs(self.ammomissiles) do - if string.match(Tammo,_type) then - _gotmissile=true + if #self.ammomissiles>0 then + for _,_type in pairs(self.ammomissiles) do + if string.match(Tammo,_type) then + _gotmissile=true + end end + else + if Category==Weapon.Category.ROCKET then + _gotmissile=true + end end -- We are specifically looking for shells or rockets here. @@ -1917,7 +1957,7 @@ function ARTY:GetAmmo(display) nshells=nshells+Nammo -- Debug info. - text=text..string.format("- %d shells of type %s\n", Nammo, Tammo) + text=text..string.format("- %d shells of type %s (category=%d mc=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) elseif _gotrocket then @@ -1925,7 +1965,7 @@ function ARTY:GetAmmo(display) nrockets=nrockets+Nammo -- Debug info. - text=text..string.format("- %d rockets of type %s\n", Nammo, Tammo) + text=text..string.format("- %d rockets of type %s (category=%d mc=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) elseif _gotmissile then @@ -1933,12 +1973,12 @@ function ARTY:GetAmmo(display) nmissiles=nmissiles+Nammo -- Debug info. - text=text..string.format("- %d missiles of type %s\n", name, Nammo, Tammo) + text=text..string.format("- %d missiles of type %s (category=%d mc=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) else -- Debug info. - text=text..string.format("- %d unknown ammo of type %s\n", Nammo, Tammo) + text=text..string.format("- %d unknown ammo of type %s (category=%d mc=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) end @@ -1946,7 +1986,7 @@ function ARTY:GetAmmo(display) end -- Debug text and send message. - self:T2(ARTY.id..text) + self:T(ARTY.id..text) MESSAGE:New(text, 10):ToAllIf(display) end diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua index 10b28166e..9726f1666 100644 --- a/Moose Development/Moose/Functional/PseudoATC.lua +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -1,5 +1,5 @@ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **Functional** - Pseudo ATC. +--- **Functional** - (R2.4) Rudimentary ATC. -- -- ![Banner Image](..\Presentations\PSEUDOATC\PSEUDOATC_Main.jpg) -- @@ -11,35 +11,31 @@ -- -- ## Features -- --- * Report QFE or QNH pressures at nearby airbases. --- * Report wind direction and strength at airbases. --- * Report temperature at airbases. --- * Report absolute bearing and range to nearest airports. +-- * Weather report at nearby airbases and mission waypoints. +-- * Report absolute bearing and range to nearest airports and mission waypoints. -- * Report current altitude AGL of own aircraft. -- * Upon request, ATC reports altitude until touchdown. --- * Report weather (pressure temperature, wind) and BR at players mission waypoints. -- * Works with static and dynamic weather. --- * Player can select the unit system (metric or imperial) in which data is reported. --- * All maps supported (Caucasus, NTTR, Normandy, Persion Gulf and all future maps). +-- * Player can select the unit system (metric or imperial) in which information is reported. +-- * All maps supported (Caucasus, NTTR, Normandy, Persian Gulf and all future maps). -- -- ==== -- -- # Demo Missions -- --- ### [ALL Demo Missions of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master) --- ### [ALL Demo Missions of the latest deveopment branch](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop) +-- ### [MOOSE - ALL Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS) -- -- ==== -- -- # YouTube Channel -- --- ### [MOOSE YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) -- -- === -- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** -- --- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536)** +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) -- -- ==== -- @module PseudoATC @@ -51,14 +47,37 @@ -- @field #table player Table comprising each player info. -- @field #boolean Debug If true, print debug info to dcs.log file. -- @field #number mdur Duration in seconds how low messages to the player are displayed. --- @field #number mrefresh Interval in seconds after which the F10 menu is refreshed. E.g. by the closest airports. +-- @field #number mrefresh Interval in seconds after which the F10 menu is refreshed. E.g. by the closest airports. Default is 120 sec. +-- @field #number talt Interval in seconds between reporting altitude until touchdown. Default 3 sec. +-- @field #boolean chatty Display some messages on events like take-off and touchdown. -- @field #boolean eventsmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. -- @extends Core.Base#BASE ---# PSEUDOATC class, extends @{Base#BASE} -- The PSEUDOATC class adds some rudimentary ATC functionality via the radio menu. -- --- ## Scripting: +-- Local weather reports can be requested for nearby airports and player's mission waypoints. +-- The weather report includes +-- +-- * QFE and QNH pressures, +-- * Temperature, +-- * Wind direction and strength. +-- +-- The list of airports is updated every 60 seconds. This interval can be adjusted by the function @{#PSEUDOATC.SetMenuRefresh}(*interval*). +-- +-- Likewise, absolute bearing and range to the close by airports and mission waypoints can be requested. +-- +-- The player can switch the unit system in which all information is displayed during the mission with the MOOSE settings radio menu. +-- The unit system can be set to either imperial or metric. Altitudes are reported in feet or meter, distances in kilometers or nautical miles, +-- temperatures in degrees Fahrenheit or Celsius and QFE/QNH pressues in inHg or mmHg. +-- Note that the pressures are also reported in hPa independent of the unit system setting. +-- +-- In bad weather conditions, the ATC can "talk you down", i.e. will continuously report your altitude on the final approach. +-- Default reporting time interval is 3 seconds. This can be adjusted via the @{#PSEUDOATC.SetReportAltInterval}(*interval*) function. +-- The reporting stops automatically when the player lands or can be stopped manually by clicking on the radio menu item again. +-- So the radio menu item acts as a toggle to switch the reporting on and off. +-- +-- ## Scripting -- -- Scripting is almost trivial. Just add the following two lines to your script: -- @@ -72,38 +91,31 @@ PSEUDOATC={ player={}, Debug=false, mdur=30, - mrefresh=60, + mrefresh=120, + talt=3, + chatty=true, eventsmoose=true, } ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- PSEUDOATC unit conversions. --- @list unit -PSEUDOATC.unit={ - hPa2inHg=0.0295299830714, - hPa2mmHg=0.7500615613030, - meter2feet=3.28084, - km2nm=0.539957, -} - --- Some ID to identify who we are in output of the DCS.log file. -- @field #string id PSEUDOATC.id="PseudoATC | " --- PSEUDOATC version. -- @field #number version -PSEUDOATC.version="0.7.0" +PSEUDOATC.version="0.9.0" ----------------------------------------------------------------------------------------------------------------------------------------- -- TODO list --- TODO: Add takeoff event. --- TODO: Add user functions. +-- DONE: Add takeoff event. +-- DONE: Add user functions. ----------------------------------------------------------------------------------------------------------------------------------------- ---- PSEUDOATC contructor. Starts the PseudoATC. +--- PSEUDOATC contructor. -- @param #PSEUDOATC self -- @return #PSEUDOATC Returns a PSEUDOATC object. function PSEUDOATC:New() @@ -118,9 +130,8 @@ function PSEUDOATC:New() return self end ---- PSEUDOATC contructor. Starts the PseudoATC. +--- Starts the PseudoATC event handlers. -- @param #PSEUDOATC self --- @return #PSEUDOATC Returns a PSEUDOATC object. function PSEUDOATC:Start() self:F() @@ -160,7 +171,19 @@ function PSEUDOATC:DebugOff() self.Debug=false end ---- Set message duration how long messages are displayed. +--- Chatty mode on. Display some messages on take-off and touchdown. +-- @param #PSEUDOATC self +function PSEUDOATC:ChattyOn() + self.chatty=true +end + +--- Chatty mode off. Don't display some messages on take-off and touchdown. +-- @param #PSEUDOATC self +function PSEUDOATC:ChattyOff() + self.chatty=false +end + +--- Set duration how long messages are displayed. -- @param #PSEUDOATC self -- @param #number duration Time in seconds. Default is 30 sec. function PSEUDOATC:SetMessageDuration(duration) @@ -169,18 +192,25 @@ end --- Set time interval after which the F10 radio menu is refreshed. -- @param #PSEUDOATC self --- @param #number interval Interval in seconds. Default is every 60 sec. -function PSEUDOATC:SetMessageDuration(interval) - self.mrefresh=interval or 60 +-- @param #number interval Interval in seconds. Default is every 120 sec. +function PSEUDOATC:SetMenuRefresh(interval) + self.mrefresh=interval or 120 end --- Enable/disable event handling by MOOSE or DCS. -- @param #PSEUDOATC self --- @param #boolean switch If true, events are handled by MOOSE (default). If fase, events are handled directly by DCS. -function PSEUDOATC:SetMessageDuration(switch) +-- @param #boolean switch If true, events are handled by MOOSE (default). If false, events are handled directly by DCS. +function PSEUDOATC:SetEventsMoose(switch) self.eventsmoose=switch end +--- Set time interval for reporting altitude until touchdown. +-- @param #PSEUDOATC self +-- @param #number interval Interval in seconds. Default is every 3 sec. +function PSEUDOATC:SetReportAltInterval(interval) + self.talt=interval or 3 +end + ----------------------------------------------------------------------------------------------------------------------------------------- -- Event Handling @@ -231,7 +261,7 @@ function PSEUDOATC:onEvent(Event) end -- Event takeoff. - if Event.id == world.event.S_EVENT_TAKEOFF and _playername then + if Event.id == world.event.S_EVENT_TAKEOFF and _playername and EventData.Place then self:_PlayerTakeOff(EventData) end @@ -259,12 +289,7 @@ function PSEUDOATC:onEvent(Event) if Event.id == world.event.S_EVENT_PILOT_DEAD and _playername then self:_PlayerLeft(EventData) end - - -- Event pilot dead ==> player left unit - if Event.id == world.event.S_EVENT_PILOT_DEAD and _playername then - self:_PlayerLeft(EventData) - end - + end --- Function called my MOOSE event handler when a player enters a unit. @@ -376,7 +401,7 @@ function PSEUDOATC:PlayerEntered(unit) self.player[GID].waypoints=group:GetTaskRoute() -- Info message. - local text=string.format("Player %s entered unit %s of group %s. ID = %d", PlayerName, UnitName, GroupName, GID) + local text=string.format("Player %s entered unit %s of group %s (id=%d).", PlayerName, UnitName, GroupName, GID) self:T(PSEUDOATC.id..text) MESSAGE:New(text, 30):ToAllIf(self.Debug) @@ -414,7 +439,7 @@ function PSEUDOATC:PlayerLanded(unit, place) local CallSign=self.player[id].callsign -- Debug message. - local text=string.format("Player %s (%s) from group %s (ID %d) landed at %s", PlayerName, UnitName, GroupName, id, place) + local text=string.format("Player %s in unit %s of group %s (id=%d) landed at %s.", PlayerName, UnitName, GroupName, id, place) self:T(PSEUDOATC.id..text) MESSAGE:New(text, 30):ToAllIf(self.Debug) @@ -422,7 +447,7 @@ function PSEUDOATC:PlayerLanded(unit, place) self:AltitudeTimerStop(id) -- Welcome message. - if place then + if place and self.chatty then local text=string.format("Touchdown! Welcome to %s. Have a nice day!", place) MESSAGE:New(text, self.mdur):ToGroup(group) end @@ -446,13 +471,13 @@ function PSEUDOATC:PlayerTakeOff(unit, place) local CallSign=self.player[id].callsign -- Debug message. - local text=string.format("Player %s (%s) from group %s (ID %d) took off at %s", PlayerName, UnitName, GroupName, id, place) + local text=string.format("Player %s in unit %s of group %s (id=%d) took off at %s.", PlayerName, UnitName, GroupName, id, place) self:T(PSEUDOATC.id..text) MESSAGE:New(text, 30):ToAllIf(self.Debug) -- Bye-Bye message. - if place then - local text=string.format("%s, %s, your are airborn. Have a save trip!", place, CallSign) + if place and self.chatty then + local text=string.format("%s, %s, you are airborn. Have a save trip!", place, CallSign) MESSAGE:New(text, self.mdur):ToGroup(group) end @@ -468,26 +493,30 @@ function PSEUDOATC:PlayerLeft(unit) local group=unit:GetGroup() local id=group:GetID() - -- Debug message. - local text=string.format("Player %s (%s) callsign %s of group %s just left.", self.player[id].playername, self.player[id].unitname, self.player[id].callsign, self.player[id].groupname) - self:T(PSEUDOATC.id..text) - MESSAGE:New(text, 30):ToAllIf(self.Debug) + if self.player[id] then - -- Stop scheduler for menu updates - if self.player[id].schedulerid then - self.player[id].scheduler:Stop(self.player[id].schedulerid) + -- Debug message. + local text=string.format("Player %s (callsign %s) of group %s just left unit %s.", self.player[id].playername, self.player[id].callsign, self.player[id].groupname, self.player[id].unitname) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + + -- Stop scheduler for menu updates + if self.player[id].schedulerid then + self.player[id].scheduler:Stop(self.player[id].schedulerid) + end + + -- Stop scheduler for reporting alt if it runs. + self:AltitudeTimerStop(id) + + -- Remove main menu. + if self.player[id].menu_main then + missionCommands.removeItem(self.player[id].menu_main) + end + + -- Remove player array. + self.player[id]=nil + end - - -- Stop scheduler for reporting alt if it runs. - self:AltitudeTimerStop(id) - - -- Remove main menu. - if self.player[id].menu_main then - missionCommands.removeItem(self.player[id].menu_main) - end - - -- Remove player array. - self.player[id]=nil end ----------------------------------------------------------------------------------------------------------------------------------------- @@ -551,14 +580,14 @@ function PSEUDOATC:MenuClear(id) end -- Delete request current alt menu command. - if self.player[id].menu_requesttalt then + if self.player[id].menu_requestalt then missionCommands.removeItemForGroup(id, self.player[id].menu_requestalt) self.player[id].menu_requestalt=nil end end ---- Create "F10/Pseudo ATC/Local Airports" menu item. +--- Create "F10/Pseudo ATC/Local Airports/Airport Name/" menu items each containing weather report and BR request. -- @param #PSEUDOATC self -- @param #number id Group id of player unit for which menues are created. function PSEUDOATC:MenuAirports(id) @@ -591,7 +620,7 @@ function PSEUDOATC:MenuAirports(id) end end ---- Create F10/Pseudo ATC/Waypoints menu items and misc items. +--- Create "F10/Pseudo ATC/Waypoints/ menu items. -- @param #PSEUDOATC self -- @param #number id Group id of player unit for which menues are created. function PSEUDOATC:MenuWaypoints(id) @@ -600,10 +629,9 @@ function PSEUDOATC:MenuWaypoints(id) -- Player unit and callsign. local unit=self.player[id].unit --Wrapper.Unit#UNIT local callsign=self.player[id].callsign - local name=string.format("My Aircraft (%s)", callsign) -- Debug info. - self:T(PSEUDOATC.id..string.format("Creating waypoint menu for %s (ID %d).", name, id)) + self:T(PSEUDOATC.id..string.format("Creating waypoint menu for %s (ID %d).", callsign, id)) if #self.player[id].waypoints>0 then @@ -633,8 +661,8 @@ function PSEUDOATC:MenuWaypoints(id) end end - self.player[id].menu_reportalt = missionCommands.addCommandForGroup(id, "Report alt until touchdown", self.player[id].menu_main, self.AltidudeTimerToggle, self, id) - self.player[id].menu_requestalt = missionCommands.addCommandForGroup(id, "Request altitude AGL", self.player[id].menu_main, self.ReportHeight, self, id) + self.player[id].menu_reportalt = missionCommands.addCommandForGroup(id, "Talk me down", self.player[id].menu_main, self.AltidudeTimerToggle, self, id) + self.player[id].menu_requestalt = missionCommands.addCommandForGroup(id, "Request altitude", self.player[id].menu_main, self.ReportHeight, self, id) end ----------------------------------------------------------------------------------------------------------------------------------------- @@ -657,28 +685,27 @@ function PSEUDOATC:ReportWeather(id, position, location) local Pqnh=position:GetPressure(0) -- Get pressure at sea level. local Pqfe=position:GetPressure() -- Get pressure at (land) height of position. + -- Pressure conversion + local hPa2inHg=0.0295299830714 + local hPa2mmHg=0.7500615613030 + -- Unit conversion. - local _Pqnh=string.format("%.2f inHg", Pqnh * PSEUDOATC.unit.hPa2inHg) - local _Pqfe=string.format("%.2f inHg", Pqfe * PSEUDOATC.unit.hPa2inHg) + local _Pqnh=string.format("%.2f inHg", Pqnh * hPa2inHg) + local _Pqfe=string.format("%.2f inHg", Pqfe * hPa2inHg) if settings:IsMetric() then - _Pqnh=string.format("%.1f mmHg", Pqnh * PSEUDOATC.unit.hPa2mmHg) - _Pqfe=string.format("%.1f mmHg", Pqfe * PSEUDOATC.unit.hPa2mmHg) + _Pqnh=string.format("%.1f mmHg", Pqnh * hPa2mmHg) + _Pqfe=string.format("%.1f mmHg", Pqfe * hPa2mmHg) end -- Message text. text=text..string.format("QFE %.1f hPa = %s.\n", Pqfe, _Pqfe) text=text..string.format("QNH %.1f hPa = %s.\n", Pqnh, _Pqnh) - --- convert celsius to fahrenheit - local function celsius2fahrenheit(degC) - return degC*1.8+32 - end - -- Get temperature at position in degrees Celsius. local T=position:GetTemperature() -- Correct unit system. - local _T=string.format('%d°F', celsius2fahrenheit(T)) + local _T=string.format('%d°F', UTILS.CelciusToFarenheit(T)) if settings:IsMetric() then _T=string.format('%d°C', T) end @@ -696,9 +723,9 @@ function PSEUDOATC:ReportWeather(id, position, location) local Ds = string.format('%03d°', Dir) -- Velocity in player units. - local Vs=string.format('%.1f m/s', Vel) - if settings:IsImperial() then - Vs=string.format("%.1f knots", Vel*1.94384) + local Vs=string.format("%.1f knots", UTILS.MpsToKnots(Vel)) + if settings:IsMetric() then + Vs=string.format('%.1f m/s', Vel) end -- Message text. @@ -709,111 +736,6 @@ function PSEUDOATC:ReportWeather(id, position, location) end ---- Report pressure. --- @param #PSEUDOATC self --- @param #number id Group id to which the report is delivered. --- @param #string Qcode Can be "QNH" for pressure at sea level or "QFE" for pressure at field elevation. Default is QFE or more precisely pressure at position. --- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. --- @param #string location Name of the location at which the pressure is measured. -function PSEUDOATC:ReportPressure(id, Qcode, position, location) - self:F({id=id, Qcode=Qcode, position=position, location=location}) - - -- Get pressure in hPa. - local P - if Qcode=="QNH" then - P=position:GetPressure(0) -- Get pressure at sea level. - else - P=position:GetPressure() -- Get pressure at (land) height of position. - end - - -- Settings. - local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS - - -- Unit conversion. - local P_inHg=P * PSEUDOATC.unit.hPa2inHg - local P_mmHg=P * PSEUDOATC.unit.hPa2mmHg - - local P_set=string.format("%.2f inHg", P_inHg) - if settings:IsMetric() then - P_set=string.format("%.1f mmHg", P_mmHg) - end - - -- Message text. - local text=string.format("%s at %s: P = %.1f hPa = %s.", Qcode, location, P, P_set) - - -- Send message. - MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) -end - ---- Report temperature. --- @param #PSEUDOATC self --- @param #number id Group id to the report is delivered. --- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. --- @param #string location Name of the location at which the pressure is measured. -function PSEUDOATC:ReportTemperature(id, position, location) - self:F({id=id, position=position, location=location}) - - --- convert celsius to fahrenheit - local function celsius2fahrenheit(degC) - return degC*1.8+32 - end - - -- Get temperature at position in degrees Celsius. - local T=position:GetTemperature() - - -- Formatted temperature in Celsius and Fahrenheit. - local Tc=string.format('%d°C', T) - local Tf=string.format('%d°F', celsius2fahrenheit(T)) - - -- Settings. - local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS - - -- Correct unit system. - local _T=string.format('%d°F', celsius2fahrenheit(T)) - if settings:IsMetric() then - _T=string.format('%d°C', T) - end - - -- Message text. - local text=string.format("Temperature at %s is %s", location, _T) - - -- Send message to player group. - MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) -end - ---- Report wind direction and strength. --- @param #PSEUDOATC self --- @param #number id Group id to the report is delivered. --- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. --- @param #string location Name of the location at which the pressure is measured. -function PSEUDOATC:ReportWind(id, position, location) - self:F({id=id, position=position, location=location}) - - -- Get wind direction and speed. - local Dir,Vel=position:GetWind() - - -- Get Beaufort wind scale. - local Bn,Bd=UTILS.BeaufortScale(Vel) - - -- Formatted wind direction. - local Ds = string.format('%03d°', Dir) - - -- Player settings. - local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS - - -- Velocity in player units. - local Vs=string.format('%.1f m/s', Vel) - if settings:IsImperial() then - Vs=string.format("%.1f knots", Vel*1.94384) - end - - -- Message text. - local text=string.format("%s: Wind from %s at %s (%s).", location, Ds, Vs, Bd) - - -- Send message to player group. - MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) -end - --- Report absolute bearing and range form player unit to airport. -- @param #PSEUDOATC self -- @param #number id Group id to the report is delivered. @@ -838,9 +760,10 @@ function PSEUDOATC:ReportBR(id, position, location) -- Settings. local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS - local Rs=string.format("%.1f km", range/1000) - if settings:IsImperial() then - Rs=string.format("%.1f NM", range/1000 * PSEUDOATC.unit.km2nm) + + local Rs=string.format("%.1f NM", UTILS.MetersToNM(range)) + if settings:IsMetric() then + Rs=string.format("%.1f km", range/1000) end -- Message text. @@ -867,11 +790,9 @@ function PSEUDOATC:ReportHeight(id, dt, _clear) -- Return height [m] above ground level. local function get_AGL(p) local agl=0 - if p then - local vec2={x=p.x,y=p.z} - local ground=land.getHeight(vec2) - local agl=p.y-ground - end + local vec2={x=p.x,y=p.z} + local ground=land.getHeight(vec2) + local agl=p.y-ground return agl end @@ -887,16 +808,23 @@ function PSEUDOATC:ReportHeight(id, dt, _clear) -- Settings. local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS - local Hs=string.format("%d m", height) + env.info("FF height = "..height) + + -- Height string. + local Hs=string.format("%d ft", UTILS.MetersToFeet(height)) if settings:IsMetric() then - Hs=string.format("%d ft", height*PSEUDOATC.unit.meter2feet) + Hs=string.format("%d m", height) end -- Message text. - local _text=string.format("%s: Your altitude is %s AGL.", callsign, Hs) + local _text=string.format("%s, your altitude is %s AGL.", callsign, Hs) + + -- Append flight level. + if _clear==false then + _text=_text..string.format(" FL%03d.", position.y/30.48) + end -- Send message to player group. - --MESSAGE:New(text, dt):ToGroup(self.player[id].group) self:_DisplayMessageToGroup(self.player[id].unit,_text, dt,_clear) -- Return height @@ -908,7 +836,7 @@ end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Toggle report altitude reporting on/of. +--- Toggle report altitude reporting on/off. -- @param #PSEUDOATC self. -- @param #number id Group id of player unit. function PSEUDOATC:AltidudeTimerToggle(id) @@ -932,8 +860,7 @@ function PSEUDOATC:AltitudeTimeStart(id) -- Debug info. self:T(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.", id)) - -- Start timer. - --self.player[id].altimer=timer.scheduleFunction(self.ReportAltTouchdown, self, id, Tnow+2) + -- Start timer. Altitude is reported every ~3 seconds. self.player[id].altimer, self.player[id].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, id, 0.1, true}, 1, 3) end @@ -946,7 +873,6 @@ function PSEUDOATC:AltitudeTimerStop(id) self:T(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", id)) -- Stop timer. - --timer.removeFunction(self.player[id].alttimer) if self.player[id].altimerid then self.player[id].altimer:Stop(self.player[id].altimerid) end @@ -1061,3 +987,16 @@ function PSEUDOATC:_DisplayMessageToGroup(_unit, _text, _time, _clear) end +--- Returns a string which consits of this callsign and the player name. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +function PSEUDOATC:_myname(unitname) + self:F2(unitname) + + local unit=UNIT:FindByName(unitname) + local pname=unit:GetPlayerName() + local csign=unit:GetCallsign() + + return string.format("%s (%s)", csign, pname) +end + diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index d5e78cdce..91f42eb48 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -42,20 +42,21 @@ -- -- # Demo Missions -- --- ### [RAT Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/Release/RAT%20-%20Random%20Air%20Traffic) --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- ### [MOOSE - ALL Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS) +-- ### [MOOSE - RAT Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/RAT%20-%20Random%20Air%20Traffic) -- -- === -- -- # YouTube Channel -- --- ### [DCS WORLD - MOOSE - RAT - Random Air Traffic](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0u4Zxywtg-mx_ov4vi68CO) +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- ### [MOOSE - RAT - Random Air Traffic](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0u4Zxywtg-mx_ov4vi68CO) -- -- === -- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** -- --- ### Contributions: **Sven van de Velde ([FlightControl](https://forums.eagle.ru/member.php?u=89536))** +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) -- -- === -- @module Rat @@ -116,7 +117,7 @@ -- @field #boolean continuejourney Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. -- @field #number ngroups Number of groups to be spawned in total. -- @field #number alive Number of groups which are alive. --- @field #boolean f10menu Add an F10 menu for RAT. +-- @field #boolean f10menu If true, add an F10 radiomenu for RAT. Default is false. -- @field #table Menu F10 menu items for this RAT object. -- @field #string SubMenuName Submenu name for RAT object. -- @field #boolean respawn_at_landing Respawn aircraft the moment they land rather than at engine shutdown. @@ -350,7 +351,7 @@ RAT={ continuejourney=false, -- Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. alive=0, -- Number of groups which are alive. ngroups=nil, -- Number of groups to be spawned in total. - f10menu=true, -- Add an F10 menu for RAT. + f10menu=false, -- Add an F10 menu for RAT. Menu={}, -- F10 menu items for this RAT object. SubMenuName=nil, -- Submenu name for RAT object. respawn_at_landing=false, -- Respawn aircraft the moment they land rather than at engine shutdown. @@ -506,7 +507,7 @@ RAT.id="RAT | " --- RAT version. -- @list version RAT.version={ - version = "2.2.1", + version = "2.2.2", print = true, } @@ -1363,6 +1364,21 @@ function RAT:Immortal() self.immortal=true end +--- Radio menu On. Default is off. +-- @param #RAT self +function RAT:RadioMenuON() + self:F2() + self.f10menu=true +end + +--- Radio menu Off. This is the default setting. +-- @param #RAT self +function RAT:RadioMenuOFF() + self:F2() + self.f10menu=false +end + + --- Activate uncontrolled aircraft. -- @param #RAT self -- @param #number maxactivated Maximal numnber of activated aircraft. Absolute maximum will be the number of spawned groups. Default is 1. diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index bf8cbb193..bce0b8ef1 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -32,12 +32,13 @@ -- -- # Demo Missions -- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- ### [MOOSE - ALL Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS) -- -- === -- -- # YouTube Channel --- +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) -- ### [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M) -- -- === diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua index e9f42c626..8eb913930 100644 --- a/Moose Development/Moose/Functional/Suppression.lua +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -1,5 +1,5 @@ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **Functional** - Suppress fire of ground units when they get hit. +--- **Functional** - (R2.4) Suppress fire of ground units when they get hit. -- -- ![Banner Image](..\Presentations\SUPPRESSION\Suppression_Main.png) -- @@ -16,19 +16,19 @@ -- -- # Demo Missions -- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- ### [MOOSE - ALL Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS) -- -- ==== -- -- # YouTube Channel -- --- ### [MOOSE YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) -- -- === -- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** -- --- ### Contributions: **[FlightControl](https://forums.eagle.ru/member.php?u=89536)** +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) -- -- ==== -- @module Suppression @@ -194,10 +194,6 @@ -- -- ![Process](..\Presentations\SUPPRESSION\Suppression_Example_01.png) -- --- ## Suppression and Rescure --- This example shows how the event **Retreat** can be captured. Here, a transport is started which picks up the wounded troups and drives them to a safe zone. --- --- ![Process](..\Presentations\SUPPRESSION\Suppression_Rescue.png) -- -- # Customization and Fine Tuning -- The following user functions can be used to change the default values @@ -281,11 +277,11 @@ SUPPRESSION.MenuF10=nil --- Some ID to identify who we are in output of the DCS.log file. -- @field #string id -SUPPRESSION.id="SFX | " +SUPPRESSION.id="SUPPRESSION | " --- PSEUDOATC version. -- @field #number version -SUPPRESSION.version="0.7.0" +SUPPRESSION.version="0.9.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1333,6 +1329,7 @@ end -- @param #SUPPRESSION self -- @param Core.Event#EVENTDATA EventData function SUPPRESSION:_OnEventHit(EventData) + self:F(EventData) local GroupNameSelf=self.Controllable:GetName() local GroupNameTgt=EventData.TgtGroupName @@ -1343,7 +1340,7 @@ function SUPPRESSION:_OnEventHit(EventData) -- Check that correct group was hit. if GroupNameTgt == GroupNameSelf then - self:T2(SUPPRESSION.id..string.format("Hit event at t = %5.1f", timer.getTime())) + self:T(SUPPRESSION.id..string.format("Hit event at t = %5.1f", timer.getTime())) -- Flare unit that was hit. if self.flare or self.Debug then From 6f0507ea7f11b29ae4a25b6a5364166fdc239568 Mon Sep 17 00:00:00 2001 From: funkyfranky Date: Thu, 10 May 2018 15:40:10 +0200 Subject: [PATCH 31/31] Minor fixes. Positionalble: Added isExist because of problems with getting coords from scenery objects. Suppresson: Fixes. Added new transitions. PseudoATC: Removed eject. Artillery: Optimized debug output. --- .../Moose/Functional/Artillery.lua | 82 +++++++++++-------- .../Moose/Functional/PseudoATC.lua | 9 +- .../Moose/Functional/Suppression.lua | 15 +++- .../Moose/Wrapper/Positionable.lua | 6 +- Moose Setup/Moose.files | 3 + 5 files changed, 69 insertions(+), 46 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 418bb359a..8849c68e0 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -330,7 +330,7 @@ -- @field #ARTY ARTY={ ClassName="ARTY", - Debug=true, + Debug=false, targets={}, moves={}, currentTarget=nil, @@ -384,7 +384,7 @@ ARTY.WeaponType={ ARTY.id="ARTY | " --- Arty script version. --- @field #number version +-- @field #string version ARTY.version="0.9.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -799,7 +799,8 @@ function ARTY:onafterStart(Controllable, From, Event, To) self:_EventFromTo("onafterStart", Event, From, To) -- Debug output. - local text=string.format("Started ARTY for group %s.", Controllable:GetName()) + local text=string.format("Started ARTY version %s for group %s.", ARTY.version, Controllable:GetName()) + self:E(ARTY.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) -- Get Ammo. @@ -954,31 +955,31 @@ function ARTY:_OnEventShot(EventData) -- Weapon type name for current target. local _weapontype=self:_WeaponTypeName(self.currentTarget.weapontype) - self:T(ARTY.id..string.format("nammo=%d, nshells=%d, nrockets=%d, nmissiles=%d", _nammo, _nshells, _nrockets, _nmissiles)) - self:T(ARTY.id..string.format("Weapontype = %s", _weapontype)) + self:T(ARTY.id..string.format("Group %s ammo: total=%d, shells=%d, rockets=%d, missiles=%d", self.Controllable:GetName(), _nammo, _nshells, _nrockets, _nmissiles)) + self:T2(ARTY.id..string.format("Group %s uses weapontype %s for current target.", self.Controllable:GetName(), _weapontype)) -- Special weapon type requested ==> Check if corresponding ammo is empty. if self.currentTarget.weapontype==ARTY.WeaponType.Cannon and _nshells==0 then - self:T(ARTY.id.."Cannons requested but shells empty.") + self:T(ARTY.id.."Group %s, cannons requested but shells empty.", self.Controllable:GetName()) self:CeaseFire(self.currentTarget) return elseif self.currentTarget.weapontype==ARTY.WeaponType.Rockets and _nrockets==0 then - self:T(ARTY.id.."Rockets requested but rockets empty.") + self:T(ARTY.id.."Group %s, rockets requested but rockets empty.", self.Controllable:GetName()) self:CeaseFire(self.currentTarget) return elseif self.currentTarget.weapontype==ARTY.WeaponType.UnguidedAny and _nshells+_nrockets==0 then - self:T(ARTY.id.."Unguided weapon requested but shells AND rockets empty.") + self:T(ARTY.id.."Group %s, unguided weapon requested but shells AND rockets empty.", self.Controllable:GetName()) self:CeaseFire(self.currentTarget) return elseif (self.currentTarget.weapontype==ARTY.WeaponType.GuidedMissile or self.currentTarget.weapontype==ARTY.WeaponType.CruiseMissile or self.currentTarget.weapontype==ARTY.WeaponType.AntiShipMissile) and _nmissiles==0 then - self:T(ARTY.id.."Guided, anti-ship or cruise missiles requested but all missiles empty.") + self:T(ARTY.id.."Group %s, guided, anti-ship or cruise missiles requested but all missiles empty.", self.Controllable:GetName()) self:CeaseFire(self.currentTarget) return @@ -995,7 +996,7 @@ function ARTY:_OnEventShot(EventData) end else - self:E(ARTY.id..string.format("ERROR: No current target?!")) + self:E(ARTY.id..string.format("ERROR: No current target for group %s?!", self.Controllable:GetName())) end end end @@ -1042,22 +1043,20 @@ function ARTY:onafterStatus(Controllable, From, Event, To) -- Group is out of ammo. if self:is("OutOfAmmo") then - env.info(string.format("FF: OutOfAmmo. ==> Rearm")) + self:T2(ARTY.id..string.format("%s: OutOfAmmo. ==> Rearm", Controllable:GetName())) self:Rearm() end -- Group is out of moving. if self:is("Moving") then - --local _speed=self.Controllable:GetVelocityKMH() - --env.info(string.format("FF: Moving. Velocity = %d km/h", _speed)) - env.info(string.format("FF: Moving")) + self:T2(ARTY.id..string.format("%s: Moving", Controllable:GetName())) end -- Group is rearming. if self:is("Rearming") then local _rearmed=self:_CheckRearmed() - env.info(string.format("FF: Rearming. _rearmed = %s", tostring(_rearmed))) if _rearmed then + self:T2(ARTY.id..string.format("%s: Rearming ==> Rearmed", Controllable:GetName())) self:Rearmed() end end @@ -1065,15 +1064,16 @@ function ARTY:onafterStatus(Controllable, From, Event, To) -- Group finished rearming. if self:is("Rearmed") then local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) - env.info(string.format("FF: Rearmed. Distance ARTY to InitalCoord = %d", distance)) + self:T2(ARTY.id..string.format("%s: Rearmed. Distance ARTY to InitalCoord = %d m", Controllable:GetName(), distance)) if distance <= self.RearmingDistance then + self:T2(ARTY.id..string.format("%s: Rearmed ==> CombatReady", Controllable:GetName())) self:CombatReady() end end -- Group arrived at destination. if self:is("Arrived") then - env.info(string.format("FF: Arrived. ==> CombatReady")) + self:T2(ARTY.id..string.format("%s: Arrived ==> CombatReady", Controllable:GetName())) self:CombatReady() end @@ -1093,15 +1093,18 @@ function ARTY:onafterStatus(Controllable, From, Event, To) -- Get a commaned move to another location. local _move=self:_CheckMoves() - -- Group is combat ready or firing but we have a high prio timed target. + if (self:is("CombatReady") or self:is("Firing")) and _move then + -- Group is combat ready or firing but we have a move. + self:T2(ARTY.id..string.format("%s: CombatReady/Firing ==> Move", Controllable:GetName())) -- Command to move. self.currentMove=_move self:Move(_move.coord, _move.speed, _move.onroad) elseif self:is("CombatReady") or (self:is("Firing") and _timedTarget) then - env.info(string.format("FF: Combatready or firing and high prio timed target.")) + -- Group is combat ready or firing but we have a high prio timed target. + self:T2(ARTY.id..string.format("%s: CombatReady or Firing+Timed Target ==> OpenFire", Controllable:GetName())) -- Engage target. if _timedTarget then @@ -1137,7 +1140,7 @@ end function ARTY:onenterCombatReady(Controllable, From, Event, To) self:_EventFromTo("onenterCombatReady", Event, From, To) -- Debug info - self:T(string.format("FF: onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) + self:T3(ARTY.id..string.format("onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1882,14 +1885,16 @@ function ARTY:GetAmmo(display) local weapons=#ammotable - self:T2(ARTY.id..string.format("Number of weapons %d.", weapons)) - self:T2({ammotable=ammotable}) - - self:T(ARTY.id.."Ammotable:") - for id,bla in pairs(ammotable) do - self:T({id=id, ammo=bla}) + -- Display ammo table + if display then + self:E(ARTY.id..string.format("Number of weapons %d.", weapons)) + self:E({ammotable=ammotable}) + self:E(ARTY.id.."Ammotable:") + for id,bla in pairs(ammotable) do + self:E({id=id, ammo=bla}) + end end - + -- Loop over all weapons. for w=1,weapons do @@ -1957,7 +1962,7 @@ function ARTY:GetAmmo(display) nshells=nshells+Nammo -- Debug info. - text=text..string.format("- %d shells of type %s (category=%d mc=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) + text=text..string.format("- %d shells of type %s (category=%d, missile category=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) elseif _gotrocket then @@ -1965,7 +1970,7 @@ function ARTY:GetAmmo(display) nrockets=nrockets+Nammo -- Debug info. - text=text..string.format("- %d rockets of type %s (category=%d mc=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) + text=text..string.format("- %d rockets of type %s (category=%d, missile category=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) elseif _gotmissile then @@ -1973,12 +1978,12 @@ function ARTY:GetAmmo(display) nmissiles=nmissiles+Nammo -- Debug info. - text=text..string.format("- %d missiles of type %s (category=%d mc=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) + text=text..string.format("- %d missiles of type %s (category=%d, missile category=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) else -- Debug info. - text=text..string.format("- %d unknown ammo of type %s (category=%d mc=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) + text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) end @@ -1986,7 +1991,11 @@ function ARTY:GetAmmo(display) end -- Debug text and send message. - self:T(ARTY.id..text) + if display then + self:E(ARTY.id..text) + else + self:T3(ARTY.id..text) + end MESSAGE:New(text, 10):ToAllIf(display) end @@ -2112,7 +2121,7 @@ function ARTY:_CheckName(givennames, name) until (unique) -- Debug output and return new name. - self:T(string.format("Original name %s, new name = %s", name, newname)) + self:T2(string.format("Original name %s, new name = %s", name, newname)) return newname end @@ -2171,6 +2180,8 @@ function ARTY:_WeaponTypeName(tnumber) name="Cruise Missiles" elseif tnumber==ARTY.WeaponType.GuidedMissile then name="Guided Missiles" + elseif tnumber==ARTY.WeaponType.AntiShipMissile then + name="Anti-Ship Missiles" end return name end @@ -2398,12 +2409,15 @@ function ARTY._PassingWaypoint(group, arty, i, final) if final then text=string.format("%s, arrived at destination.", group:GetName()) end - env.info(ARTY.id..text) + arty:T(ARTY.id..text) + + --[[ if final then MESSAGE:New(text, 10):ToCoalitionIf(group:GetCoalition(), arty.Debug or arty.report) else MESSAGE:New(text, 10):ToAllIf(arty.Debug) end + ]] -- Arrived event. if final and arty.Controllable:GetName()==group:GetName() then diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua index 9726f1666..ce7f4e737 100644 --- a/Moose Development/Moose/Functional/PseudoATC.lua +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -146,7 +146,7 @@ function PSEUDOATC:Start() self:HandleEvent(EVENTS.Takeoff, self._PlayerTakeOff) self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) self:HandleEvent(EVENTS.Crash, self._PlayerLeft) - self:HandleEvent(EVENTS.Ejection, self._PlayerLeft) + --self:HandleEvent(EVENTS.Ejection, self._PlayerLeft) --self:HandleEvent(EVENTS.PilotDead, self._PlayerLeft) else self:T(PSEUDOATC.id.."Events are handled by DCS.") @@ -280,6 +280,7 @@ function PSEUDOATC:onEvent(Event) self:_PlayerLeft(EventData) end +--[[ -- Event eject ==> player left unit if Event.id == world.event.S_EVENT_EJECTION and _playername then self:_PlayerLeft(EventData) @@ -289,7 +290,7 @@ function PSEUDOATC:onEvent(Event) if Event.id == world.event.S_EVENT_PILOT_DEAD and _playername then self:_PlayerLeft(EventData) end - +]] end --- Function called my MOOSE event handler when a player enters a unit. @@ -477,7 +478,7 @@ function PSEUDOATC:PlayerTakeOff(unit, place) -- Bye-Bye message. if place and self.chatty then - local text=string.format("%s, %s, you are airborn. Have a save trip!", place, CallSign) + local text=string.format("%s, %s, you are airborne. Have a safe trip!", place, CallSign) MESSAGE:New(text, self.mdur):ToGroup(group) end @@ -808,8 +809,6 @@ function PSEUDOATC:ReportHeight(id, dt, _clear) -- Settings. local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS - env.info("FF height = "..height) - -- Height string. local Hs=string.format("%d ft", UTILS.MetersToFeet(height)) if settings:IsMetric() then diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua index 8eb913930..41fb3dc09 100644 --- a/Moose Development/Moose/Functional/Suppression.lua +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -357,6 +357,8 @@ function SUPPRESSION:New(group) self:AddTransition("Retreating", "Retreated", "Retreated") self:AddTransition("*", "Dead", "*") + self:AddTransition("TakingCover", "Hit", "TakingCover") + self:AddTransition("FallingBack", "Hit", "FallingBack") --- User function for OnBefore "Hit" event. -- @function [parent=#SUPPRESSION] OnBeforeHit @@ -1078,7 +1080,9 @@ function SUPPRESSION:onafterFallBack(Controllable, From, Event, To, AttackUnit) local Coord=DCoord:Translate(self.FallbackDist, heading) -- Place marker - local MarkerID=Coord:MarkToAll("Fall back position for group "..Controllable:GetName()) + if self.Debug then + local MarkerID=Coord:MarkToAll("Fall back position for group "..Controllable:GetName()) + end -- Smoke the coordinate. if self.smoke or self.Debug then @@ -1468,7 +1472,7 @@ end function SUPPRESSION:_Run(fin, speed, formation, wait) speed=speed or 20 - formation=formation or "Vee" + formation=formation or "Off road" wait=wait or 30 local group=self.Controllable -- Wrapper.Controllable#CONTROLLABLE @@ -1506,8 +1510,11 @@ function SUPPRESSION:_Run(fin, speed, formation, wait) -- First waypoint is the current position of the group. wp[1]=ini:WaypointGround(speed, formation) - local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName())) tasks[1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, 1, false) + + if self.Debug then + local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName())) + end self:T2(SUPPRESSION.id..string.format("Number of waypoints %d", nx)) for i=1,nx-2 do @@ -1830,7 +1837,7 @@ end -- @param #string To To state. function SUPPRESSION:_EventFromTo(BA, Event, From, To) local text=string.format("\n%s: %s EVENT %s: %s --> %s", BA, self.Controllable:GetName(), Event, From, To) - self:T(SUPPRESSION.id..text) + self:T2(SUPPRESSION.id..text) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index d9308e8bb..1ac85b6a4 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -391,7 +391,7 @@ function POSITIONABLE:GetVelocityVec3() local DCSPositionable = self:GetDCSObject() - if DCSPositionable then + if DCSPositionable and DCSPositionable:isExist() then local PositionableVelocityVec3 = DCSPositionable:getVelocity() self:T3( PositionableVelocityVec3 ) return PositionableVelocityVec3 @@ -433,7 +433,7 @@ function POSITIONABLE:GetVelocityKMH() local DCSPositionable = self:GetDCSObject() - if DCSPositionable then + if DCSPositionable and DCSPositionable:isExist() then local VelocityVec3 = self:GetVelocityVec3() local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec local Velocity = Velocity * 3.6 -- now it is in km/h. @@ -452,7 +452,7 @@ function POSITIONABLE:GetVelocityMPS() local DCSPositionable = self:GetDCSObject() - if DCSPositionable then + if DCSPositionable and DCSPositionable:isExist() then local VelocityVec3 = self:GetVelocityVec3() local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec self:T3( Velocity ) diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index f909972b7..1974715d9 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -54,6 +54,9 @@ Functional/Range.lua Functional/ZoneGoal.lua Functional/ZoneGoalCoalition.lua Functional/ZoneCaptureCoalition.lua +Functional/Artillery.lua +Functional/Suppression.lua +Functional/PseudoATC.lua AI/AI_Balancer.lua AI/AI_A2A.lua