diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index 80dfcb0c5..0b49c3449 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -913,7 +913,7 @@ do -- FSM --- Check if FSM is in state. -- @param #FSM self -- @param #string State State name. - -- @param #boolean If true, FSM is in this state. + -- @return #boolean If true, FSM is in this state. function FSM:Is( State ) return self.current == State end @@ -921,7 +921,7 @@ do -- FSM --- Check if FSM is in state. -- @param #FSM self -- @param #string State State name. - -- @param #boolean If true, FSM is in this state. + -- @return #boolean If true, FSM is in this state. function FSM:is(state) return self.current == state end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 6c9975ff1..3a5160f86 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -283,7 +283,13 @@ do -- COORDINATE return self end - + + --- Return the coordinates itself. Sounds stupid but can be useful for compatibility. + -- @param #COORDINATE self + -- @return #COORDINATE self + function COORDINATE:GetCoordinate() + return self + end --- Return the coordinates of the COORDINATE in Vec3 format. -- @param #COORDINATE self @@ -733,6 +739,13 @@ do -- COORDINATE return Velocity or 0 end + --- Return the "name" of the COORDINATE. Obviously, a coordinate does not have a name like a unit, static or group. So here we take the MGRS coordinates of the position. + -- @param #COORDINATE self + -- @return #string MGRS coordinates. + function COORDINATE:GetName() + local name=self:ToStringMGRS() + return rname + end --- Return velocity text of the COORDINATE. -- @param #COORDINATE self diff --git a/Moose Development/Moose/Core/Timer.lua b/Moose Development/Moose/Core/Timer.lua index 26f809c1d..258996d68 100644 --- a/Moose Development/Moose/Core/Timer.lua +++ b/Moose Development/Moose/Core/Timer.lua @@ -27,6 +27,7 @@ -- @field #number dT Time interval between function calls in seconds. -- @field #number ncalls Counter of function calls. -- @field #number ncallsMax Max number of function calls. If reached, timer is stopped. +-- @field #boolean isrunning If `true`, timer is running. Else it was not started yet or was stopped. -- @extends Core.Base#BASE --- *Better three hours too soon than a minute too late.* - William Shakespeare @@ -111,7 +112,7 @@ _TIMERID=0 --- TIMER class version. -- @field #string version -TIMER.version="0.1.0" +TIMER.version="0.1.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -143,6 +144,9 @@ function TIMER:New(Function, ...) -- Number of function calls. self.ncalls=0 + -- Not running yet. + self.isrunning=false + -- Increase counter _TIMERID=_TIMERID+1 @@ -186,6 +190,9 @@ function TIMER:Start(Tstart, dT, Duration) -- Set log id. self.lid=string.format("TIMER UID=%d/%d | ", self.uid, self.tid) + -- Is now running. + self.isrunning=true + -- Debug info. self:T(self.lid..string.format("Starting Timer in %.3f sec, dT=%s, Tstop=%s", self.Tstart-Tnow, tostring(self.dT), tostring(self.Tstop))) @@ -210,6 +217,9 @@ function TIMER:Stop(Delay) self:T(self.lid..string.format("Stopping timer by removing timer function after %d calls!", self.ncalls)) timer.removeFunction(self.tid) + -- Not running any more. + self.isrunning=false + -- Remove DB entry. --_TIMERDB[self.uid]=nil @@ -229,6 +239,13 @@ function TIMER:SetMaxFunctionCalls(Nmax) return self end +--- Check if the timer has been started and was not stopped. +-- @param #TIMER self +-- @return #boolean If `true`, the timer is running. +function TIMER:IsRunning() + return self.isrunning +end + --- Call timer function. -- @param #TIMER self -- @param #number time DCS model time in seconds. diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index 15c73c29a..2d190d215 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -736,7 +736,66 @@ do -- Airbase end -- Airbase +do -- Spot + --- [DCS Class Spot](https://wiki.hoggitworld.com/view/DCS_Class_Spot) + -- Represents a spot from laser or IR-pointer. + -- @type Spot + -- @field #Spot.Category Category enum that stores spot categories. + + --- Enum that stores spot categories. + -- @type Spot.Category + -- @field #string INFRA_RED + -- @field #string LASER + + + --- Creates a laser ray emanating from the given object to a point in 3d space. + -- @function [parent=#Spot] createLaser + -- @param DCS#Object Source The source object of the laser. + -- @param DCS#Vec3 LocalRef An optional 3D offset for the source. + -- @param DCS#Vec3 Vec3 Target coordinate where the ray is pointing at. + -- @param #number LaserCode Any 4 digit number between 1111 and 1788. + -- @return #Spot + + --- Creates an infrared ray emanating from the given object to a point in 3d space. Can be seen with night vision goggles. + -- @function [parent=#Spot] createInfraRed + -- @param DCS#Object Source Source position of the IR ray. + -- @param DCS#Vec3 LocalRef An optional 3D offset for the source. + -- @param DCS#Vec3 Vec3 Target coordinate where the ray is pointing at. + -- @return #Spot + + --- Returns a vec3 table of the x, y, and z coordinates for the position of the given object in 3D space. Coordinates are dependent on the position of the maps origin. + -- @function [parent=#Spot] getPoint + -- @param #Spot self + -- @return DCS#Vec3 Point in 3D, where the beam is pointing at. + + --- Sets the destination point from which the source of the spot is drawn toward. + -- @function [parent=#Spot] setPoint + -- @param #Spot self + -- @param DCS#Vec3 Vec3 Point in 3D, where the beam is pointing at. + + --- Returns the number that is used to define the laser code for which laser designation can track. + -- @function [parent=#Spot] getCode + -- @param #Spot self + -- @return #number Code The laser code used. + + --- Sets the number that is used to define the laser code for which laser designation can track. + -- @function [parent=#Spot] setCode + -- @param #Spot self + -- @param #number Code The laser code. Default value is 1688. + + --- Destroys the spot. + -- @function [parent=#Spot] destroy + -- @param #Spot self + + --- Gets the category of the spot (laser or IR). + -- @function [parent=#Spot] getCategory + -- @param #Spot self + -- @return #string Category. + + Spot = {} --#Spot + +end -- Spot do -- Controller --- Controller is an object that performs A.I.-routines. Other words controller is an instance of A.I.. Controller stores current main task, active enroute tasks and behavior options. Controller performs commands. Please, read DCS A-10C GUI Manual EN.pdf chapter "Task Planning for Unit Groups", page 91 to understand A.I. system of DCS:A-10C. diff --git a/Moose Development/Moose/Ops/AirWing.lua b/Moose Development/Moose/Ops/AirWing.lua index 9a768dd73..0ea78cdc2 100644 --- a/Moose Development/Moose/Ops/AirWing.lua +++ b/Moose Development/Moose/Ops/AirWing.lua @@ -152,7 +152,7 @@ AIRWING = { --- AIRWING class version. -- @field #string version -AIRWING.version="0.5.0" +AIRWING.version="0.5.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -472,11 +472,19 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) return nil elseif #payloads==1 then -- Only one payload anyway. - return payloads[1] + local payload=payloads[1] --#AIRWING.Payload + if not payload.unlimited then + payload.navail=payload.navail-1 + end + return payload else -- Sort payloads. table.sort(payloads, sortpayloads) - return payloads[1] + local payload=payloads[1] --#AIRWING.Payload + if not payload.unlimited then + payload.navail=payload.navail-1 + end + return payload end end diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index 115607b46..e834f8d4d 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -1,12 +1,29 @@ --- **Ops** - Enhanced Ground Group. -- --- **Main Features:** +-- ## Main Features: -- --- * Dynamically add and remove waypoints. --- +-- * Patrol waypoints *ad infinitum* +-- * Easy change of ROE and alarm state, formation and other settings +-- * Dynamically add and remove waypoints +-- * Sophisticated task queueing system (know when DCS tasks start and end) +-- * Convenient checks when the group enters or leaves a zone +-- * Detection events for new, known and lost units +-- * Simple LASER and IR-pointer setup +-- * Compatible with AUFTRAG class +-- * Many additional events that the mission designer can hook into +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Armygroup). +-- -- === -- -- ### Author: **funkyfranky** +-- +-- == +-- -- @module Ops.ArmyGroup -- @image OPS_ArmyGroup.png @@ -15,6 +32,7 @@ -- @type ARMYGROUP -- @field #boolean adinfinitum Resume route at first waypoint when final waypoint is reached. -- @field #boolean formationPerma Formation that is used permanently and overrules waypoint formations. +-- @field #boolean isMobile If true, group is mobile. -- @extends Ops.OpsGroup#OPSGROUP --- *Your soul may belong to Jesus, but your ass belongs to the marines.* -- Eugene B. Sledge @@ -36,7 +54,12 @@ ARMYGROUP = { --- Army group element. -- @type ARMYGROUP.Element -- @field #string name Name of the element, i.e. the unit. +-- @field Wrapper.Unit#UNIT unit The UNIT object. +-- @field #string status The element status. -- @field #string typename Type name. +-- @field #number length Length of element in meters. +-- @field #number width Width of element in meters. +-- @field #number height Height of element in meters. --- Army Group version. -- @field #string version @@ -45,8 +68,11 @@ ARMYGROUP.version="0.3.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- TODO: A lot. + +-- TODO: Suppression of fire. +-- TODO: Check if group is mobile. +-- TODO: F10 menu. +-- DONE: Rearm. Specify a point where to go and wait until ammo is full. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -54,12 +80,12 @@ ARMYGROUP.version="0.3.0" --- Create a new ARMYGROUP class object. -- @param #ARMYGROUP self --- @param #string GroupName Name of the group. +-- @param Wrapper.Group#GROUP Group The group object. Can also be given by its group name as `#string`. -- @return #ARMYGROUP self -function ARMYGROUP:New(GroupName) +function ARMYGROUP:New(Group) -- Inherit everything from FSM class. - local self=BASE:Inherit(self, OPSGROUP:New(GroupName)) -- #ARMYGROUP + local self=BASE:Inherit(self, OPSGROUP:New(Group)) -- #ARMYGROUP -- Set some string id for output to DCS.log file. self.lid=string.format("ARMYGROUP %s | ", self.groupname) @@ -73,10 +99,14 @@ function ARMYGROUP:New(GroupName) -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("*", "FullStop", "Holding") -- Hold position. - self:AddTransition("*", "Cruise", "Cruising") -- Hold position. - + self:AddTransition("*", "Cruise", "Cruising") -- Cruise along the given route of waypoints. + self:AddTransition("*", "Detour", "OnDetour") -- Make a detour to a coordinate and resume route afterwards. self:AddTransition("OnDetour", "DetourReached", "Cruising") -- Group reached the detour coordinate. + + self:AddTransition("*", "Rearm", "Rearm") -- Group is send to a coordinate and waits until ammo is refilled. + self:AddTransition("Rearm", "Rearming", "Rearming") -- Group has arrived at the rearming coodinate and is waiting to be fully rearmed. + self:AddTransition("Rearming", "Rearmed", "Cruising") -- Group was rearmed. ------------------------ --- Pseudo Functions --- @@ -102,7 +132,9 @@ function ARMYGROUP:New(GroupName) -- Handle events: self:HandleEvent(EVENTS.Birth, self.OnEventBirth) self:HandleEvent(EVENTS.Dead, self.OnEventDead) - self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) + self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) + + --self:HandleEvent(EVENTS.Hit, self.OnEventHit) -- Start the status monitoring. self:__Status(-1) @@ -164,6 +196,8 @@ end -- @return Ops.OpsGroup#OPSGROUP.Task The task table. function ARMYGROUP:AddTaskFireAtPoint(Coordinate, Clock, Radius, Nshots, WeaponType, Prio) + Coordinate=self:_CoordinateFromObject(Coordinate) + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType) local task=self:AddTask(DCStask, Clock, nil, Prio) @@ -182,6 +216,8 @@ end -- @return Ops.OpsGroup#OPSGROUP.Task The task table. function ARMYGROUP:AddTaskWaypointFireAtPoint(Coordinate, Waypoint, Radius, Nshots, WeaponType, Prio) + Coordinate=self:_CoordinateFromObject(Coordinate) + Waypoint=Waypoint or self:GetWaypointNext() local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType) @@ -224,7 +260,7 @@ end --- Check if the group is currently on a detour. -- @param #ARMYGROUP self --- @return #boolean If true, group is on a detour +-- @return #boolean If true, group is on a detour. function ARMYGROUP:IsOnDetour() return self:Is("OnDetour") end @@ -265,10 +301,13 @@ function ARMYGROUP:onafterStatus(From, Event, To) if self.detectionOn then self:_CheckDetectedUnits() end + + -- Check ammo status. + self:_CheckAmmoStatus() -- Update position etc. self:_UpdatePosition() - + -- Check if group got stuck. self:_CheckStuck() @@ -282,11 +321,11 @@ function ARMYGROUP:onafterStatus(From, Event, To) local alarm=self:GetAlarmstate() local speed=UTILS.MpsToKnots(self.velocity) local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) - local formation=self.option.Formation + local formation=self.option.Formation or "unknown" -- Info text. - local text=string.format("%s: Wp=%d/%d-->%d Speed=%.1f (%d) Heading=%03d ROE=%d Alarm=%d Formation=%s Tasks=%d Missions=%d", - fsmstate, self.currentwp, #self.waypoints, self:GetWaypointIndexNext(), speed, speedEx, self.heading, roe, alarm, formation, nTaskTot, nMissions) + local text=string.format("%s [ROE-AS=%d-%d T/M=%d/%d]: Wp=%d/%d-->%d (final %s), Speed=%.1f (%d), Heading=%03d", + fsmstate, roe, alarm, nTaskTot, nMissions, self.currentwp, #self.waypoints, self:GetWaypointIndexNext(), tostring(self.passedfinalwp), speed, speedEx, self.heading) self:I(self.lid..text) end @@ -340,7 +379,7 @@ function ARMYGROUP:onafterSpawned(From, Event, To) -- Update position. self:_UpdatePosition() - if self.ai then + if self.isAI then -- Set default ROE. self:SwitchROE(self.option.ROE) @@ -358,10 +397,19 @@ function ARMYGROUP:onafterSpawned(From, Event, To) self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, true) end + -- Formation + if not self.option.Formation then + self.option.Formation=self.optionDefault.Formation + end + end -- Update route. - self:Cruise(nil, self.option.Formation or self.optionDefault.Formation) + if #self.waypoints>1 then + self:Cruise(nil, self.option.Formation or self.optionDefault.Formation) + else + self:FullStop() + end end @@ -375,6 +423,10 @@ end -- @param #number Formation Formation of the group. function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Formation) + -- Debug info. + local text=string.format("Update route n=%s, Speed=%s, Formation=%s", tostring(n), tostring(Speed), tostring(Formation)) + self:T(self.lid..text) + -- Update route from this waypoint number onwards. n=n or self:GetWaypointIndexNext(self.adinfinitum) @@ -451,9 +503,6 @@ function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Formation) local wp=_wp local text=string.format("WP #%d UID=%d type=%s: Speed=%d m/s, alt=%d m, Action=%s", i, wp.uid and wp.uid or 0, wp.type, wp.speed, wp.alt, wp.action) self:T(text) - if false and wp.coordinate then - wp.coordinate:MarkToAll(text) - end end end @@ -479,6 +528,43 @@ function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Formation) end +--- On after "GotoWaypoint" event. Group will got to the given waypoint and execute its route from there. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number UID The goto waypoint unique ID. +-- @param #number Speed (Optional) Speed to waypoint in knots. +-- @param #number Formation (Optional) Formation to waypoint. +function ARMYGROUP:onafterGotoWaypoint(From, Event, To, UID, Speed, Formation) + + local n=self:GetWaypointIndex(UID) + + --env.info(string.format("FF AG Goto waypoint UID=%s Index=%s, Speed=%s, Formation=%s", tostring(UID), tostring(n), tostring(Speed), tostring(Formation))) + + if n then + + -- TODO: switch to re-enable waypoint tasks. + if false then + local tasks=self:GetTasksWaypoint(n) + + for _,_task in pairs(tasks) do + local task=_task --Ops.OpsGroup#OPSGROUP.Task + task.status=OPSGROUP.TaskStatus.SCHEDULED + end + + end + + -- Speed to waypoint. + Speed=Speed or self:GetSpeedToWaypoint(n) + + -- Update the route. + self:UpdateRoute(n, Speed, Formation) + + end + +end + --- On after "Detour" event. -- @param #ARMYGROUP self -- @param #string From From state. @@ -508,6 +594,55 @@ function ARMYGROUP:onafterDetour(From, Event, To, Coordinate, Speed, Formation, end +--- On after "Rearm" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onafterRearm(From, Event, To, Coordinate, Formation) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Add waypoint after current. + local wp=self:AddWaypoint(Coordinate, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + wp.detour=0 + +end + +--- On after "Rearming" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterRearming(From, Event, To) + + -- Get current position. + local pos=self:GetCoordinate() + + -- Create a new waypoint. + local wp=pos:WaypointGround(0) + + -- Create new route consisting of only this position ==> Stop! + self:Route({wp}) + +end + +--- On after "Rearmed" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterRearmed(From, Event, To) + + self:_CheckGroupDone(1) + +end + --- On after "DetourReached" event. -- @param #ARMYGROUP self -- @param #string From From state. @@ -549,7 +684,7 @@ function ARMYGROUP:onafterCruise(From, Event, To, Speed, Formation) end ---- On after Start event. Starts the ARMYGROUP FSM and event handlers. +--- On after "Stop" event. -- @param #ARMYGROUP self -- @param #string From From state. -- @param #string Event Event. @@ -631,7 +766,7 @@ function ARMYGROUP:OnEventDead(EventData) end ---- Event function handling the crash of a unit. +--- Event function handling when a unit is removed from the game. -- @param #ARMYGROUP self -- @param Core.Event#EVENTDATA EventData Event data. function ARMYGROUP:OnEventRemoveUnit(EventData) @@ -654,6 +789,22 @@ function ARMYGROUP:OnEventRemoveUnit(EventData) end +--- Event function handling when a unit is hit. +-- @param #ARMYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function ARMYGROUP:OnEventHit(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- TODO: suppression + + end +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Routing ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -668,6 +819,8 @@ end -- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation, Updateroute) + local coordinate=self:_CoordinateFromObject(Coordinate) + -- Set waypoint index. local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) @@ -680,7 +833,7 @@ function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation Speed=Speed or self:GetSpeedCruise() -- Create a Naval waypoint. - local wp=Coordinate:WaypointGround(UTILS.KnotsToKmph(Speed), Formation) + local wp=coordinate:WaypointGround(UTILS.KnotsToKmph(Speed), Formation) -- Create waypoint data table. local waypoint=self:_CreateWaypoint(wp) @@ -689,15 +842,15 @@ function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation self:_AddWaypoint(waypoint, wpnumber) -- Get closest point to road. - waypoint.roadcoord=Coordinate:GetClosestPointToRoad(false) + waypoint.roadcoord=coordinate:GetClosestPointToRoad(false) if waypoint.roadcoord then - waypoint.roaddist=Coordinate:Get2DDistance(waypoint.roadcoord) + waypoint.roaddist=coordinate:Get2DDistance(waypoint.roadcoord) else waypoint.roaddist=1000*1000 --1000 km. end -- Debug info. - self:I(self.lid..string.format("Adding waypoint UID=%d (index=%d), Speed=%.1f knots, Dist2Road=%d m, Action=%s", waypoint.uid, wpnumber, Speed, waypoint.roaddist, waypoint.action)) + self:T(self.lid..string.format("Adding waypoint UID=%d (index=%d), Speed=%.1f knots, Dist2Road=%d m, Action=%s", waypoint.uid, wpnumber, Speed, waypoint.roaddist, waypoint.action)) -- Update route. if Updateroute==nil or Updateroute==true then @@ -726,8 +879,8 @@ function ARMYGROUP:_InitGroup() self.isNaval=false self.isGround=true - -- Ships are always AI. - self.ai=true + -- Ground are always AI. + self.isAI=true -- Is (template) group late activated. self.isLateActivated=self.template.lateActivation @@ -764,51 +917,63 @@ function ARMYGROUP:_InitGroup() for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT + + -- TODO: this is wrong when grouping is used! + local unittemplate=unit:GetTemplate() local element={} --#ARMYGROUP.Element element.name=unit:GetName() - element.typename=unit:GetTypeName() + element.unit=unit element.status=OPSGROUP.ElementStatus.INUTERO - element.unit=unit + element.typename=unit:GetTypeName() + element.skill=unittemplate.skill or "Unknown" + element.ai=true + element.category=element.unit:GetUnitCategory() + element.categoryname=element.unit:GetCategoryName() + element.size, element.length, element.height, element.width=unit:GetObjectSize() + element.ammo0=self:GetAmmoUnit(unit, false) + + -- Debug text. + if self.verbose>=2 then + local text=string.format("Adding element %s: status=%s, skill=%s, category=%s (%d), size: %.1f (L=%.1f H=%.1f W=%.1f)", + element.name, element.status, element.skill, element.categoryname, element.category, element.size, element.length, element.height, element.width) + self:I(self.lid..text) + end + + -- Add element to table. table.insert(self.elements, element) - self:GetAmmoUnit(unit, false) + -- Get Descriptors. + self.descriptors=self.descriptors or unit:GetDesc() - if unit:IsAlive() then + -- Set type name. + self.actype=self.actype or unit:GetTypeName() + + if unit:IsAlive() then + -- Trigger spawned event. self:ElementSpawned(element) end end - -- Get first unit. This is used to extract other parameters. - local unit=self.group:GetUnit(1) - - if unit then - - self.descriptors=unit:GetDesc() - - self.actype=unit:GetTypeName() - - -- Debug info. - if self.verbose>=1 then - local text=string.format("Initialized Army Group %s:\n", self.groupname) - text=text..string.format("Unit type = %s\n", self.actype) - text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) - text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) - text=text..string.format("Elements = %d\n", #self.elements) - text=text..string.format("Waypoints = %d\n", #self.waypoints) - text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) - text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles) - text=text..string.format("FSM state = %s\n", self:GetState()) - text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) - text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) - self:I(self.lid..text) - end - - -- Init done. - self.groupinitialized=true - + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Army Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + self:I(self.lid..text) end + + -- Init done. + self.groupinitialized=true return self end diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 002509c7d..1153ba5f6 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -1,18 +1,26 @@ --- **Ops** - Auftrag (mission) for Ops. -- --- **Main Features:** +-- ## Main Features: -- --- * Simplifies defining and executing DCS tasks. --- * Additional useful events. --- * Set mission start/stop times. --- * Set mission priority and urgency (can cancel running missions). +-- * Simplifies defining and executing DCS tasks +-- * Additional useful events +-- * Set mission start/stop times +-- * Set mission priority and urgency (can cancel running missions) -- * Specific mission options for ROE, ROT, formation, etc. --- * Interface to FLIGHTGROUP, AIRWING and WINGCOMMANDER classes. --- * FSM events when a mission is done, successful or failed. --- +-- * Compatible with FLIGHTGROUP, NAVYGROUP, ARMYGROUP, AIRWING, WINGCOMMANDER and CHIEF classes +-- * FSM events when a mission is done, successful or failed +-- -- === -- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Auftrag). +-- +-- === +-- -- ### Author: **funkyfranky** +-- +-- === -- @module Ops.Auftrag -- @image OPS_Auftrag.png @@ -1682,6 +1690,13 @@ function AUFTRAG:GetPriority() return self.prio end +--- Get casualties, i.e. number of units that died during this mission. +-- @param #AUFTRAG self +-- @return #number Number of dead units. +function AUFTRAG:GetCasualties() + return self.Ncasualties or 0 +end + --- Check if mission is "urgent". -- @param #AUFTRAG self -- @return #boolean If `true`, mission is "urgent". @@ -1942,7 +1957,7 @@ function AUFTRAG:IsReadyToCancel() return true end - + -- Evaluate failure condition. One is enough. local failure=self:EvalConditionsAny(self.conditionFailure) if failure then @@ -1950,6 +1965,7 @@ function AUFTRAG:IsReadyToCancel() return true end + -- Evaluate success consitions. One is enough. local success=self:EvalConditionsAny(self.conditionSuccess) if success then diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 81a8f4bf6..87e207d8f 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -1,16 +1,30 @@ --- **Ops** - Enhanced Airborne Group. -- --- **Main Features:** +-- ## Main Features: -- --- * Monitor flight status of elements and/or the entire group. --- * Monitor fuel and ammo status. --- * Conveniently set radio freqencies, TACAN, ROE etc. --- * Sophisticated task queueing system. --- * Many additional events for each element and the whole group. +-- * Monitor flight status of elements and/or the entire group +-- * Monitor fuel and ammo status +-- * Conveniently set radio freqencies, TACAN, ROE etc +-- * Order helos to land at specifc coordinates +-- * Dynamically add and remove waypoints +-- * Sophisticated task queueing system (know when DCS tasks start and end) +-- * Convenient checks when the group enters or leaves a zone +-- * Detection events for new, known and lost units +-- * Simple LASER and IR-pointer setup +-- * Compatible with AUFTRAG class +-- * Many additional events that the mission designer can hook into -- -- === -- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Flightgroup). +-- +-- === +-- -- ### Author: **funkyfranky** +-- +-- === -- @module Ops.FlightGroup -- @image OPS_FlightGroup.png @@ -50,7 +64,7 @@ -- -- @extends Ops.OpsGroup#OPSGROUP ---- *To invent an airplane is nothing. To build one is something. To fly is everything.* -- Otto Lilienthal +--- *To invent an airplane is nothing; to build one is something; to fly is everything.* -- Otto Lilienthal -- -- === -- @@ -186,6 +200,7 @@ FLIGHTGROUP.version="0.6.0" -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: VTOL aircraft. -- TODO: Use new UnitLost event instead of crash/dead. -- TODO: Options EPLRS, Afterburner restrict etc. -- DONE: Add TACAN beacon. @@ -210,7 +225,7 @@ FLIGHTGROUP.version="0.6.0" --- Create a new FLIGHTGROUP object and start the FSM. -- @param #FLIGHTGROUP self --- @param Wrapper.Group#GROUP group The group object. Can also be given as #string with the name of the group. +-- @param Wrapper.Group#GROUP Group The group object. Can also be given by its group name as `#string`. -- @return #FLIGHTGROUP self function FLIGHTGROUP:New(group) @@ -396,7 +411,7 @@ function FLIGHTGROUP:SetFlightControl(flightcontrol) table.insert(flightcontrol.flights, self) -- Update flight's F10 menu. - if self.ai==false then + if self.isAI==false then self:_UpdateMenu(0.5) end @@ -705,7 +720,7 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Update status. --- @param #FLIHGTGROUP self +-- @param #FLIGHTGROUP self function FLIGHTGROUP:onbeforeStatus(From, Event, To) -- First we check if elements are still alive. Could be that they were despawned without notice, e.g. when landing on a too small airbase. @@ -1040,7 +1055,7 @@ function FLIGHTGROUP:OnEventEngineStartup(EventData) -- TODO: could be that this element is part of a human flight group. -- Problem: when player starts hot, the AI does too and starts to taxi immidiately :( -- when player starts cold, ? - if self.ai then + if self.isAI then self:ElementEngineOn(element) else if element.ai then @@ -1447,7 +1462,7 @@ function FLIGHTGROUP:onafterSpawned(From, Event, To) -- Update position. self:_UpdatePosition() - if self.ai then + if self.isAI then -- Set ROE. self:SwitchROE(self.option.ROE) @@ -1522,7 +1537,7 @@ function FLIGHTGROUP:onafterParking(From, Event, To) self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.PARKING) -- Update player menu. - if not self.ai then + if not self.isAI then self:_UpdateMenu(0.5) end @@ -1547,7 +1562,7 @@ function FLIGHTGROUP:onafterTaxiing(From, Event, To) if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then -- Add AI flight to takeoff queue. - if self.ai then + if self.isAI then -- AI flights go directly to TAKEOFF as we don't know when they finished taxiing. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAKEOFF) else @@ -1586,7 +1601,7 @@ end function FLIGHTGROUP:onafterAirborne(From, Event, To) self:T(self.lid..string.format("Flight airborne")) - if self.ai then + if self.isAI then self:_CheckGroupDone(1) else self:_UpdateMenu() @@ -1737,7 +1752,7 @@ function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n) --end -- Only AI flights. - if not self.ai then + if not self.isAI then allowed=false end @@ -1840,7 +1855,7 @@ end -- @param #number delay Delay in seconds. function FLIGHTGROUP:_CheckGroupDone(delay) - if self:IsAlive() and self.ai then + if self:IsAlive() and self.isAI then if delay and delay>0 then -- Delayed call. @@ -2099,7 +2114,7 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp end - if self.ai then + if self.isAI then local routeto=false if fc or world.event.S_EVENT_KILL then @@ -2279,7 +2294,7 @@ function FLIGHTGROUP:onafterHolding(From, Event, To) -- Set flight status to holding self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.HOLDING) - if not self.ai then + if not self.isAI then self:_UpdateMenu() end @@ -2626,10 +2641,10 @@ function FLIGHTGROUP:_InitGroup() self.tacan=UTILS.DeepCopy(self.tacanDefault) -- Is this purely AI? - self.ai=not self:_IsHuman(group) + self.isAI=not self:_IsHuman(group) -- Create Menu. - if not self.ai then + if not self.isAI then self.menu=self.menu or {} self.menu.atc=self.menu.atc or {} self.menu.atc.root=self.menu.atc.root or MENU_GROUP:New(self.group, "ATC") @@ -2665,7 +2680,7 @@ function FLIGHTGROUP:_InitGroup() text=text..string.format("Ceiling = %.1f feet\n", UTILS.MetersToFeet(self.ceiling)) text=text..string.format("Tanker type = %s\n", tostring(self.tankertype)) text=text..string.format("Refuel type = %s\n", tostring(self.refueltype)) - text=text..string.format("AI = %s\n", tostring(self.ai)) + text=text..string.format("AI = %s\n", tostring(self.isAI)) text=text..string.format("Helicopter = %s\n", tostring(self.group:IsHelicopter())) text=text..string.format("Elements = %d\n", #self.elements) text=text..string.format("Waypoints = %d\n", #self.waypoints) @@ -2729,6 +2744,7 @@ function FLIGHTGROUP:AddElementByName(unitname) element.ai=true end + -- Debug text. local text=string.format("Adding element %s: status=%s, skill=%s, modex=%s, fuelmass=%.1f (%d), category=%d, categoryname=%s, callsign=%s, ai=%s", element.name, element.status, element.skill, element.modex, element.fuelmass, element.fuelrel*100, element.category, element.categoryname, element.callsign, tostring(element.ai)) self:T(self.lid..text) diff --git a/Moose Development/Moose/Ops/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua index 1ef4a3473..6c6f54d44 100644 --- a/Moose Development/Moose/Ops/NavyGroup.lua +++ b/Moose Development/Moose/Ops/NavyGroup.lua @@ -1,15 +1,33 @@ --- **Ops** - Enhanced Naval Group. -- --- **Main Features:** +-- ## Main Features: -- --- * Dynamically add and remove waypoints. --- * Let the group steam into the wind. --- * Command a full stop. --- * Let a submarine dive and surface. --- +-- * Let the group steam into the wind +-- * Command a full stop +-- * Patrol waypoints *ad infinitum* +-- * Collision warning, if group is heading towards a land mass +-- * Automatic pathfinding, e.g. around islands +-- * Let a submarine dive and surface +-- * Manage TACAN and ICLS beacons +-- * Dynamically add and remove waypoints +-- * Sophisticated task queueing system (know when DCS tasks start and end) +-- * Convenient checks when the group enters or leaves a zone +-- * Detection events for new, known and lost units +-- * Simple LASER and IR-pointer setup +-- * Compatible with AUFTRAG class +-- * Many additional events that the mission designer can hook into +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Navygroup) +-- -- === -- -- ### Author: **funkyfranky** +-- +-- === -- @module Ops.NavyGroup -- @image OPS_NavyGroup.png @@ -19,6 +37,7 @@ -- @field #boolean turning If true, group is currently turning. -- @field #NAVYGROUP.IntoWind intowind Into wind info. -- @field #table Qintowind Queue of "into wind" turns. +-- @field #number intowindcounter Counter of into wind IDs. -- @field #number depth Ordered depth in meters. -- @field #boolean collisionwarning If true, collition warning. -- @field #boolean pathfindingOn If true, enable pathfining. @@ -74,7 +93,9 @@ NAVYGROUP.version="0.5.0" -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Collision warning. +-- TODO: Extend, shorten turn into wind windows +-- TODO: Skipper menu. +-- DONE: Collision warning. -- DONE: Detour, add temporary waypoint and resume route. -- DONE: Stop and resume route. -- DONE: Add waypoints. @@ -109,7 +130,9 @@ function NAVYGROUP:New(GroupName) self:AddTransition("*", "Cruise", "Cruising") -- Hold position. self:AddTransition("*", "TurnIntoWind", "IntoWind") -- Command the group to turn into the wind. - self:AddTransition("*", "TurnIntoWindOver", "Cruising") -- Turn into wind is over. + self:AddTransition("IntoWind", "TurnedIntoWind", "IntoWind") -- Group turned into wind. + self:AddTransition("IntoWind", "TurnIntoWindStop", "IntoWind") -- Stop a turn into wind. + self:AddTransition("IntoWind", "TurnIntoWindOver", "Cruising") -- Turn into wind is over. self:AddTransition("*", "TurningStarted", "*") -- Group started turning. self:AddTransition("*", "TurningStopped", "*") -- Group stopped turning. @@ -144,14 +167,6 @@ function NAVYGROUP:New(GroupName) -- Initialize the group. self:_InitGroup() - -- Debug trace. - if false then - self.Debug=true - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - end - -- Handle events: self:HandleEvent(EVENTS.Birth, self.OnEventBirth) self:HandleEvent(EVENTS.Dead, self.OnEventDead) @@ -272,7 +287,7 @@ function NAVYGROUP:AddTaskAttackGroup(TargetGroup, WeaponExpend, WeaponType, Clo return task end ---- Add aircraft recovery time window and recovery case. +--- Create a turn into wind window. Note that this is not executed as it not added to the queue. -- @param #NAVYGROUP self -- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. -- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. @@ -280,7 +295,7 @@ end -- @param #boolean uturn If true (or nil), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint. -- @param #number offset Offset angle in degrees, e.g. to account for an angled runway. Default 0 deg. -- @return #NAVYGROUP.IntoWind Recovery window. -function NAVYGROUP:CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset) +function NAVYGROUP:_CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset) -- Absolute mission time in seconds. local Tnow=timer.getAbsTime() @@ -335,17 +350,17 @@ function NAVYGROUP:CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset) return recovery end ---- Add aircraft recovery time window and recovery case. +--- Add a time window, where the groups steams into the wind. -- @param #NAVYGROUP self -- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. -- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. -- @param #number speed Speed in knots during turn into wind leg. --- @param #boolean uturn If true (or nil), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint. +-- @param #boolean uturn If `true` (or `nil`), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint. -- @param #number offset Offset angle in degrees, e.g. to account for an angled runway. Default 0 deg. --- @return #NAVYGROUP.IntoWind Recovery window. +-- @return #NAVYGROUP.IntoWind Turn into window data table. function NAVYGROUP:AddTurnIntoWind(starttime, stoptime, speed, uturn, offset) - local recovery=self:CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset) + local recovery=self:_CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset) --TODO: check if window is overlapping with an other and if extend the window. @@ -355,6 +370,31 @@ function NAVYGROUP:AddTurnIntoWind(starttime, stoptime, speed, uturn, offset) return recovery end +--- Remove steam into wind window from queue. If the window is currently active, it is stopped first. +-- @param #NAVYGROUP self +-- @param #NAVYGROUP.IntoWind IntoWindData Turn into window data table. +-- @return #NAVYGROUP self +function NAVYGROUP:RemoveTurnIntoWind(IntoWindData) + + -- Check if this is a window currently open. + if self.intowind and self.intowind.Id==IntoWindData.Id then + --env.info("FF stop in remove") + self:TurnIntoWindStop() + return + end + + for i,_tiw in pairs(self.Qintowind) do + local tiw=_tiw --#NAVYGROUP.IntoWind + if tiw.Id==IntoWindData.Id then + --env.info("FF removing window "..tiw.Id) + table.remove(self.Qintowind, i) + break + end + end + + return self +end + --- Check if the group is currently holding its positon. -- @param #NAVYGROUP self @@ -527,6 +567,37 @@ function NAVYGROUP:onafterStatus(From, Event, To) end + --- + -- Recovery Windows + --- + + if self.verbose>=2 then + + -- Debug output: + local text=string.format(self.lid.."Turn into wind time windows:") + + -- Handle case with no recoveries. + if #self.Qintowind==0 then + text=text.." none!" + end + + -- Loop over all slots. + for i,_recovery in pairs(self.Qintowind) do + local recovery=_recovery --#NAVYGROUP.IntoWind + + -- Get start/stop clock strings. + local Cstart=UTILS.SecondsToClock(recovery.Tstart) + local Cstop=UTILS.SecondsToClock(recovery.Tstop) + + -- Debug text. + text=text..string.format("\n[%d] ID=%d Start=%s Stop=%s Open=%s Over=%s", i, recovery.Id, Cstart, Cstop, tostring(recovery.Open), tostring(recovery.Over)) + end + + -- Debug output. + self:I(self.lid..text) + + end + --- -- Tasks & Missions @@ -568,7 +639,7 @@ function NAVYGROUP:onafterSpawned(From, Event, To) -- Update position. self:_UpdatePosition() - if self.ai then + if self.isAI then -- Set default ROE. self:SwitchROE(self.option.ROE) @@ -592,7 +663,11 @@ function NAVYGROUP:onafterSpawned(From, Event, To) end -- Update route. - self:Cruise() + if #self.waypoints>1 then + self:Cruise() + else + self:FullStop() + end end @@ -757,36 +832,83 @@ function NAVYGROUP:onafterTurnIntoWind(From, Event, To, IntoWind) end +--- On before "TurnIntoWindStop" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onbeforeTurnIntoWindStop(From, Event, To) + + if self.intowind then + return true + else + return false + end + +end + +--- On after "TurnIntoWindStop" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterTurnIntoWindStop(From, Event, To) + self:TurnIntoWindOver(self.intowind) +end + --- On after "TurnIntoWindOver" event. -- @param #NAVYGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number Duration Duration in seconds. --- @param #number Speed Speed in knots. --- @param #boolean Uturn Return to the place we came from. -function NAVYGROUP:onafterTurnIntoWindOver(From, Event, To) +-- @param #NAVYGROUP.IntoWind IntoWindData Data table. +function NAVYGROUP:onafterTurnIntoWindOver(From, Event, To, IntoWindData) - -- Debug message. - self:T2(self.lid.."Turn Into Wind Over!") + if IntoWindData and self.intowind and IntoWindData.Id==self.intowind.Id then - self.intowind.Over=true - self.intowind.Open=false + -- Debug message. + self:T2(self.lid.."Turn Into Wind Over!") - -- Remove additional waypoint. - self:RemoveWaypointByID(self.intowind.waypoint.uid) + -- Window over and not open anymore. + self.intowind.Over=true + self.intowind.Open=false + + -- Remove additional waypoint. + self:RemoveWaypointByID(self.intowind.waypoint.uid) + + if self.intowind.Uturn then + + --- + -- U-turn ==> Go to coordinate where we left the route. + --- + + -- Detour to where we left the route. + self:T(self.lid.."FF Turn Into Wind Over ==> Uturn!") + self:Detour(self.intowind.Coordinate, self:GetSpeedCruise(), 0, true) + + else + + --- + -- Go directly to next waypoint. + --- + + -- Next waypoint index and speed. + local indx=self:GetWaypointIndexNext() + local speed=self:GetWaypointSpeed(indx) + + -- Update route. + self:T(self.lid..string.format("FF Turn Into Wind Over ==> Next WP Index=%d at %.1f knots via update route!", indx, speed)) + self:__UpdateRoute(-1, indx, speed) + + end + + -- Set current window to nil. + self.intowind=nil + + -- Remove window from queue. + self:RemoveTurnIntoWind(IntoWindData) - if self.intowind.Uturn then - self:T(self.lid.."Turn Into Wind Over ==> Uturn!") - self:Detour(self.intowind.Coordinate, self:GetSpeedCruise(), 0, true) - else - self:T(self.lid.."FF Turn Into Wind Over ==> Next WP!") - local indx=self:GetWaypointIndexNext() - local speed=self:GetWaypointSpeed(indx) - self:__UpdateRoute(-1, indx, speed) end - - self.intowind=nil end @@ -874,6 +996,11 @@ end function NAVYGROUP:onafterTurningStopped(From, Event, To) self.turning=false self.collisionwarning=false + + if self:IsSteamingIntoWind() then + self:TurnedIntoWind() + end + end --- On after "CollisionWarning" event. @@ -1009,10 +1136,10 @@ function NAVYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Depth, Up -- Check if a coordinate was given or at least a positionable. if not Coordinate:IsInstanceOf("COORDINATE") then if Coordinate:IsInstanceOf("POSITIONABLE") or Coordinate:IsInstanceOf("ZONE_BASE") then - self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE. Trying to get coordinate") + self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") Coordinate=Coordinate:GetCoordinate() else - self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE!") + self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") return nil end end @@ -1071,7 +1198,7 @@ function NAVYGROUP:_InitGroup() --self.isSubmarine=self.group:IsSubmarine() -- Ships are always AI. - self.ai=true + self.isAI=true -- Is (template) group late activated. self.isLateActivated=self.template.lateActivation @@ -1111,51 +1238,64 @@ function NAVYGROUP:_InitGroup() for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT + -- Get unit template. + local unittemplate=unit:GetTemplate() + local element={} --#NAVYGROUP.Element element.name=unit:GetName() - element.typename=unit:GetTypeName() - element.status=OPSGROUP.ElementStatus.INUTERO element.unit=unit + element.status=OPSGROUP.ElementStatus.INUTERO + element.typename=unit:GetTypeName() + element.skill=unittemplate.skill or "Unknown" + element.ai=true + element.category=element.unit:GetUnitCategory() + element.categoryname=element.unit:GetCategoryName() + element.size, element.length, element.height, element.width=unit:GetObjectSize() + element.ammo0=self:GetAmmoUnit(unit, false) + + -- Debug text. + if self.verbose>=2 then + local text=string.format("Adding element %s: status=%s, skill=%s, category=%s (%d), size: %.1f (L=%.1f H=%.1f W=%.1f)", + element.name, element.status, element.skill, element.categoryname, element.category, element.size, element.length, element.height, element.width) + self:I(self.lid..text) + end + + -- Add element to table. table.insert(self.elements, element) + + -- Get Descriptors. + self.descriptors=self.descriptors or unit:GetDesc() - self:GetAmmoUnit(unit, false) + -- Set type name. + self.actype=self.actype or unit:GetTypeName() - if unit:IsAlive() then + if unit:IsAlive() then + -- Trigger spawned event. self:ElementSpawned(element) end end - -- Get first unit. This is used to extract other parameters. - local unit=self.group:GetUnit(1) - - if unit then - - self.descriptors=unit:GetDesc() - - self.actype=unit:GetTypeName() - - -- Debug info. - if self.verbose>=1 then - local text=string.format("Initialized Navy Group %s:\n", self.groupname) - text=text..string.format("Unit type = %s\n", self.actype) - text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) - text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) - text=text..string.format("Elements = %d\n", #self.elements) - text=text..string.format("Waypoints = %d\n", #self.waypoints) - text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) - text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos) - text=text..string.format("FSM state = %s\n", self:GetState()) - text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) - text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) - self:I(self.lid..text) - end - - -- Init done. - self.groupinitialized=true + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Navy Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + self:I(self.lid..text) end + -- Init done. + self.groupinitialized=true + return self end @@ -1306,66 +1446,60 @@ function NAVYGROUP:_CheckTurnsIntoWind() -- Get current abs time. local time=timer.getAbsTime() - local Cnow=UTILS.SecondsToClock(time) - -- Debug output: - local text=string.format(self.lid.."Recovery time windows:") + if self.intowind then - -- Handle case with no recoveries. - if #self.Qintowind==0 then - text=text.." none!" - end + -- Check if time is over. + if time>=self.intowind.Tstop then + self:TurnIntoWindOver(self.intowind) + end + + else + + -- Get next window. + local IntoWind=self:GetTurnIntoWindNext() - -- Sort windows wrt to start time. - table.sort(self.Qintowind, function(a, b) return a.Tstart=recovery.Tstart and time=self.intowind.Tstop then - self:TurnIntoWindOver() - end - end end ---- Check queued turns into wind. +--- Get the next turn into wind window, which is not yet running. -- @param #NAVYGROUP self --- @return #NAVYGROUP.IntoWind Next into wind data. -function NAVYGROUP:GetNextTurnIntoWind() +-- @return #NAVYGROUP.IntoWind Next into wind data. Could be `nil` if there is not next window. +function NAVYGROUP:GetTurnIntoWindNext() - -- Loop over all windows. - for _,_recovery in pairs(self.Qintowind) do - local recovery=_recovery --#NAVYGROUP.IntoWind - + if #self.Qintowind>0 then + + -- Get current abs time. + local time=timer.getAbsTime() + + -- Sort windows wrt to start time. + table.sort(self.Qintowind, function(a, b) return a.Tstart=recovery.Tstart and time=ThreatLevelMin and threatlevel<=ThreatLevelMax then + + if threatlevellevelmax then + threat=unit + levelmax=threatlevel + end + + end + + return threat, levelmax +end + +--- Check if an element of the group has line of sight to a coordinate. +-- @param #OPSGROUP self +-- @param Core.Point#COORDINATE Coordinate The position to which we check the LoS. +-- @param #OPSGROUP.Element Element The (optinal) element. If not given, all elements are checked. +-- @param DCS#Vec3 OffsetElement Offset vector of the element. +-- @param DCS#Vec3 OffsetCoordinate Offset vector of the coordinate. +-- @return #boolean If `true`, there is line of sight to the specified coordinate. +function OPSGROUP:HasLoS(Coordinate, Element, OffsetElement, OffsetCoordinate) + + -- Target vector. + local Vec3=Coordinate:GetVec3() + + -- Optional offset. + if OffsetCoordinate then + Vec3=UTILS.VecAdd(Vec3, OffsetCoordinate) + end + + --- Function to check LoS for an element of the group. + local function checklos(element) + local vec3=element.unit:GetVec3() + if OffsetElement then + vec3=UTILS.VecAdd(vec3, OffsetElement) + end + local _los=land.isVisible(vec3, Vec3) + --self:I({los=_los, source=vec3, target=Vec3}) + return _los + end + + if Element then + local los=checklos(Element) + return los + else + + for _,element in pairs(self.elements) do + -- Get LoS of this element. + local los=checklos(element) + if los then + return true + end + end + + return false + end + + return nil end --- Get MOOSE GROUP object. @@ -999,6 +1217,21 @@ function OPSGROUP:HasPassedFinalWaypoint() return self.passedfinalwp end +--- Check if the group is currently rearming. +-- @param #OPSGROUP self +-- @return #boolean If true, group is rearming. +function OPSGROUP:IsRearming() + local rearming=self:Is("Rearming") or self:Is("Rearm") + return rearming +end + +--- Check if the group has currently switched a LASER on. +-- @param #OPSGROUP self +-- @return #boolean If true, LASER of the group is on. +function OPSGROUP:IsLasing() + return self.spot.On +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Waypoint Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1736,7 +1969,6 @@ function OPSGROUP:CountRemainingTasks() Ntot=Ntot+1 if task.type==OPSGROUP.TaskType.WAYPOINT then - --TODO: maybe check that waypoint was not already passed? Nwp=Nwp+1 elseif task.type==OPSGROUP.TaskType.SCHEDULED then Nsched=Nsched+1 @@ -1833,7 +2065,7 @@ function OPSGROUP:GetTaskByID(id, status) return nil end ---- On after TaskExecute event. +--- On after "TaskExecute" event. -- @param #OPSGROUP self -- @param #string From From state. -- @param #string Event Event. @@ -2722,7 +2954,7 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) end ---- On after "GotoWaypoint" event. Group will got to the given waypoint and execute its route from there. +--- Set tasks at this waypoint -- @param #OPSGROUP self -- @param #OPSGROUP.Waypoint Waypoint The waypoint. -- @return #number Number of tasks. @@ -2806,7 +3038,7 @@ function OPSGROUP:onafterGotoWaypoint(From, Event, To, UID) end ---- On after "DetectedUnit" event. Add newly detected unit to detected units set. +--- On after "DetectedUnit" event. -- @param #OPSGROUP self -- @param #string From From state. -- @param #string Event Event. @@ -2819,31 +3051,71 @@ function OPSGROUP:onafterDetectedUnit(From, Event, To, Unit) -- Debug. self:T2(self.lid..string.format("Detected unit %s", unitname)) - - + if self.detectedunits:FindUnit(unitname) then -- Unit is already in the detected unit set ==> Trigger "DetectedUnitKnown" event. self:DetectedUnitKnown(Unit) else -- Unit is was not detected ==> Trigger "DetectedUnitNew" event. self:DetectedUnitNew(Unit) - end - + end + end ---- On after "DetectedUnitNew" event. +--- On after "DetectedUnitNew" event. Add newly detected unit to detected unit set. -- @param #OPSGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param Wrapper.Unit#UNIT Unit The detected unit. function OPSGROUP:onafterDetectedUnitNew(From, Event, To, Unit) + + -- Debug info. self:T(self.lid..string.format("Detected New unit %s", Unit:GetName())) -- Add unit to detected unit set. self.detectedunits:AddUnit(Unit) end +--- On after "DetectedGroup" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP Group The detected Group. +function OPSGROUP:onafterDetectedGroup(From, Event, To, Group) + + -- Get group name. + local groupname=Group and Group:GetName() or "unknown" + + -- Debug info. + self:T(self.lid..string.format("Detected group %s", groupname)) + + if self.detectedgroups:FindGroup(groupname) then + -- Group is already in the detected set ==> Trigger "DetectedGroupKnown" event. + self:DetectedGroupKnown(Group) + else + -- Group is was not detected ==> Trigger "DetectedGroupNew" event. + self:DetectedGroupNew(Group) + end + +end + +--- On after "DetectedGroupNew" event. Add newly detected group to detected group set. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP Group The detected group. +function OPSGROUP:onafterDetectedGroupNew(From, Event, To, Group) + + -- Debug info. + self:T(self.lid..string.format("Detected New group %s", Group:GetName())) + + -- Add unit to detected unit set. + self.detectedgroups:AddGroup(Group) +end + --- On after "EnterZone" event. Sets self.inzones[zonename]=true. -- @param #OPSGROUP self -- @param #string From From state. @@ -2868,6 +3140,458 @@ function OPSGROUP:onafterLeaveZone(From, Event, To, Zone) self.inzones:Remove(zonename, true) end +--- On before "LaserOn" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Target Target Coordinate. Target can also be any POSITIONABLE from which we can obtain its coordinates. +function OPSGROUP:onbeforeLaserOn(From, Event, To, Target) + + -- Check if LASER is already on. + if self.spot.On then + return false + end + + if Target then + + -- Target specified ==> set target. + self:SetLaserTarget(Target) + + else + -- No target specified. + self:E(self.lid.."ERROR: No target provided for LASER!") + return false + end + + -- Get the first element alive. + local element=self:GetElementAlive() + + if element then + + -- Set element. + self.spot.element=element + + -- Height offset. No offset for aircraft. We take the height for ground or naval. + local offsetY=0 + if self.isGround or self.isNaval then + offsetY=element.height + end + + -- Local offset of the LASER source. + self.spot.offset={x=0, y=offsetY, z=0} + + -- Check LOS. + if self.spot.CheckLOS then + + -- Check LOS. + local los=self:HasLoS(self.spot.Coordinate, self.spot.element, self.spot.offset) + + --self:I({los=los, coord=self.spot.Coordinate, offset=self.spot.offset}) + + if los then + self:LaserGotLOS() + else + -- Try to switch laser on again in 10 sec. + self:I(self.lid.."LASER got no LOS currently. Trying to switch the laser on again in 10 sec") + self:__LaserOn(-10, Target) + return false + end + + end + + else + self:E(self.lid.."ERROR: No element alive for lasing") + return false + end + + return true +end + +--- On after "LaserOn" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Target Target Coordinate. Target can also be any POSITIONABLE from which we can obtain its coordinates. +function OPSGROUP:onafterLaserOn(From, Event, To, Target) + + -- Start timer that calls the update twice per sec by default. + if not self.spot.timer:IsRunning() then + self.spot.timer:Start(nil, self.spot.dt) + end + + -- Get DCS unit. + local DCSunit=self.spot.element.unit:GetDCSObject() + + -- Create laser and IR beams. + self.spot.Laser=Spot.createLaser(DCSunit, self.spot.offset, self.spot.vec3, self.spot.Code or 1688) + if self.spot.IRon then + self.spot.IR=Spot.createInfraRed(DCSunit, self.spot.offset, self.spot.vec3) + end + + -- Laser is on. + self.spot.On=true + + -- No paused in case it was. + self.spot.Paused=false + + -- Debug message. + self:T(self.lid.."Switching LASER on") + +end + +--- On before "LaserOff" event. Check if LASER is on. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeLaserOff(From, Event, To) + return self.spot.On or self.spot.Paused +end + +--- On after "LaserOff" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLaserOff(From, Event, To) + + -- Debug message. + self:T(self.lid.."Switching LASER off") + + -- "Destroy" the laser beam. + if self.spot.On then + self.spot.Laser:destroy() + self.spot.IR:destroy() + + -- Set to nil. + self.spot.Laser=nil + self.spot.IR=nil + end + + -- Stop update timer. + self.spot.timer:Stop() + + -- No target unit. + self.spot.TargetUnit=nil + + -- Laser is off. + self.spot.On=false + + -- Not paused if it was. + self.spot.Paused=false +end + +--- On after "LaserPause" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLaserPause(From, Event, To) + + -- Debug message. + self:I(self.lid.."Switching LASER off temporarily") + + -- "Destroy" the laser beam. + self.spot.Laser:destroy() + self.spot.IR:destroy() + + -- Set to nil. + self.spot.Laser=nil + self.spot.IR=nil + + -- Laser is off. + self.spot.On=false + + -- Laser is paused. + self.spot.Paused=true + +end + +--- On before "LaserResume" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeLaserResume(From, Event, To) + return self.spot.Paused +end + +--- On after "LaserResume" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLaserResume(From, Event, To) + + -- Debug info. + self:I(self.lid.."Resuming LASER") + + -- Unset paused. + self.spot.Paused=false + + -- Set target. + local target=nil + if self.spot.TargetType==0 then + target=self.spot.Coordinate + elseif self.spot.TargetType==1 or self.spot.TargetType==2 then + target=self.spot.TargetUnit + elseif self.spot.TargetType==3 then + target=self.spot.TargetGroup + end + + -- Switch laser back on. + if target then + + -- Debug message. + self:I(self.lid.."Switching LASER on again at target ".. target:GetName()) + + self:LaserOn(target) + end + +end + +--- On after "LaserCode" event. Changes the LASER code. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Code Laser code. Default is 1688. +function OPSGROUP:onafterLaserCode(From, Event, To, Code) + + -- Default is 1688. + self.spot.Code=Code or 1688 + + -- Debug message. + self:T2(self.lid..string.format("Setting LASER Code to %d", self.spot.Code)) + + if self.spot.On then + + -- Debug info. + self:T(self.lid..string.format("New LASER Code is %d", self.spot.Code)) + + -- Set LASER code. + self.spot.Laser:setCode(self.spot.Code) + end + +end + +--- On after "LaserLostLOS" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLaserLostLOS(From, Event, To) + + --env.info("FF lost LOS") + + -- No of sight. + self.spot.LOS=false + + -- Lost line of sight. + self.spot.lostLOS=true + + if self.spot.On then + + --env.info("FF lost LOS ==> pause laser") + + -- Switch laser off. + self:LaserPause() + + end + +end + +--- On after "LaserGotLOS" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLaserGotLOS(From, Event, To) + + -- Has line of sight. + self.spot.LOS=true + + --env.info("FF Laser Got LOS") + + if self.spot.lostLOS then + + -- Did not loose LOS anymore. + self.spot.lostLOS=false + + --env.info("FF had lost LOS and regained it") + + -- Resume laser if currently paused. + if self.spot.Paused then + --env.info("FF laser was paused ==> resume") + self:LaserResume() + end + + end + +end + +--- Set LASER target. +-- @param #OPSGROUP self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to lase. Can also be a COORDINATE object. +function OPSGROUP:SetLaserTarget(Target) + + if Target then + + -- Check if we have a POSITIONABLE. + if Target:IsInstanceOf("POSITIONABLE") then + local target=Target --Wrapper.Positionable#POSITIONABLE + + if target:IsAlive() then + + if target:IsInstanceOf("GROUP") then + -- We got a GROUP as target. + self.spot.TargetGroup=target + self.spot.TargetUnit=target:GetHighestThreat() + self.spot.TargetType=3 + else + -- We got a UNIT or STATIC as target. + self.spot.TargetUnit=target + if target:IsInstanceOf("STATIC") then + self.spot.TargetType=1 + elseif target:IsInstanceOf("UNIT") then + self.spot.TargetType=2 + end + end + + -- Get object size. + local size,x,y,z=self.spot.TargetUnit:GetObjectSize() + + if y then + self.spot.offsetTarget={x=0, y=y*0.75, z=0} + else + self.spot.offsetTarget={x=0, 2, z=0} + end + + --env.info(string.format("Target offset %.3f", y)) + + else + self:E("WARNING: LASER target is not alive!") + return + end + + elseif Target:IsInstanceOf("COORDINATE") then + -- Coordinate as target. + self.spot.TargetType=0 + self.spot.offsetTarget={x=0, y=0, z=0} + else + self:E(self.lid.."ERROR: LASER target should be a POSITIONABLE (GROUP, UNIT or STATIC) or a COORDINATE object!") + return + end + + -- Set vec3 and account for target offset. + self.spot.vec3=UTILS.VecAdd(Target:GetVec3(), self.spot.offsetTarget) + + -- Set coordinate. + self.spot.Coordinate:UpdateFromVec3(self.spot.vec3) + end + +end + +--- Update laser point. +-- @param #OPSGROUP self +function OPSGROUP:_UpdateLaser() + + -- Check if we have a POSITIONABLE to lase. + if self.spot.TargetUnit then + + --- + -- Lasing a possibly moving target + --- + + if self.spot.TargetUnit:IsAlive() then + + -- Get current target position. + local vec3=self.spot.TargetUnit:GetVec3() + + -- Add target offset. + vec3=UTILS.VecAdd(vec3, self.spot.offsetTarget) + + -- Calculate distance + local dist=UTILS.VecDist3D(vec3, self.spot.vec3) + + -- Store current position. + self.spot.vec3=vec3 + + -- Update beam coordinate. + self.spot.Coordinate:UpdateFromVec3(vec3) + + -- Update laser if target moved more than one meter. + if dist>1 then + + -- If the laser is ON, set the new laser target point. + if self.spot.On then + self.spot.Laser:setPoint(vec3) + if self.spot.IRon then + self.spot.IR:setPoint(vec3) + end + end + + end + + else + + if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive() then + + -- Get first alive unit in the group. + local unit=self.spot.TargetGroup:GetHighestThreat() + + if unit then + self:I(self.lid..string.format("Switching to target unit %s in the group", unit:GetName())) + self.spot.TargetUnit=unit + -- We update the laser position in the next update cycle and then check the LOS. + return + else + -- Switch laser off. + self:I(self.lid.."Target is not alive any more ==> switching LASER off") + self:LaserOff() + return + end + + else + + -- Switch laser off. + self:I(self.lid.."Target is not alive any more ==> switching LASER off") + self:LaserOff() + return + end + + end + end + + -- Check LOS. + if self.spot.CheckLOS then + + -- Check current LOS. + local los=self:HasLoS(self.spot.Coordinate, self.spot.element, self.spot.offset) + + --env.info(string.format("FF check LOS current=%s previous=%s", tostring(los), tostring(self.spot.LOS))) + + if los then + -- Got LOS + if self.spot.lostLOS then + --self:I({los=self.spot.LOS, coord=self.spot.Coordinate, offset=self.spot.offset}) + self:LaserGotLOS() + end + + else + -- No LOS currently + if not self.spot.lostLOS then + self:LaserLostLOS() + end + + end + + end + +end + --- On after "ElementDestroyed" event. -- @param #OPSGROUP self @@ -2902,9 +3626,44 @@ end -- @param #OPSGROUP.Element Element The flight group element. function OPSGROUP:onafterElementDead(From, Event, To, Element) self:T(self.lid..string.format("Element dead %s", Element.name)) - + -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) + + -- Check if element was lasing and if so, switch to another unit alive to lase. + if self.spot.On and self.spot.element.name==Element.name then + + -- Switch laser off. + self:LaserOff() + + -- If there is another element alive, switch laser on again. + if self:GetNelements()>0 then + + -- New target if any. + local target=nil + + if self.spot.TargetType==0 then + -- Coordinate + target=self.spot.Coordinate + elseif self.spot.TargetType==1 or self.spot.TargetType==2 then + -- Static or unit + if self.spot.TargetUnit and self.spot.TargetUnit:IsAlive() then + target=self.spot.TargetUnit + end + elseif self.spot.TargetType==3 then + -- Group + if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive() then + target=self.spot.TargetGroup + end + end + + -- Switch laser on again. + if target then + self:__LaserOn(-1, target) + end + end + end + end --- On after "Dead" event. @@ -3024,6 +3783,7 @@ function OPSGROUP:_CheckDetectedUnits() local detectedtargets=self.group:GetDetectedTargets() local detected={} + local groups={} for DetectionObjectID, Detection in pairs(detectedtargets or {}) do local DetectedObject=Detection.object -- DCS#Object @@ -3043,9 +3803,22 @@ function OPSGROUP:_CheckDetectedUnits() -- Trigger detected unit event ==> This also triggers the DetectedUnitNew and DetectedUnitKnown events. self:DetectedUnit(unit) + -- Get group of unit. + local group=unit:GetGroup() + + -- Add group to table. + if group then + groups[group:GetName()]=group + end + end end end + + -- Call detected group event. + for groupname, group in pairs(groups) do + self:DetectedGroup(group) + end -- Loop over units in detected set. local lost={} @@ -3071,8 +3844,32 @@ function OPSGROUP:_CheckDetectedUnits() -- Remove lost units from detected set. self.detectedunits:RemoveUnitsByName(lost) - end + -- Loop over groups in detected set. + local lost={} + for _,_group in pairs(self.detectedgroups:GetSet()) do + local group=_group --Wrapper.Group#GROUP + + -- Loop over detected units + local gotit=false + for _,_du in pairs(groups) do + local du=_du --Wrapper.Group#GROUP + if group:GetName()==du:GetName() then + gotit=true + end + end + + if not gotit then + table.insert(lost, group:GetName()) + self:DetectedGroupLost(group) + end + + end + + -- Remove lost units from detected set. + self.detectedgroups:RemoveGroupsByName(lost) + + end end @@ -3081,7 +3878,7 @@ end -- @param #number delay Delay in seconds. function OPSGROUP:_CheckGroupDone(delay) - if self:IsAlive() and self.ai then + if self:IsAlive() and self.isAI then if delay and delay>0 then -- Delayed call. @@ -3090,6 +3887,8 @@ function OPSGROUP:_CheckGroupDone(delay) -- Get current waypoint. local waypoint=self:GetWaypoint(self.currentwp) + + --env.info("FF CheckGroupDone") if waypoint then @@ -3167,6 +3966,142 @@ function OPSGROUP:_CheckGroupDone(delay) end +--- Check if group got stuck. +-- @param #OPSGROUP self +function OPSGROUP:_CheckStuck() + + -- Holding means we are not stuck. + if self:IsHolding() or self:Is("Rearming") then + return + end + + -- Current time. + local Tnow=timer.getTime() + + -- Expected speed in m/s. + local ExpectedSpeed=self:GetExpectedSpeed() + + -- Current speed in m/s. + local speed=self:GetVelocity() + + -- Check speed. + if speed<0.5 then + + if ExpectedSpeed>0 and not self.stuckTimestamp then + self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected", speed, ExpectedSpeed)) + self.stuckTimestamp=Tnow + self.stuckVec3=self:GetVec3() + end + + else + -- Moving (again). + self.stuckTimestamp=nil + end + + -- Somehow we are not moving... + if self.stuckTimestamp then + + -- Time we are holding. + local holdtime=Tnow-self.stuckTimestamp + + if holdtime>=10*60 then + + self:E(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + + --TODO: Stuck event! + + end + + end + +end + +--- Check ammo is full. +-- @param #OPSGROUP self +-- @return #boolean If true, ammo is full. +function OPSGROUP:_CheckAmmoFull() + + -- Get current ammo. + local ammo=self:GetAmmoTot() + + for key,value in pairs(self.ammo) do + + if ammo[key]0 then + + -- Get current ammo. + local ammo=self:GetAmmoTot() + + -- Check if rearming is completed. + if self:IsRearming() then + if ammo.Total==self.ammo.Total then + self:Rearmed() + end + end + + -- Total. + if self.outofAmmo and ammo.Total>0 then + self.outofAmmo=false + end + if ammo.Total==0 and not self.outofAmmo then + self.outofAmmo=true + self:OutOfAmmo() + end + + -- Guns. + if self.outofGuns and ammo.Guns>0 then + self.outoffGuns=false + end + if ammo.Guns==0 and self.ammo.Guns>0 and not self.outofGuns then + self.outofGuns=true + self:OutOfGuns() + end + + -- Rockets. + if self.outofRockets and ammo.Rockets>0 then + self.outoffRockets=false + end + if ammo.Rockets==0 and self.ammo.Rockets>0 and not self.outofRockets then + self.outofRockets=true + self:OutOfRockets() + end + + -- Bombs. + if self.outofBombs and ammo.Bombs>0 then + self.outoffBombs=false + end + if ammo.Bombs==0 and self.ammo.Bombs>0 and not self.outofBombs then + self.outofBombs=true + self:OutOfBombs() + end + + -- Missiles. + if self.outofMissiles and ammo.Missiles>0 then + self.outoffMissiles=false + end + if ammo.Missiles==0 and self.ammo.Missiles>0 and not self.outofMissiles then + self.outofMissiles=true + self:OutOfMissiles() + end + + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Status Info Common to Air, Land and Sea ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3440,40 +4375,51 @@ function OPSGROUP._PassingWaypoint(group, opsgroup, uid) opsgroup.speed=wpnext.speed end - - -- Check if the group is still pathfinding. - if opsgroup.ispathfinding and not waypoint.astar then - opsgroup.ispathfinding=false - end - - -- Check special waypoints. - if waypoint.astar then - - opsgroup:RemoveWaypointByID(uid) - - elseif waypoint.detour then - - opsgroup:RemoveWaypointByID(uid) - - -- Trigger event. - opsgroup:DetourReached() - - if waypoint.detour==0 then - opsgroup:FullStop() - elseif waypoint.detour==1 then - opsgroup:Cruise() - else - opsgroup:E("ERROR: waypoint.detour should be 0 or 1") - end - - end -- Debug message. local text=string.format("Group passing waypoint uid=%d", uid) opsgroup:T(opsgroup.lid..text) -- Trigger PassingWaypoint event. - if not (waypoint.astar or waypoint.detour) then + if waypoint.astar then + + -- Remove Astar waypoint. + opsgroup:RemoveWaypointByID(uid) + + -- Cruise. + opsgroup:Cruise() + + elseif waypoint.detour then + + -- Remove detour waypoint. + opsgroup:RemoveWaypointByID(uid) + + if opsgroup:IsRearming() then + + -- Trigger Rearming event. + opsgroup:Rearming() + + else + + -- Trigger DetourReached event. + opsgroup:DetourReached() + + if waypoint.detour==0 then + opsgroup:FullStop() + elseif waypoint.detour==1 then + opsgroup:Cruise() + else + opsgroup:E("ERROR: waypoint.detour should be 0 or 1") + end + + end + + else + + -- Check if the group is still pathfinding. + if opsgroup.ispathfinding then + opsgroup.ispathfinding=false + end -- Increase passing counter. waypoint.npassed=waypoint.npassed+1 @@ -4497,6 +5443,23 @@ function OPSGROUP:GetElementByName(unitname) return nil end +--- Get the first element of a group, which is alive. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Element The element or `#nil` if no element is alive any more. +function OPSGROUP:GetElementAlive() + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD then + if element.unit and element.unit:IsAlive() then + return element + end + end + end + + return nil +end + --- Get number of elements alive. -- @param #OPSGROUP self -- @param #string status (Optional) Only count number, which are in a special status. @@ -4750,54 +5713,24 @@ function OPSGROUP:_MissileCategoryName(categorynumber) return cat end ---- Check if group got stuck. +--- Get coordinate from an object. -- @param #OPSGROUP self -function OPSGROUP:_CheckStuck() - - -- Holding means we are not stuck. - if self:IsHolding() then - return - end +-- @param Wrapper.Object#OBJECT Object The object. +-- @return Core.Point#COORDINATE The coordinate of the object. +function OPSGROUP:_CoordinateFromObject(Object) - -- Current time. - local Tnow=timer.getTime() - - -- Expected speed in m/s. - local ExpectedSpeed=self:GetExpectedSpeed() - - -- Current speed in m/s. - local speed=self:GetVelocity() - - -- Check speed. - if speed<0.5 then - - if ExpectedSpeed>0 and not self.stuckTimestamp then - self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected", speed, ExpectedSpeed)) - self.stuckTimestamp=Tnow - self.stuckVec3=self:GetVec3() - end - + if Object:IsInstanceOf("COORDINATE") then + return Object else - -- Moving (again). - self.stuckTimestamp=nil - end - - -- Somehow we are not moving... - if self.stuckTimestamp then - - -- Time we are holding. - local holdtime=Tnow-self.stuckTimestamp - - if holdtime>=10*60 then - - self:E(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) - - --TODO: Stuck event! - + if Object:IsInstanceOf("POSITIONABLE") or Object:IsInstanceOf("ZONE_BASE") then + self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") + return Object:GetCoordinate() + else + self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") end - - end - + end + + return nil end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 9c6c0a79c..436e6f57f 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -2169,6 +2169,40 @@ function GROUP:GetThreatLevel() return threatlevelMax end +--- Get the unit in the group with the highest threat level, which is still alive. +-- @param #GROUP self +-- @return Wrapper.Unit#UNIT The most dangerous unit in the group. +-- @return #number Threat level of the unit. +function GROUP:GetHighestThreat() + + -- Get units of the group. + local units=self:GetUnits() + + if units then + + local threat=nil ; local maxtl=0 + for _,_unit in pairs(units or {}) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit and unit:IsAlive() then + + -- Threat level of group. + local tl=unit:GetThreatLevel() + + -- Check if greater the current threat. + if tl>maxtl then + maxtl=tl + threat=unit + end + end + end + + return threat, maxtl + end + + return nil, nil +end + --- Returns true if the first unit of the GROUP is in the air. -- @param Wrapper.Group#GROUP self diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index fe1c86e15..7f36de4c4 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -492,8 +492,7 @@ end --- Returns the unit's group if it exist and nil otherwise. -- @param Wrapper.Unit#UNIT self --- @return Wrapper.Group#GROUP The Group of the Unit. --- @return #nil The DCS Unit is not existing or alive. +-- @return Wrapper.Group#GROUP The Group of the Unit or `nil` if the unit does not exist. function UNIT:GetGroup() self:F2( self.UnitName )