From 4cc1db13651f531b803f21aee0d3281408d9f1b4 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 28 Jan 2023 22:31:53 +0100 Subject: [PATCH 01/15] Update Auftrag.lua - condition success and failure evaluation --- Moose Development/Moose/Ops/Auftrag.lua | 31 ++++++++++++++----------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 57c5125c7..674bf6707 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -639,7 +639,7 @@ AUFTRAG.Category={ --- AUFTRAG class version. -- @field #string version -AUFTRAG.version="0.9.10" +AUFTRAG.version="1.0.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -3375,7 +3375,7 @@ function AUFTRAG:GetPriority() return self.prio end ---- Get casualties, i.e. number of units that died during this mission. +--- Get casualties, *i.e.* number of own units that died during this mission. -- @param #AUFTRAG self -- @return #number Number of dead units. function AUFTRAG:GetCasualties() @@ -3902,6 +3902,15 @@ function AUFTRAG:onafterStatus(From, Event, To) local Ngroups=self:CountOpsGroups() local Nassigned=self.Nassigned and self.Nassigned-self.Ndead or 0 + + -- check conditions if set + local conditionDone=false + if self.conditionFailureSet then + conditionDone = self:EvalConditionsAny(self.conditionFailure) + end + if self.conditionSuccessSet and not conditionDone then + conditionDone = self:EvalConditionsAny(self.conditionSuccess) + end -- Check if mission is not OVER yet. if self:IsNotOver() then @@ -3915,6 +3924,11 @@ function AUFTRAG:onafterStatus(From, Event, To) -- Cancel mission if stop time passed. self:Cancel() + + elseif conditionDone then + + -- Cancel mission if conditions were met. + self:Cancel() elseif self.durationExe and self.Texecuting and Tnow-self.Texecuting>self.durationExe then @@ -4014,18 +4028,7 @@ function AUFTRAG:onafterStatus(From, Event, To) end self:I(self.lid..text) end - - -- check conditions if set - if self.conditionFailureSet then - local failed = self:EvalConditionsAny(self.conditionFailure) - if failed then self:__Failed(-1) end - end - - if self.conditionSuccessSet then - local success = self:EvalConditionsAny(self.conditionSuccess) - if success then self:__Success(-1) end - end - + -- Ready to evaluate mission outcome? local ready2evaluate=self.Tover and Tnow-self.Tover>=self.dTevaluate or false From c279ab6311b017f3e6e349871d8fdd08551cda25 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 29 Jan 2023 12:19:04 +0100 Subject: [PATCH 02/15] Update Auftrag.lua --- Moose Development/Moose/Ops/Auftrag.lua | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 674bf6707..ff4e95d56 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -1296,11 +1296,7 @@ end function AUFTRAG:NewCAP(ZoneCAP, Altitude, Speed, Coordinate, Heading, Leg, TargetTypes) -- Ensure given TargetTypes parameter is a table. - if TargetTypes then - if type(TargetTypes)~="table" then - TargetTypes={TargetTypes} - end - end + TargetTypes=UTILS.EnsureTable(TargetTypes, true) -- Create ORBIT first. local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAP:GetCoordinate(), Altitude or 10000, Speed or 350, Heading, Leg) @@ -1342,11 +1338,8 @@ end function AUFTRAG:NewCAPGROUP(Grp, Altitude, Speed, RelHeading, Leg, OffsetDist, OffsetAngle, UpdateDistance, TargetTypes, EngageRange) -- Ensure given TargetTypes parameter is a table. - if TargetTypes then - if type(TargetTypes)~="table" then - TargetTypes={TargetTypes} - end - end + TargetTypes=UTILS.EnsureTable(TargetTypes, true) + -- Six NM astern. local OffsetVec2={r=OffsetDist or 6, phi=OffsetAngle or 180} @@ -1395,11 +1388,7 @@ end function AUFTRAG:NewCAS(ZoneCAS, Altitude, Speed, Coordinate, Heading, Leg, TargetTypes) -- Ensure given TargetTypes parameter is a table. - if TargetTypes then - if type(TargetTypes)~="table" then - TargetTypes={TargetTypes} - end - end + TargetTypes=UTILS.EnsureTable(TargetTypes, true) -- Create ORBIT first. local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAS:GetCoordinate(), Altitude or 10000, Speed, Heading, Leg) @@ -2428,9 +2417,9 @@ function AUFTRAG:NewFromTarget(Target, MissionType) elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then mission=self:NewBOMBRUNWAY(Target, Altitude) elseif MissionType==AUFTRAG.Type.CAS then - mission=self:NewCAS(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000),Altitude,Speed,Target:GetAverageCoordinate(),Heading,Leg,TargetTypes) + mission=self:NewCAS(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000), Altitude, Speed, Target:GetAverageCoordinate(), Heading, Leg, TargetTypes) elseif MissionType==AUFTRAG.Type.CASENHANCED then - mission=self:NewCASENHANCED(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000),Altitude,Speed,RangeMax,NoEngageZoneSet,TargetTypes) + mission=self:NewCASENHANCED(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000), Altitude, Speed, RangeMax, NoEngageZoneSet, TargetTypes) elseif MissionType==AUFTRAG.Type.INTERCEPT then mission=self:NewINTERCEPT(Target) elseif MissionType==AUFTRAG.Type.SEAD then From 0b3aea2bf3b0e8e4e1d843d7b906dc8a61e6af37 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 30 Jan 2023 21:52:22 +0100 Subject: [PATCH 03/15] Scenery --- Moose Development/Moose/Core/Zone.lua | 4 +-- Moose Development/Moose/Functional/Range.lua | 2 +- Moose Development/Moose/Wrapper/Scenery.lua | 26 +++++++++++--------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 26dff536e..6de72c546 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -555,7 +555,7 @@ function ZONE_BASE:GetZoneMaybe() end end --- Returns the Value of the zone with the given PropertyName, or nil if no matching property exists. +--- Returns the Value of the zone with the given PropertyName, or nil if no matching property exists. -- @param #ZONE_BASE self -- @param #string PropertyName The name of a the TriggerZone Property to be retrieved. -- @return #string The Value of the TriggerZone Property with the given PropertyName, or nil if absent. @@ -569,7 +569,7 @@ function ZONE_BASE:GetProperty(PropertyName) return self.Properties[PropertyName] end --- Returns the zone Properties table. +--- Returns the zone Properties table. -- @param #ZONE_BASE self -- @return #table The Key:Value table of TriggerZone properties of the zone. function ZONE_BASE:GetAllProperties() diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index c3425f26b..363ae2c6e 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -1530,7 +1530,7 @@ end --- Add a unit or static object as bombing target. -- @param #RANGE self --- @param Wrapper.Positionable#POSITIONABLE unit Positionable (unit or static) of the strafe target. +-- @param Wrapper.Positionable#POSITIONABLE unit Positionable (unit or static) of the bombing target. -- @param #number goodhitrange Max distance from unit which is considered as a good hit. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. -- @return #RANGE self diff --git a/Moose Development/Moose/Wrapper/Scenery.lua b/Moose Development/Moose/Wrapper/Scenery.lua index 9f40bc230..8fe17ecfe 100644 --- a/Moose Development/Moose/Wrapper/Scenery.lua +++ b/Moose Development/Moose/Wrapper/Scenery.lua @@ -4,7 +4,7 @@ -- -- ### Author: **FlightControl** -- --- ### Contributions: **Applevangelist** +-- ### Contributions: **Applevangelist**, **funkyfranky** -- -- === -- @@ -12,12 +12,12 @@ -- @image Wrapper_Scenery.JPG - ---- @type SCENERY --- @field #string ClassName --- @field #string SceneryName --- @field #DCS.Object SceneryObject --- @field #number Life0 +--- SCENERY Class +-- @type SCENERY +-- @field #string ClassName Name of the class. +-- @field #string SceneryName Name of the scenery object. +-- @field DCS#Object SceneryObject DCS scenery object. +-- @field #number Life0 Initial life points. -- @extends Wrapper.Positionable#POSITIONABLE @@ -37,12 +37,16 @@ SCENERY = { --- Register scenery object as POSITIONABLE. --@param #SCENERY self --@param #string SceneryName Scenery name. ---@param #DCS.Object SceneryObject DCS scenery object. +--@param DCS#Object SceneryObject DCS scenery object. --@return #SCENERY Scenery object. function SCENERY:Register( SceneryName, SceneryObject ) + local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) ) + self.SceneryName = SceneryName + self.SceneryObject = SceneryObject + if self.SceneryObject then self.Life0 = self.SceneryObject:getLife() else @@ -53,7 +57,7 @@ end --- Obtain DCS Object from the SCENERY Object. --@param #SCENERY self ---@return #DCS.Object DCS scenery object. +--@return DCS#Object DCS scenery object. function SCENERY:GetDCSObject() return self.SceneryObject end @@ -69,7 +73,7 @@ function SCENERY:GetLife() return life end ---- Get current initial life points from the SCENERY Object. +--- Get initial life points of the SCENERY Object. --@param #SCENERY self --@return #number life function SCENERY:GetLife0() @@ -90,7 +94,7 @@ function SCENERY:IsDead() return self:GetLife() < 1 and true or false end ---- Get the threat level of a SCENERY object. Always 0. +--- Get the threat level of a SCENERY object. Always 0 as scenery does not pose a threat to anyone. --@param #SCENERY self --@return #number Threat level 0. --@return #string "Scenery". From 6e6bb1e88d84e86cb3f469c5ec732248bcc0d4f0 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 30 Jan 2023 23:29:03 +0100 Subject: [PATCH 04/15] RANGE --- Moose Development/Moose/Functional/Range.lua | 316 ++++++++----------- Moose Development/Moose/Utilities/Socket.lua | 1 + 2 files changed, 139 insertions(+), 178 deletions(-) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 363ae2c6e..683271368 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -56,7 +56,7 @@ -- @field #string ClassName Name of the Class. -- @field #boolean Debug If true, debug info is sent as messages on the screen. -- @field #boolean verbose Verbosity level. Higher means more output to DCS log file. --- @field #string id String id of range for output in DCS log. +-- @field #string lid String id of range for output in DCS log. -- @field #string rangename Name of the range. -- @field Core.Point#COORDINATE location Coordinate of the range location. -- @field #number rangeradius Radius of range defining its total size for e.g. smoking bomb impact points and sending radio messages. Default 5 km. @@ -86,7 +86,6 @@ -- @field #number illuminationmaxalt Maximum altitude in meters AGL at which illumination bombs are fired. Default is 1000 m. -- @field #number scorebombdistance Distance from closest target up to which bomb hits are counted. Default 1000 m. -- @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. @@ -102,10 +101,10 @@ -- @field #boolean targetsheet If true, players can save their target sheets. Rangeboss will not work if targetsheets do not save. -- @field #string targetpath Path where to save the target sheets. -- @field #string targetprefix File prefix for target sheet files. --- @field Sound.SRS#MSRS controlmsrs --- @field Sound.SRS#MSRSQUEUE controlsrsQ --- @field Sound.SRS#MSRS instructmsrs --- @field Sound.SRS#MSRSQUEUE instructsrsQ +-- @field Sound.SRS#MSRS controlmsrs SRS wrapper for range controller. +-- @field Sound.SRS#MSRSQUEUE controlsrsQ SRS queue for range controller. +-- @field Sound.SRS#MSRS instructmsrs SRS wrapper for range instructor. +-- @field Sound.SRS#MSRSQUEUE instructsrsQ SRS queue for range instructor. -- @extends Core.Fsm#FSM --- *Don't only practice your art, but force your way into its secrets; art deserves that, for it and knowledge can raise man to the Divine.* - Ludwig van Beethoven @@ -232,8 +231,8 @@ -- -- ## Voice output via SRS -- --- Alternatively, the voice output can be fully done via SRS, **no sound file additions needed**. Set up SRS with @{#RANGE.SetSRS}(). Range control and instructor frequencies and voices can then be --- set via @{#RANGE.SetSRSRangeControl}() and @{#RANGE.SetSRSRangeInstructor}() +-- Alternatively, the voice output can be fully done via SRS, **no sound file additions needed**. Set up SRS with @{#RANGE.SetSRS}(). +-- Range control and instructor frequencies and voices can then be set via @{#RANGE.SetSRSRangeControl}() and @{#RANGE.SetSRSRangeInstructor}(). -- -- # Persistence -- @@ -343,7 +342,6 @@ RANGE = { illuminationmaxalt = 1000, scorebombdistance = 1000, TdelaySmoke = 3.0, - eventmoose = true, trackbombs = true, trackrockets = true, trackmissiles = true, @@ -360,7 +358,18 @@ RANGE = { } --- Default range parameters. --- @list Defaults +-- @type RANGE.Defaults +-- @param #number goodhitrange Radius for good hits in meters. +-- @param #number strafemaxalt Max altitude in meters for players to enter a strafing pit. +-- @param #number dtBombtrack Timer interval in seconds. +-- @param #number Tmsg Message display time in seconds. +-- @param #number ndisplayresults Number of results to display. +-- @param #number rangeradius Radius of range in meters. +-- @param #number TdelaySmoke Time delay in seconds before smoke is triggered. +-- @param #number boxlength Length of strafe pit box in meters. +-- @param #number boxwidth Width of strafe pit box in meters. +-- @param #number goodpass Number of hits for a good strafing pit pass. +-- @param #number foulline Distance of foul line in meters. RANGE.Defaults = { goodhitrange = 25, strafemaxalt = 914, @@ -377,13 +386,15 @@ RANGE.Defaults = { --- Target type, i.e. unit, static, or coordinate. -- @type RANGE.TargetType --- @field #string UNIT Target is a unit. --- @field #string STATIC Target is a static. +-- @field #string UNIT Target is a unitobject. +-- @field #string STATIC Target is a static object. -- @field #string COORD Target is a coordinate. +-- @field #string SCENERY Target is a scenery object. RANGE.TargetType = { UNIT = "Unit", STATIC = "Static", - COORD = "Coordinate" + COORD = "Coordinate", + SCENERY = "Scenery" } --- Player settings. @@ -578,9 +589,10 @@ RANGE.MenuF10Root = nil --- Range script version. -- @field #string version -RANGE.version = "2.5.1" +RANGE.version = "2.6.0" -- TODO list: +-- TODO: Scenery as targets. -- TODO: Verbosity level for messages. -- TODO: Add option for default settings such as smoke off. -- TODO: Add custom weapons, which can be specified by the user. @@ -610,11 +622,11 @@ function RANGE:New( RangeName ) self.rangename = RangeName or "Practice Range" -- Log id. - self.id = string.format( "RANGE %s | ", self.rangename ) + self.lid = string.format( "RANGE %s | ", self.rangename ) -- Debug info. local text = string.format( "Script version %s - creating new RANGE object %s.", RANGE.version, self.rangename ) - self:I( self.id .. text ) + self:I( self.lid .. text ) -- Defaults self:SetDefaultPlayerSmokeBomb() @@ -802,7 +814,7 @@ function RANGE:onafterStart() 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.nstrafetargets, self.nbombtargets ) - self:E( self.id .. text ) + self:E( self.lid .. text ) return end @@ -813,31 +825,20 @@ function RANGE:onafterStart() -- Starting range. local text = string.format( "Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets ) - self:I( self.id .. text ) + self:I( self.lid .. text ) -- Event handling. - if self.eventmoose then - -- Events are handled my MOOSE. - self:T( self.id .. "Events are handled by MOOSE." ) - self:HandleEvent( EVENTS.Birth ) - self:HandleEvent( EVENTS.Hit ) - self:HandleEvent( EVENTS.Shot ) - else - -- Events are handled directly by DCS. - self:T( self.id .. "Events are handled directly by DCS." ) - world.addEventHandler( self ) - end + self:HandleEvent( EVENTS.Birth ) + self:HandleEvent( EVENTS.Hit ) + self:HandleEvent( EVENTS.Shot ) -- Make bomb target move randomly within the range zone. for _, _target in pairs( self.bombingTargets ) do + local target=_target --#RANGE.BombTarget - -- Check if it is a static object. - -- local _static=self:_CheckStatic(_target.target:GetName()) - local _static = _target.type == RANGE.TargetType.STATIC - - if _target.move and _static == false and _target.speed > 1 then - local unit = _target.target -- Wrapper.Unit#UNIT - _target.target:PatrolZones( { self.rangezone }, _target.speed * 0.75, "Off road" ) + -- Check if unit and can move. + if target.move and target.type==RANGE.TargetType.UNIT and target.speed > 1 then + target.target:PatrolZones( { self.rangezone }, target.speed * 0.75, ENUMS.Formation.Vehicle.OffRoad ) end end @@ -1064,7 +1065,7 @@ end --- Set smoke color for marking bomb targets. By default bomb targets are marked by red smoke. -- @param #RANGE self --- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Red. +-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default `SMOKECOLOR.Red`. -- @return #RANGE self function RANGE:SetBombTargetSmokeColor( colorid ) self.BombSmokeColor = colorid or SMOKECOLOR.Red @@ -1082,7 +1083,7 @@ end --- Set smoke color for marking strafe targets. By default strafe targets are marked by green smoke. -- @param #RANGE self --- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Green. +-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default `SMOKECOLOR.Green`. -- @return #RANGE self function RANGE:SetStrafeTargetSmokeColor( colorid ) self.StrafeSmokeColor = colorid or SMOKECOLOR.Green @@ -1091,7 +1092,7 @@ end --- Set smoke color for marking strafe pit approach boxes. By default strafe pit boxes are marked by white smoke. -- @param #RANGE self --- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.White. +-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default `SMOKECOLOR.White`. -- @return #RANGE self function RANGE:SetStrafePitSmokeColor( colorid ) self.StrafePitSmokeColor = colorid or SMOKECOLOR.White @@ -1191,13 +1192,14 @@ end -- @param #RANGE self -- @param #string PathToSRS Path to SRS directory. -- @param #number Port SRS port. Default 5002. --- @param #number Coalition Coalition side, e.g. coalition.side.BLUE or coalition.side.RED --- @param #number Frequency Frequency to use, defaults to 256 (same as rangecontrol) +-- @param #number Coalition Coalition side, e.g. `coalition.side.BLUE` or `coalition.side.RED`. Default `coalition.side.BLUE`. +-- @param #number Frequency Frequency to use. Default is 256 MHz for range control and 305 MHz for instructor. If given, both control and instructor get this frequency. -- @param #number Modulation Modulation to use, defaults to radio.modulation.AM -- @param #number Volume Volume, between 0.0 and 1.0. Defaults to 1.0 -- @param #string PathToGoogleKey Path to Google TTS credentials. -- @return #RANGE self function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume, PathToGoogleKey) + if PathToSRS then self.useSRS=true @@ -1215,7 +1217,7 @@ function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume, self.instructsrsQ = MSRSQUEUE:New("INSTRUCT") if PathToGoogleKey then - self.instructmsrs:SetGoogle(PathToGoogleKey) + self.controlmsrs:SetGoogle(PathToGoogleKey) self.instructmsrs:SetGoogle(PathToGoogleKey) end @@ -1304,7 +1306,7 @@ end -- @return #RANGE self function RANGE:SetSoundfilesPath( path ) self.soundpath = tostring( path or "Range Soundfiles/" ) - self:I( self.id .. string.format( "Setting sound files path to %s", self.soundpath ) ) + self:I( self.lid .. string.format( "Setting sound files path to %s", self.soundpath ) ) return self end @@ -1342,20 +1344,20 @@ function RANGE:AddStrafePit( targetnames, boxlength, boxwidth, heading, inverseh if _isstatic == true then -- Add static object. - self:T( self.id .. string.format( "Adding STATIC object %s as strafe target #%d.", _name, _i ) ) + self:T( self.lid .. string.format( "Adding STATIC object %s as strafe target #%d.", _name, _i ) ) unit = STATIC:FindByName( _name, false ) elseif _isstatic == false then -- Add unit object. - self:T( self.id .. string.format( "Adding UNIT object %s as strafe target #%d.", _name, _i ) ) + self:T( self.lid .. string.format( "Adding UNIT object %s as strafe target #%d.", _name, _i ) ) unit = UNIT:FindByName( _name ) else -- Neither unit nor static object with this name could be found. local text = string.format( "ERROR! Could not find ANY strafe target object with name %s.", _name ) - self:E( self.id .. text ) + self:E( self.lid .. text ) end @@ -1374,7 +1376,7 @@ function RANGE:AddStrafePit( targetnames, boxlength, boxwidth, heading, inverseh -- Check if at least one target could be found. if ntargets == 0 then local text = string.format( "ERROR! No strafe target could be found when calling RANGE:AddStrafePit() for range %s", self.rangename ) - self:E( self.id .. text ) + self:E( self.lid .. text ) return end @@ -1443,7 +1445,7 @@ function RANGE:AddStrafePit( targetnames, boxlength, boxwidth, heading, inverseh -- Debug info local text = string.format( "Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline ) - self:T( self.id .. text ) + self:T( self.lid .. text ) return self end @@ -1513,14 +1515,14 @@ function RANGE:AddBombingTargets( targetnames, goodhitrange, randommove ) if _isstatic == true then local _static = STATIC:FindByName( name ) - self:T2( self.id .. string.format( "Adding static bombing target %s with hit range %d.", name, goodhitrange, false ) ) + self:T2( self.lid .. 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( self.id .. string.format( "Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove ) ) + self:T2( self.lid .. string.format( "Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove ) ) self:AddBombingTargetUnit( _unit, goodhitrange, randommove ) else - self:E( self.id .. string.format( "ERROR! Could not find bombing target %s.", name ) ) + self:E( self.lid .. string.format( "ERROR! Could not find bombing target %s.", name ) ) end end @@ -1553,11 +1555,11 @@ function RANGE:AddBombingTargetUnit( unit, goodhitrange, randommove ) -- Debug or error output. if _isstatic == true then - self:I( self.id .. string.format( "Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring( randommove ) ) ) + self:I( self.lid .. string.format( "Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring( randommove ) ) ) elseif _isstatic == false then - self:I( self.id .. string.format( "Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring( randommove ) ) ) + self:I( self.lid .. string.format( "Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring( randommove ) ) ) else - self:E( self.id .. string.format( "ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!", name ) ) + self:E( self.lid .. 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. @@ -1650,7 +1652,7 @@ function RANGE:GetFoullineDistance( namepit, namefoulline ) elseif _staticpit == false then pit = UNIT:FindByName( namepit ) else - self:E( self.id .. string.format( "ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit ) ) + self:E( self.lid .. 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. @@ -1660,7 +1662,7 @@ function RANGE:GetFoullineDistance( namepit, namefoulline ) elseif _staticfoul == false then foul = UNIT:FindByName( namefoulline ) else - self:E( self.id .. string.format( "ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline ) ) + self:E( self.lid .. 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. @@ -1668,10 +1670,10 @@ function RANGE:GetFoullineDistance( namepit, namefoulline ) if pit ~= nil and foul ~= nil then fouldist = pit:GetCoordinate():Get2DDistance( foul:GetCoordinate() ) else - self:E( self.id .. string.format( "ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline ) ) + self:E( self.lid .. 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( self.id .. string.format( "Foul line distance = %.1f m.", fouldist ) ) + self:T( self.lid .. string.format( "Foul line distance = %.1f m.", fouldist ) ) return fouldist end @@ -1679,73 +1681,6 @@ end -- Event Handling ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- General event handler. --- @param #RANGE self --- @param #table Event DCS event table. -function RANGE:onEvent( Event ) - self:F3( Event ) - - if Event == nil or Event.initiator == nil then - self:T3( "Skipping onEvent. Event or Event.initiator unknown." ) - return true - end - if Unit.getByName( Event.initiator:getName() ) == nil then - self:T3( "Skipping onEvent. Initiator unit name unknown." ) - return true - end - - local DCSiniunit = Event.initiator - local DCStgtunit = Event.target - local DCSweapon = Event.weapon - - 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.target then - EventData.TgtUnitName = Event.target:getName() - EventData.TgtUnit = UNIT:FindByName( EventData.TgtUnitName ) - end - - if Event.weapon then - EventData.Weapon = Event.weapon - EventData.weapon = Event.weapon - EventData.WeaponTypeName = Event.weapon:getTypeName() - end - - -- Event info. - self:T3( self.id .. string.format( "EVENT: Event in onEvent with ID = %s", tostring( Event.id ) ) ) - self:T3( self.id .. string.format( "EVENT: Ini unit = %s", tostring( EventData.IniUnitName ) ) ) - self:T3( self.id .. string.format( "EVENT: Ini group = %s", tostring( EventData.IniGroupName ) ) ) - self:T3( self.id .. string.format( "EVENT: Ini player = %s", tostring( _playername ) ) ) - self:T3( self.id .. string.format( "EVENT: Tgt unit = %s", tostring( EventData.TgtUnitName ) ) ) - self:T3( self.id .. string.format( "EVENT: Wpn type = %s", tostring( EventData.WeaponTypeName ) ) ) - - -- Call event Birth function. - if Event.id == world.event.S_EVENT_BIRTH and _playername then - self:OnEventBirth( EventData ) - end - - -- Call event Shot function. - if Event.id == world.event.S_EVENT_SHOT and _playername and Event.weapon then - self:OnEventShot( EventData ) - end - - -- Call event Hit function. - if Event.id == world.event.S_EVENT_HIT and _playername and DCStgtunit then - self:OnEventHit( EventData ) - end - -end - --- Range event handler for event birth. -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData @@ -1755,9 +1690,9 @@ function RANGE:OnEventBirth( EventData ) local _unitName = EventData.IniUnitName local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) - self:T3( self.id .. "BIRTH: unit = " .. tostring( EventData.IniUnitName ) ) - self:T3( self.id .. "BIRTH: group = " .. tostring( EventData.IniGroupName ) ) - self:T3( self.id .. "BIRTH: player = " .. tostring( _playername ) ) + self:T3( self.lid .. "BIRTH: unit = " .. tostring( EventData.IniUnitName ) ) + self:T3( self.lid .. "BIRTH: group = " .. tostring( EventData.IniGroupName ) ) + self:T3( self.lid .. "BIRTH: player = " .. tostring( _playername ) ) if _unit and _playername then @@ -1768,7 +1703,7 @@ function RANGE:OnEventBirth( EventData ) -- Debug output. local text = string.format( "Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid ) - self:T( self.id .. text ) + self:T( self.lid .. text ) -- Reset current strafe status. self.strafeStatus[_uid] = nil @@ -1806,9 +1741,9 @@ function RANGE:OnEventHit( EventData ) self:F( { eventhit = EventData } ) -- Debug info. - self:T3( self.id .. "HIT: Ini unit = " .. tostring( EventData.IniUnitName ) ) - self:T3( self.id .. "HIT: Ini group = " .. tostring( EventData.IniGroupName ) ) - self:T3( self.id .. "HIT: Tgt target = " .. tostring( EventData.TgtUnitName ) ) + self:T3( self.lid .. "HIT: Ini unit = " .. tostring( EventData.IniUnitName ) ) + self:T3( self.lid .. "HIT: Ini group = " .. tostring( EventData.IniGroupName ) ) + self:T3( self.lid .. "HIT: Tgt target = " .. tostring( EventData.TgtUnitName ) ) -- Player info local _unitName = EventData.IniUnitName @@ -1861,7 +1796,7 @@ function RANGE:OnEventHit( EventData ) self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2) end self:_DisplayMessageToGroup( _unit, text ) - self:T2( self.id .. text ) + self:T2( self.lid .. text ) _currentTarget.pastfoulline = true end end @@ -1908,14 +1843,7 @@ function RANGE:OnEventShot( EventData ) self:F( { eventshot = EventData } ) -- Nil checks. - if EventData.Weapon == nil then - return - end - if EventData.IniDCSUnit == nil then - return - end - - if EventData.IniPlayerName == nil then + if EventData.Weapon == nil or EventData.IniDCSUnit == nil or EventData.IniPlayerName == nil then return end @@ -1931,12 +1859,12 @@ function RANGE:OnEventShot( EventData ) local weaponcategory = desc.category -- Debug info. - self:T( self.id .. "EVENT SHOT: Range " .. self.rangename ) - self:T( self.id .. "EVENT SHOT: Ini unit = " .. EventData.IniUnitName ) - self:T( self.id .. "EVENT SHOT: Ini group = " .. EventData.IniGroupName ) - self:T( self.id .. "EVENT SHOT: Weapon type = " .. _weapon ) - self:T( self.id .. "EVENT SHOT: Weapon name = " .. _weaponName ) - self:T( self.id .. "EVENT SHOT: Weapon cate = " .. weaponcategory ) + self:T( self.lid .. "EVENT SHOT: Range " .. self.rangename ) + self:T( self.lid .. "EVENT SHOT: Ini unit = " .. EventData.IniUnitName ) + self:T( self.lid .. "EVENT SHOT: Ini group = " .. EventData.IniGroupName ) + self:T( self.lid .. "EVENT SHOT: Weapon type = " .. _weapon ) + self:T( self.lid .. "EVENT SHOT: Weapon name = " .. _weaponName ) + self:T( self.lid .. "EVENT SHOT: Weapon cate = " .. weaponcategory ) -- Tracking conditions for bombs, rockets and missiles. local _bombs = weaponcategory == Weapon.Category.BOMB -- string.match(_weapon, "weapons.bombs") @@ -1963,7 +1891,7 @@ function RANGE:OnEventShot( EventData ) -- Distance player to range. if _unit and _playername then dPR = _unit:GetCoordinate():Get2DDistance( self.location ) - self:T( self.id .. string.format( "Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR / 1000 ) ) + self:T( self.lid .. string.format( "Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR / 1000 ) ) end -- Only track if distance player to range is < 25 km. Also check that a player shot. No need to track AI weapons. @@ -1973,7 +1901,7 @@ function RANGE:OnEventShot( EventData ) local playerData = self.PlayerSettings[_playername] -- #RANGE.PlayerData -- Tracking info and init of last bomb position. - self:T( self.id .. string.format( "RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName() ) ) + self:T( self.lid .. string.format( "RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName() ) ) -- Init bomb position. local _lastBombPos = { x = 0, y = 0, z = 0 } -- DCS#Vec3 @@ -1986,7 +1914,7 @@ function RANGE:OnEventShot( EventData ) return _ordnance:getPoint() end ) - self:T2( self.id .. string.format( "Range %s: Bomb still in air: %s", self.rangename, tostring( _status ) ) ) + self:T2( self.lid .. string.format( "Range %s: Bomb still in air: %s", self.rangename, tostring( _status ) ) ) if _status then ---------------------------- @@ -2124,11 +2052,11 @@ function RANGE:OnEventShot( EventData ) end else - self:T( self.id .. "Weapon impacted outside range zone." ) + self:T( self.lid .. "Weapon impacted outside range zone." ) end -- Terminate the timer - self:T( self.id .. string.format( "Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername ) ) + self:T( self.lid .. string.format( "Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername ) ) return nil end -- _status check @@ -2136,13 +2064,21 @@ function RANGE:OnEventShot( EventData ) end -- end function trackBomb -- Weapon is not yet "alife" just yet. Start timer in one second. - self:T( self.id .. string.format( "Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername ) ) + self:T( self.lid .. string.format( "Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername ) ) timer.scheduleFunction( trackBomb, EventData.weapon, timer.getTime() + 0.1 ) end -- if _track (string.match) and player-range distance < threshold. end +--- Check spawn queue and spawn aircraft if necessary. +-- @param #RANGE self +-- @param #string PlayerName Name of player. +-- @return #RANGE.BombResult +function RANGE:_GetBombResults(PlayerName) + +end + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2183,7 +2119,7 @@ function RANGE:onafterStatus( From, Event, To ) end -- Check range status. - self:I( self.id .. text ) + self:I( self.lid .. text ) end @@ -2205,22 +2141,31 @@ function RANGE:onafterEnterRange( From, Event, To, player ) if self.instructor and self.rangecontrol then if self.useSRS then + + local text = string.format("You entered the bombing range. For hit assessment, contact the range controller at %.3f MHz", self.rangecontrolfreq) local ttstext = string.format("You entered the bombing range. For hit assessment, contact the range controller at %.3f mega hertz.", self.rangecontrolfreq) + local group = player.client:GetGroup() - self.instructsrsQ:NewTransmission(ttstext,nil,self.instructmsrs,nil,1,{group},text,10) + + self.instructsrsQ:NewTransmission(ttstext, nil, self.instructmsrs, nil, 1, {group}, text, 10) + else + -- Range control radio frequency split. local RF = UTILS.Split( string.format( "%.3f", self.rangecontrolfreq ), "." ) -- Radio message that player entered the range + -- You entered the bombing range. For hit assessment, contact the range controller at xy MHz self.instructor:NewTransmission( RANGE.Sound.IREnterRange.filename, RANGE.Sound.IREnterRange.duration, self.soundpath ) self.instructor:Number2Transmission( RF[1] ) + if tonumber( RF[2] ) > 0 then self.instructor:NewTransmission( RANGE.Sound.IRDecimal.filename, RANGE.Sound.IRDecimal.duration, self.soundpath ) self.instructor:Number2Transmission( RF[2] ) end + self.instructor:NewTransmission( RANGE.Sound.IRMegaHertz.filename, RANGE.Sound.IRMegaHertz.duration, self.soundpath ) end end @@ -2238,9 +2183,24 @@ function RANGE:onafterExitRange( From, Event, To, player ) if self.instructor then -- You left the bombing range zone. Have a nice day! if self.useSRS then - local text = "You left the bombing range zone. Have a nice day!" - local group = player.client:GetGroup() - self.instructsrsQ:NewTransmission(text,nil,self.instructmsrs,nil,1,{group},text,10) + + local text = "You left the bombing range zone. " + + local r=math.random(2) + + if r==1 then + text=text.."Have a nice day!" + elseif r==2 then + text=text.."Take care and bye bye!" + elseif r==3 then + text=text.."Talk to you soon!" + elseif r==4 then + text=text.."See you in two weeks!" + elseif r==5 then + text=text.."!" + end + + self.instructsrsQ:NewTransmission(text, nil, self.instructmsrs, nil, 1, {player.client:GetGroup()}, text, 10) else self.instructor:NewTransmission( RANGE.Sound.IRExitRange.filename, RANGE.Sound.IRExitRange.duration, self.soundpath ) end @@ -2304,7 +2264,7 @@ function RANGE:onafterImpact( From, Event, To, result, player ) -- Send message. self:_DisplayMessageToGroup( unit, text, nil, true ) - self:T( self.id .. text ) + self:T( self.lid .. text ) end -- Save results. @@ -2343,7 +2303,7 @@ function RANGE:onbeforeSave( From, Event, To ) if io and lfs then return true else - self:E( self.id .. string.format( "WARNING: io and/or lfs not desanitized. Cannot save player results." ) ) + self:E( self.lid .. string.format( "WARNING: io and/or lfs not desanitized. Cannot save player results." ) ) return false end end @@ -2360,9 +2320,9 @@ function RANGE:onafterSave( From, Event, To ) if f then f:write( data ) f:close() - self:I( self.id .. string.format( "Saving player results to file %s", tostring( filename ) ) ) + self:I( self.lid .. string.format( "Saving player results to file %s", tostring( filename ) ) ) else - self:E( self.id .. string.format( "ERROR: Could not save results to file %s", tostring( filename ) ) ) + self:E( self.lid .. string.format( "ERROR: Could not save results to file %s", tostring( filename ) ) ) end end @@ -2408,7 +2368,7 @@ function RANGE:onbeforeLoad( From, Event, To ) if io and lfs then return true else - self:E( self.id .. string.format( "WARNING: io and/or lfs not desanitized. Cannot load player results." ) ) + self:E( self.lid .. string.format( "WARNING: io and/or lfs not desanitized. Cannot load player results." ) ) return false end end @@ -2424,12 +2384,12 @@ function RANGE:onafterLoad( From, Event, To ) local function _loadfile( filename ) local f = io.open( filename, "rb" ) if f then - -- self:I(self.id..string.format("Loading player results from file %s", tostring(filename))) + -- self:I(self.lid..string.format("Loading player results from file %s", tostring(filename))) local data = f:read( "*all" ) f:close() return data else - self:E( self.id .. string.format( "WARNING: Could not load player results from file %s. File might not exist just yet.", tostring( filename ) ) ) + self:E( self.lid .. string.format( "WARNING: Could not load player results from file %s. File might not exist just yet.", tostring( filename ) ) ) return nil end end @@ -2442,7 +2402,7 @@ function RANGE:onafterLoad( From, Event, To ) -- Info message. local text = string.format( "Loading player bomb results from file %s", filename ) - self:I( self.id .. text ) + self:I( self.lid .. text ) -- Load asset data from file. local data = _loadfile( filename ) @@ -2909,7 +2869,7 @@ function RANGE:_DisplayRangeInfo( _unitname ) self:_DisplayMessageToGroup( unit, text, nil, true, true, _multiplayer ) -- Debug output. - self:T2( self.id .. text ) + self:T2( self.lid .. text ) end end end @@ -3056,9 +3016,9 @@ function RANGE:_DisplayRangeWeather( _unitname ) self:_DisplayMessageToGroup( unit, text, nil, true, true, _multiplayer ) -- Debug output. - self:T2( self.id .. text ) + self:T2( self.lid .. text ) else - self:T( self.id .. string.format( "ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname ) ) + self:T( self.lid .. string.format( "ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname ) ) end end @@ -3453,10 +3413,10 @@ function RANGE:_AddF10Commands( _unitName ) local _StrPits = MENU_GROUP_COMMAND:New( group, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName ):Refresh() end else - self:E( self.id .. "Could not find group or group ID in AddF10Menu() function. Unit name: " .. _unitName or "N/A") + self:E( self.lid .. "Could not find group or group ID in AddF10Menu() function. Unit name: " .. _unitName or "N/A") end else - self:E( self.id .. "Player unit does not exist in AddF10Menu() function. Unit name: " .. _unitName or "N/A") + self:E( self.lid .. "Player unit does not exist in AddF10Menu() function. Unit name: " .. _unitName or "N/A") end end @@ -3496,7 +3456,7 @@ function RANGE:_GetBombTargetCoordinate( target ) coord = target.coordinate else - self:E( self.id .. "ERROR: Unknown target type." ) + self:E( self.lid .. "ERROR: Unknown target type." ) end return coord @@ -3524,7 +3484,7 @@ function RANGE:_GetAmmo( unitname ) if ammotable ~= nil then local weapons = #ammotable - self:T2( self.id .. string.format( "Number of weapons %d.", weapons ) ) + self:T2( self.lid .. string.format( "Number of weapons %d.", weapons ) ) for w = 1, weapons do @@ -3538,10 +3498,10 @@ function RANGE:_GetAmmo( unitname ) ammo = ammo + Nammo local text = string.format( "Player %s has %d rounds ammo of type %s", playername, Nammo, Tammo ) - self:T( self.id .. text ) + self:T( self.lid .. text ) else local text = string.format( "Player %s has %d ammo of type %s", playername, Nammo, Tammo ) - self:T( self.id .. text ) + self:T( self.lid .. text ) end end end @@ -4005,20 +3965,20 @@ function RANGE:_CheckStatic( name ) -- If static is not yet in MOOSE DB, we add it. Can happen for cargo statics! if not _MOOSEstatic then - self:T( self.id .. string.format( "Adding DCS static to MOOSE database. Name = %s.", name ) ) + self:T( self.lid .. string.format( "Adding DCS static to MOOSE database. Name = %s.", name ) ) _DATABASE:AddStatic( name ) end return true else - self:T3( self.id .. string.format( "No static object with name %s exists.", name ) ) + self:T3( self.lid .. 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( self.id .. string.format( "No unit object with name %s exists.", name ) ) + self:T3( self.lid .. string.format( "No unit object with name %s exists.", name ) ) end -- If not unit or static exist, we return nil. diff --git a/Moose Development/Moose/Utilities/Socket.lua b/Moose Development/Moose/Utilities/Socket.lua index 4729fcfbb..1970c60a2 100644 --- a/Moose Development/Moose/Utilities/Socket.lua +++ b/Moose Development/Moose/Utilities/Socket.lua @@ -44,6 +44,7 @@ SOCKET = { } --- Data type. This is the keyword the socket listener uses. +-- @type SOCKET.DataType -- @field #string TEXT Plain text. -- @field #string BOMBRESULT Range bombing. -- @field #string STRAFERESULT Range strafeing result. From c5deb09c2c84aef19f0e4eeb5d5c58cf277d6d7b Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 31 Jan 2023 23:22:48 +0100 Subject: [PATCH 05/15] WEAPON v0.0.1 - New class --- Moose Development/Moose/Modules.lua | 1 + Moose Development/Moose/Wrapper/Weapon.lua | 286 +++++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 Moose Development/Moose/Wrapper/Weapon.lua diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index ce170be0f..46458f046 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -46,6 +46,7 @@ __Moose.Include( 'Scripts/Moose/Wrapper/Positionable.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Scenery.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Static.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Unit.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Weapon.lua' ) __Moose.Include( 'Scripts/Moose/Cargo/Cargo.lua' ) __Moose.Include( 'Scripts/Moose/Cargo/CargoUnit.lua' ) diff --git a/Moose Development/Moose/Wrapper/Weapon.lua b/Moose Development/Moose/Wrapper/Weapon.lua new file mode 100644 index 000000000..8cba77152 --- /dev/null +++ b/Moose Development/Moose/Wrapper/Weapon.lua @@ -0,0 +1,286 @@ +--- **Wrapper** - Weapon. +-- +-- ## Main Features: +-- +-- * Define operation phases +-- * Define conditions when phases are over +-- * Option to have branches in the phase tree +-- * Dedicate resources to operations +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Operation). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Wrapper.Weapon +-- @image Wrapper_Weapon.png + + +--- WEAPON class. +-- @type WEAPON +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field DCS#Weapon weapon The DCS weapon object. +-- @extends Wrapper.Positionable#POSITIONABLE + +--- *Before this time tomorrow I shall have gained a peerage, or Westminster Abbey.* -- Horatio Nelson +-- +-- === +-- +-- # The WEAPON Concept +-- +-- The wrapper class is different from most others as weapon objects cannot be found with a DCS API function like `getByName()`. +-- They can only be found in DCS events like "Shot" +-- +-- +-- +-- @field #WEAPON +WEAPON = { + ClassName = "WEAPON", + verbose = 0, +} + +--- Operation phase. +-- @type WEAPON.Phase +-- @field #number uid Unique ID of the phase. +-- @field #string name Name of the phase. +-- @field Core.Condition#CONDITION conditionOver Conditions when the phase is over. +-- @field #string status Phase status. +-- @field #number Tstart Abs. mission time when the phase was started. +-- @field #number nActive Number of times the phase was active. +-- @field #number duration Duration in seconds how long the phase should be active after it started. +-- @field #WEAPON.Branch branch The branch this phase belongs to. + +--- Operation branch. +-- @type WEAPON.Branch +-- @field #number uid Unique ID of the branch. +-- @field #string name Name of the branch. +-- @field #table phases Phases of this branch. +-- @field #table edges Edges of this branch. + +--- Operation edge. +-- @type WEAPON.Edge +-- @field #number uid Unique ID of the edge. +-- @field #WEAPON.Branch branchFrom The from branch. +-- @field #WEAPON.Phase phaseFrom The from phase after which to switch. +-- @field #WEAPON.Branch branchTo The branch to switch to. +-- @field #WEAPON.Phase phaseTo The phase to switch to. +-- @field Core.Condition#CONDITION conditionSwitch Conditions when to switch the branch. + +--- Operation phase. +-- @type WEAPON.PhaseStatus +-- @field #string PLANNED Planned. +-- @field #string ACTIVE Active phase. +-- @field #string OVER Phase is over. +WEAPON.PhaseStatus={ + PLANNED="Planned", + ACTIVE="Active", + OVER="Over", +} + +--- WEAPON class version. +-- @field #string version +WEAPON.version="0.0.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot... + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new WEAPON object. +-- @param #WEAPON self +-- @param DCS#Weapon WeaponObject The DCS weapon object. +-- @return #WEAPON self +function WEAPON:New(WeaponObject) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, POSITIONABLE:New("Weapon")) -- #WEAPON + + self.weapon=WeaponObject + + self.desc=WeaponObject:getDesc() + + self.category=WeaponObject:getCategory() + + self.typeName=WeaponObject:getTypeName() + + self.name=WeaponObject:getName() + + self.coalition=WeaponObject:getCoalition() + + self.country=WeaponObject:getCountry() + + + local text=string.format("FF Weapon: Name=%s, TypeName=%s, Category=%s, Coalition=%d, Country=%d", self.name, self.typeName, self.category, self.coalition, self.country) + env.info(text) + + self:I(self.desc) + + + -- Set log ID. + self.lid=string.format("%s | ", self.name) + + self:Track() + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set verbosity level. +-- @param #WEAPON self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #WEAPON self +function WEAPON:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Check if weapon object (still) exists. +-- @param #WEAPON self +-- @return #boolean If `true`, the weapon object still exists. +function WEAPON:IsExist() + local isExist=false + if self.weapon then + isExist=self.weapon:isExist() + end + return isExist +end + +--- Destroy the weapon object. +-- @param #WEAPON self +-- @param #number Delay Delay before destroy in seconds. +-- @return #WEAPON self +function WEAPON:Destroy(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, WEAPON.Destroy, self, 0) + else + if self.weapon then + self.weapon:destroy() + end + end + + return self +end + +--- Get velocity vector of weapon. +-- @param #WEAPON self +-- @return DCS#Vec3 Velocity vector with x, y and z components in meters/second. +function WEAPON:GetVelocity() + self.weapon:getVelocity() + return self +end + +--- Get speed of weapon. +-- @param #WEAPON self +-- @return #number Speed in meters per second. +function WEAPON:GetSpeed() + + local speed=nil + + if self.weapon then + + local v=self:GetVelocity() + + speed=UTILS.VecNorm(v) + + end + + return speed +end + +--- Check if weapon is in the air. Obviously not really useful for torpedos. Well, then again, this is DCS... +-- @param #WEAPON self +-- @return #boolean If `true`, weapon is in the air. +function WEAPON:InAir() + local inAir=self.weapon:inAir() + return inAir +end + +--- Get the current 3D position vector. +-- @param #WEAPON self +-- @return DCS#Vec3 +function WEAPON:GetVec3() + + local vec3=nil + if self.weapon then + vec3=self.weapon:getPoint() + end + + return vec3 +end + +--- Start tracking the position of the weapon. +-- @param #WEAPON self +function WEAPON:Track() + + -- Weapon is not yet "alife" just yet. Start timer in one second. + self:T( self.lid .. string.format( "Tracking weapon") ) + timer.scheduleFunction(WEAPON._Track, self, timer.getTime() + 0.001 ) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Private Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Track weapon until impact. +-- @param #WEAPON self +-- @param DCS#Time time Time in seconds. +function WEAPON:_Track(time) + + -- Debug info. + self:I(string.format("Tracking at T=%.5f", time)) + + -- When the pcall returns a failure the weapon has hit. + local status, vec3= pcall( + function() + local point=self.weapon:getPoint() + return point + end + ) + + self.dtTrack=0.01 + + if status then + + ------------------------------- + -- Weapon is still in exists -- + ------------------------------- + + -- Remember this position. + self.vec3 = vec3 + + -- Check again in ~0.005 seconds ==> 200 checks per second. + return time+self.dtTrack + else + + self.impactVec3=self.vec3 + + local coord=COORDINATE:NewFromVec3(self.vec3) + + coord:MarkToAll("Impact point") + + return nil + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- From 9af242854aebb5b171358a66bd7b1001d3cbd083 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 1 Feb 2023 23:51:47 +0100 Subject: [PATCH 06/15] Weapon --- Moose Development/Moose/Core/Event.lua | 4 +- Moose Development/Moose/DCS.lua | 4 +- Moose Development/Moose/Wrapper/Weapon.lua | 392 +++++++++++++++------ 3 files changed, 295 insertions(+), 105 deletions(-) diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 1ce9b2cae..470c34bbf 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -305,8 +305,8 @@ EVENTS = { -- @field Wrapper.Airbase#AIRBASE Place The MOOSE airbase object. -- @field #string PlaceName The name of the airbase. -- --- @field #table weapon The weapon used during the event. --- @field #table Weapon +-- @field DCS#Weapon weapon The weapon used during the event. +-- @field DCS#Weapon Weapon The weapon used during the event. -- @field #string WeaponName Name of the weapon. -- @field DCS#Unit WeaponTgtDCSUnit Target DCS unit of the weapon. -- diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index fedbb3e19..f7de809a7 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -427,8 +427,8 @@ do -- Types --- Vec3 type is a 3D-vector. -- DCS world has 3-dimensional coordinate system. DCS ground is an infinite plain. -- @type Vec3 - -- @field #Distance x is directed to the north - -- @field #Distance z is directed to the east + -- @field #Distance x is directed to the North + -- @field #Distance z is directed to the East -- @field #Distance y is directed up --- Vec2 is a 2D-vector for the ground plane as a reference plane. diff --git a/Moose Development/Moose/Wrapper/Weapon.lua b/Moose Development/Moose/Wrapper/Weapon.lua index 8cba77152..52a3c86ba 100644 --- a/Moose Development/Moose/Wrapper/Weapon.lua +++ b/Moose Development/Moose/Wrapper/Weapon.lua @@ -2,16 +2,16 @@ -- -- ## Main Features: -- --- * Define operation phases --- * Define conditions when phases are over --- * Option to have branches in the phase tree --- * Dedicate resources to operations +-- * Convenient access to all DCS API functions +-- * Track weapon and get impact position +-- * Get launcher and target of weapon +-- * Destroy weapon before impact -- -- === -- -- ## Example Missions: -- --- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Operation). +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Wrapper%20-%20Weapon). -- -- === -- @@ -28,6 +28,10 @@ -- @field #number verbose Verbosity level. -- @field #string lid Class id string for output to DCS log file. -- @field DCS#Weapon weapon The DCS weapon object. +-- @field #function impactFunc Callback function for weapon impact. +-- @field #table impactArg Optional arguments for the impact callback function. +-- @field DCS#Vec3 vec3 Last known 3D position vector of the tracked weapon. +-- @field DCS#Position3 pos3 Last known 3D position and direction vector of the tracked weapon. -- @extends Wrapper.Positionable#POSITIONABLE --- *Before this time tomorrow I shall have gained a peerage, or Westminster Abbey.* -- Horatio Nelson @@ -35,11 +39,22 @@ -- === -- -- # The WEAPON Concept --- --- The wrapper class is different from most others as weapon objects cannot be found with a DCS API function like `getByName()`. --- They can only be found in DCS events like "Shot" -- +-- The WEAPON class offers an easy-to-use wrapper interface to all DCS API functions. +-- +-- Probably, the most striking highlight is that the position of the weapon can be tracked and its impact position can be determined, which is not +-- possible with the native DCS scripting engine functions. -- +-- **Note** that this wrapper class is different from most others as weapon objects cannot be found with a DCS API function like `getByName()`. +-- They can only be found in DCS events like the "Shot" event, where the weapon object is contained in the event data. +-- +-- # Dependencies +-- +-- This class is used (at least) in the MOOSE classes: +-- +-- * RANGE (to determine the impact points of bombs and missiles) +-- * ARTY (to destroy and replace shells with smoke or illumination) +-- * FOX (to destroy the missile before it hits the target) -- -- @field #WEAPON WEAPON = { @@ -47,8 +62,8 @@ WEAPON = { verbose = 0, } ---- Operation phase. --- @type WEAPON.Phase +--- Target data. +-- @type WEAPON.Target -- @field #number uid Unique ID of the phase. -- @field #string name Name of the phase. -- @field Core.Condition#CONDITION conditionOver Conditions when the phase is over. @@ -58,22 +73,6 @@ WEAPON = { -- @field #number duration Duration in seconds how long the phase should be active after it started. -- @field #WEAPON.Branch branch The branch this phase belongs to. ---- Operation branch. --- @type WEAPON.Branch --- @field #number uid Unique ID of the branch. --- @field #string name Name of the branch. --- @field #table phases Phases of this branch. --- @field #table edges Edges of this branch. - ---- Operation edge. --- @type WEAPON.Edge --- @field #number uid Unique ID of the edge. --- @field #WEAPON.Branch branchFrom The from branch. --- @field #WEAPON.Phase phaseFrom The from phase after which to switch. --- @field #WEAPON.Branch branchTo The branch to switch to. --- @field #WEAPON.Phase phaseTo The phase to switch to. --- @field Core.Condition#CONDITION conditionSwitch Conditions when to switch the branch. - --- Operation phase. -- @type WEAPON.PhaseStatus -- @field #string PLANNED Planned. @@ -94,17 +93,25 @@ WEAPON.version="0.0.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: A lot... +-- TODO: Destroy before impact. +-- TODO: Monitor target. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create a new WEAPON object. +--- Create a new WEAPON object from the DCS weapon object. -- @param #WEAPON self -- @param DCS#Weapon WeaponObject The DCS weapon object. -- @return #WEAPON self function WEAPON:New(WeaponObject) + -- Nil check on object. + if WeaponObject==nil then + env.error("ERROR: Weapon object does NOT exist") + return nil + end + -- Inherit everything from FSM class. local self=BASE:Inherit(self, POSITIONABLE:New("Weapon")) -- #WEAPON @@ -122,17 +129,27 @@ function WEAPON:New(WeaponObject) self.country=WeaponObject:getCountry() + -- Get DCS unit of the launcher. + local launcher=WeaponObject:getLauncher() - local text=string.format("FF Weapon: Name=%s, TypeName=%s, Category=%s, Coalition=%d, Country=%d", self.name, self.typeName, self.category, self.coalition, self.country) + self.launcherName="Unknown Launcher" + if launcher then + self.launcherName=launcher:getName() + self.launcher=UNIT:Find(launcher) + end + + -- Set log ID. + self.lid=string.format("[%s] %s | ", self.typeName, self.name) + + -- Set default parameters + self:SetTimeStepTrack() + + -- Debug info. + local text=string.format("FF Weapon: Name=%s, TypeName=%s, Category=%s, Coalition=%d, Country=%d, Launcher=%s", + self.name, self.typeName, self.category, self.coalition, self.country, self.launcherName) env.info(text) self:I(self.desc) - - - -- Set log ID. - self.lid=string.format("%s | ", self.name) - - self:Track() return self end @@ -150,17 +167,149 @@ function WEAPON:SetVerbosity(VerbosityLevel) return self end +--- Set track position time step. +-- @param #WEAPON self +-- @param #number TimeStep Time step in seconds when the position is updated. Default 0.01 sec ==> 100 evaluations per second. +-- @return #WEAPON self +function WEAPON:SetTimeStepTrack(TimeStep) + self.dtTrackPos=TimeStep or 0.01 + return self +end + + +--- Get the unit that launched the weapon. +-- @param #WEAPON self +-- @return Wrapper.Unit#UNIT Laucher +function WEAPON:GetLauncher() + return self.launcherUnit +end + +--- Get the target, which the weapon is guiding to. +-- @param #WEAPON self +-- @return Wrapper.Unit#UNIT Laucher +function WEAPON:GetTarget() + + local target=nil + if self.weapon then + + -- Get the DCS target object, which can be a Unit, Weapon, Static, Scenery, Airbase. + local object=self.weapon:getTarget() + + DCStarget:getCategory() + + target=UNIT:Find(DCStarget) + + end + + + return target +end + + +--- Get velocity vector of weapon. +-- @param #WEAPON self +-- @return DCS#Vec3 Velocity vector with x, y and z components in meters/second. +function WEAPON:GetVelocityVec3() + local Vvec3=nil + if self.weapon then + Vvec3=self.weapon:getVelocity() + end + return Vvec3 +end + +--- Get speed of weapon. +-- @param #WEAPON self +-- @return #number Speed in meters per second. +function WEAPON:GetSpeed() + + local speed=nil + + if self.weapon then + + local v=self:GetVelocityVec3() + + speed=UTILS.VecNorm(v) + + end + + return speed +end + +--- Get the current 3D position vector. +-- @param #WEAPON self +-- @return DCS#Vec3 +function WEAPON:GetVec3() + + local vec3=nil + if self.weapon then + vec3=self.weapon:getPoint() + end + + return vec3 +end + +--- Get type name. +-- @param #WEAPON self +-- @return #string The type name. +function WEAPON:GetTypeName() + return self.typeName +end + +--- Get coalition. +-- @param #WEAPON self +-- @return #number Coalition ID. +function WEAPON:GetCoalition() + return self.coalition +end + +--- Get country. +-- @param #WEAPON self +-- @return #number Country ID. +function WEAPON:GetCoalition() + return self.country +end + +--- Get DCS object. +-- @param #WEAPON self +-- @return DCS#Weapon The weapon object. +function WEAPON:GetDCSObject() + -- This polymorphic function is used in Wrapper.Identifiable#IDENTIFIABLE + return self.weapon +end + +--- Get the impact position. Note that this might not exist if the weapon has not impacted yet! +-- @param #WEAPON self +-- @return DCS#Vec3 Impact position vector (if any). +function WEAPON:GetImpactVec3() + return self.impactVec3 +end + + +--- Check if weapon is in the air. Obviously not really useful for torpedos. Well, then again, this is DCS... +-- @param #WEAPON self +-- @return #boolean If `true`, weapon is in the air and `false` if not. Returns `nil` if weapon object itself is `nil`. +function WEAPON:InAir() + local inAir=nil + if self.weapon then + inAir=self.weapon:inAir() + end + return inAir +end + + --- Check if weapon object (still) exists. -- @param #WEAPON self --- @return #boolean If `true`, the weapon object still exists. +-- @return #boolean If `true`, the weapon object still exists and `false` otherwise. Returns `nil` if weapon object itself is `nil`. function WEAPON:IsExist() - local isExist=false + local isExist=nil if self.weapon then isExist=self.weapon:isExist() end return isExist end + + --- Destroy the weapon object. -- @param #WEAPON self -- @param #number Delay Delay before destroy in seconds. @@ -178,61 +327,36 @@ function WEAPON:Destroy(Delay) return self end ---- Get velocity vector of weapon. --- @param #WEAPON self --- @return DCS#Vec3 Velocity vector with x, y and z components in meters/second. -function WEAPON:GetVelocity() - self.weapon:getVelocity() - return self -end - ---- Get speed of weapon. --- @param #WEAPON self --- @return #number Speed in meters per second. -function WEAPON:GetSpeed() - - local speed=nil - - if self.weapon then - - local v=self:GetVelocity() - - speed=UTILS.VecNorm(v) - - end - - return speed -end - ---- Check if weapon is in the air. Obviously not really useful for torpedos. Well, then again, this is DCS... --- @param #WEAPON self --- @return #boolean If `true`, weapon is in the air. -function WEAPON:InAir() - local inAir=self.weapon:inAir() - return inAir -end - ---- Get the current 3D position vector. --- @param #WEAPON self --- @return DCS#Vec3 -function WEAPON:GetVec3() - - local vec3=nil - if self.weapon then - vec3=self.weapon:getPoint() - end - - return vec3 -end - ---- Start tracking the position of the weapon. +--- Start tracking the position of the weapon until it impacts. +-- The position of the weapon is monitored in small time steps. Once the position cannot be determined anymore, the monitoring is stopped and the last known position is +-- the (approximate) impact point. Of course, the smaller the time step, the better the position can be determined. However, this can hit the performance as many +-- calculations per second need to be carried out. -- @param #WEAPON self -function WEAPON:Track() +-- @param #function FuncImpact Function called when weapon has impacted. First argument is the impact coordinate Core.Point#COORDINATE. +-- @param ... Optional arguments passed to the impact function after the impact coordinate. +-- @return #WEAPON self +-- +-- @usage +-- -- Function called on impact. +-- local function impactfunc(Coordinate, Weapon) +-- Coordinate:MarkToAll("Impact Coordinate of weapon") +-- end +-- +-- myweapon:Track(impactfunc) +-- +function WEAPON:TrackPosition(FuncImpact, ...) - -- Weapon is not yet "alife" just yet. Start timer in one second. - self:T( self.lid .. string.format( "Tracking weapon") ) - timer.scheduleFunction(WEAPON._Track, self, timer.getTime() + 0.001 ) + -- Debug info. + self:T(self.lid..string.format("Tracking weapon")) + -- Callback function on impact. + self.impactFunc=FuncImpact + self.impactArg=arg or {} + + -- Weapon is not yet "alife" just yet. Start timer in 0.001 seconds. + timer.scheduleFunction(WEAPON._TrackPosition, self, timer.getTime() + 0.001) + + return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -242,20 +366,19 @@ end --- Track weapon until impact. -- @param #WEAPON self -- @param DCS#Time time Time in seconds. -function WEAPON:_Track(time) +-- @return #number Time when called next or nil if not called again. +function WEAPON:_TrackPosition(time) -- Debug info. - self:I(string.format("Tracking at T=%.5f", time)) + --self:I(string.format("Tracking at T=%.5f", time)) -- When the pcall returns a failure the weapon has hit. - local status, vec3= pcall( + local status, pos3= pcall( function() - local point=self.weapon:getPoint() + local point=self.weapon:getPosition() return point end ) - - self.dtTrack=0.01 if status then @@ -263,24 +386,91 @@ function WEAPON:_Track(time) -- Weapon is still in exists -- ------------------------------- - -- Remember this position. - self.vec3 = vec3 + -- Update last known position. + self.pos3 = pos3 + + -- Update last known vec3. + self.vec3 = self.pos3.p + + if self.verbose>=5 then + + local vec2={x=self.vec3.x, y=self.vec3.z} + + local height=land.getHeight(vec2) - -- Check again in ~0.005 seconds ==> 200 checks per second. - return time+self.dtTrack + -- Current height above ground level. + local agl=self.vec3.y-height + + -- Estimated IP (if any) + local ip=self:_GetIP(100) + + local d=0 + if ip then + d=UTILS.VecDist3D(self.vec3, ip) + end + + self:I(self.lid..string.format("T=%.3f: Height=%.3f m AGL=%.3f m, dIP=%.3f", time, height, agl, d)) + + end + + -- Check again in ~0.01 seconds ==> 100 checks per second. + return time+(self.dtTrackPos or 0.01) else - self.impactVec3=self.vec3 + --------------------------- + -- Weapon does NOT exist -- + --------------------------- + + -- Get intercept point from position (p) and direction (x) in 20 meters. + local ip = land.getIP(self.pos3.p, self.pos3.x, 20) --DCS#Vec3 - local coord=COORDINATE:NewFromVec3(self.vec3) + if ip then + env.info("FF Got intercept point!") + + -- Coordinate of the impact point. + local coord=COORDINATE:NewFromVec3(ip) + + -- Mark coordinate. + coord:MarkToAll("Intercept point") + coord:SmokeBlue() + + -- Distance to last known pos. + local d=UTILS.VecDist3D(ip, self.vec3) + + env.info(string.format("FF d(ip, vec3)=%.3f meters", d)) + + end + + -- Set impact vec3. + self.impactVec3=ip or self.vec3 - coord:MarkToAll("Impact point") + -- Set impact coordinate. + self.impactCoord=COORDINATE:NewFromVec3(self.vec3) + + --self.impactCoord:MarkToAll("Impact point") + + -- Call callback function. + if self.impactFunc then + self.impactFunc(self.impactCoord, self, self.impactArg) + end return nil end end +--- Compute estimated intercept/impact point (IP) based on last known position and direction. +-- @param #WEAPON self +-- @param #number Distance Distance in meters. Default 20 m. +-- @return DCS#Vec3 Estimated intercept/impact point. Can also return `nil`, if no IP can be determined. +function WEAPON:_GetIP(Distance) + + -- Get intercept point from position (p) and direction (x) in 20 meters. + local ip = land.getIP(self.pos3.p, self.pos3.x, Distance or 20) --DCS#Vec3 + + return ip +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- From 4b4708e2a83108863894a29fb2f5bb372df40b5a Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 2 Feb 2023 23:50:00 +0100 Subject: [PATCH 07/15] Weapon --- Moose Development/Moose/DCS.lua | 9 +- Moose Development/Moose/Functional/Fox.lua | 570 +++++++++---------- Moose Development/Moose/Functional/Range.lua | 348 +++++------ Moose Development/Moose/Wrapper/Weapon.lua | 323 ++++++++--- 4 files changed, 687 insertions(+), 563 deletions(-) diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index f7de809a7..a7b8d5884 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -679,10 +679,11 @@ do -- Weapon --- Weapon.Category enum that stores weapon categories. -- @type Weapon.Category - -- @field SHELL - -- @field MISSILE - -- @field ROCKET - -- @field BOMB + -- @field #number SHELL Shell. + -- @field #number MISSILE Missile + -- @field #number ROCKET Rocket. + -- @field #number BOMB Bomb. + -- @field #number TORPEDO Torpedo. --- Weapon.GuidanceType enum that stores guidance methods. Available only for guided weapon (Weapon.Category.MISSILE and some Weapon.Category.BOMB). diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index 9aa65eabf..13ce14a41 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -168,7 +168,7 @@ FOX = { --- Missile data table. -- @type FOX.MissileData --- @field Wrapper.Unit#UNIT weapon Missile weapon unit. +-- @field DCS#Weapon weapon Missile weapon object. -- @field #boolean active If true the missile is active. -- @field #string missileType Type of missile. -- @field #string missileName Name of missile. @@ -196,7 +196,7 @@ FOX.MenuF10Root=nil --- FOX class version. -- @field #string version -FOX.version="0.6.1" +FOX.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -762,6 +762,276 @@ function FOX:_IsProtected(targetunit) return false end + +--- Missle launch event. +-- @param #FOX self +-- @param Wrapper.Weapon#WEAPON weapon Weapon object. +-- @param #FOX.MissileData missile Fired missile +function FOX:_FuncTrack(weapon, missile) + + -- Missile coordinate. + missileCoord=COORDINATE:NewFromVec3(_lastBombPos) + + -- Missile velocity in m/s. + local missileVelocity=UTILS.VecNorm(_ordnance:getVelocity()) + + -- Update missile target if necessary. + self:GetMissileTarget(missile) + + if missile.targetUnit then + + ----------------------------------- + -- Missile has a specific target -- + ----------------------------------- + + if missile.targetPlayer then + -- Target is a player. + if missile.targetPlayer.destroy==true then + target=missile.targetUnit + end + else + -- Check if unit is protected. + if self:_IsProtected(missile.targetUnit) then + target=missile.targetUnit + end + end + + else + + ------------------------------------ + -- Missile has NO specific target -- + ------------------------------------ + + -- TODO: This might cause a problem with wingman. Even if the shooter itself is excluded from the check, it's wingmen are not. + -- That would trigger the distance check right after missile launch if things to wrong. + -- + -- Possible solutions: + -- * Time check: enable this check after X seconds after missile was fired. What is X? + -- * Coalition check. But would not work in training situations where blue on blue is valid! + -- * At least enable it for surface-to-air missiles. + + local function _GetTarget(_unit) + local unit=_unit --Wrapper.Unit#UNIT + + -- Player position. + local playerCoord=unit:GetCoordinate() + + -- Distance. + local dist=missileCoord:Get3DDistance(playerCoord) + + -- Update mindist if necessary. Only include players in range of missile + 50% safety margin. + if dist<=self.explosiondist then + return unit + end + end + + -- Distance to closest player. + local mindist=nil + + -- Loop over players. + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + -- Check that player was not the one who launched the missile. + if player.unitname~=missile.shooterName then + + -- Player position. + local playerCoord=player.unit:GetCoordinate() + + -- Distance. + local dist=missileCoord:Get3DDistance(playerCoord) + + -- Distance from shooter to player. + local Dshooter2player=playerCoord:Get3DDistance(missile.shotCoord) + + -- Update mindist if necessary. Only include players in range of missile + 50% safety margin. + if (mindist==nil or dist=self.bigmissilemass + end + + -- If missile is 150 m from target ==> destroy missile if in safe zone. + if destroymissile and self:_CheckCoordSafe(targetCoord) then + + -- Destroy missile. + self:I(self.lid..string.format("Destroying missile %s(%s) fired by %s aimed at %s [player=%s] at distance %.1f m", + missile.missileType, missile.missileName, missile.shooterName, target:GetName(), tostring(missile.targetPlayer~=nil), distance)) + _ordnance:destroy() + + -- Missile is not active any more. + missile.active=false + + -- Debug smoke. + if self.Debug then + missileCoord:SmokeRed() + targetCoord:SmokeGreen() + end + + -- Create event. + self:MissileDestroyed(missile) + + -- Little explosion for the visual effect. + if self.explosionpower>0 and distance>50 and (distShooter==nil or (distShooter and distShooter>50)) then + missileCoord:Explosion(self.explosionpower) + end + + -- Target was a player. + if missile.targetPlayer then + + -- Message to target. + local text=string.format("Destroying missile. %s", self:_DeadText()) + MESSAGE:New(text, 10):ToGroup(target:GetGroup()) + + -- Increase dead counter. + missile.targetPlayer.dead=missile.targetPlayer.dead+1 + end + + -- Terminate timer. + return nil + + else + + -- Time step. + local dt=1.0 + if distance>50000 then + -- > 50 km + dt=self.dt50 --=5.0 + elseif distance>10000 then + -- 10-50 km + dt=self.dt10 --=1.0 + elseif distance>5000 then + -- 5-10 km + dt=self.dt05 --0.5 + elseif distance>1000 then + -- 1-5 km + dt=self.dt01 --0.1 + else + -- < 1 km + dt=self.dt00 --0.01 + end + + -- Check again in dt seconds. + return timer.getTime()+dt + end + + else + + -- Destroy missile. + self:T(self.lid..string.format("Missile %s(%s) fired by %s has no current target. Checking back in 0.1 sec.", missile.missileType, missile.missileName, missile.shooterName)) + return timer.getTime()+0.1 + + end + +end + +--- Callback function on impact or destroy otherwise. +-- @param #FOX self +-- @param Wrapper.Weapon#WEAPON weapon Weapon object. +-- @param #FOX.MissileData missile Fired missile +function FOX:_FuncImpact(weapon, missile) + + if target then + + -- Get human player. + local player=self:_GetPlayerFromUnit(target) + + -- Check for player and distance < 10 km. + if player and player.unit:IsAlive() then -- and missileCoord and player.unit:GetCoordinate():Get3DDistance(missileCoord)<10*1000 then + local text=string.format("Missile defeated. Well done, %s!", player.name) + MESSAGE:New(text, 10):ToClient(player.client) + + -- Increase defeated counter. + player.defeated=player.defeated+1 + end + + end + + -- Missile is not active any more. + missile.active=false + + --Terminate the timer. + self:T(FOX.lid..string.format("Terminating missile track timer.")) + return nil + +end + --- Missle launch event. -- @param #FOX self -- @param #string From From state. @@ -818,304 +1088,20 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) end end - end + end - -- Init missile position. - local _lastBombPos = {x=0,y=0,z=0} + local weapon=WEAPON:New(missile.weapon) - -- Missile coordinate. - local missileCoord = nil --Core.Point#COORDINATE + weapon:SetFuncTrack(FuncTrack) - -- Target unit of the missile. - local target=nil --Wrapper.Unit#UNIT - - --- Function monitoring the position of a bomb until impact. - local function trackMissile(_ordnance) + weapon:SetFuncImpact(FuncImpact) - -- When the pcall returns a failure the weapon has hit. - local _status,_bombPos = pcall( - function() - return _ordnance:getPoint() - end) - - -- Check if status is not nil. If so, we have a valid point. - if _status then - - ---------------------------------------------- - -- Still in the air. Remember this position -- - ---------------------------------------------- - - -- Missile position. - _lastBombPos = {x=_bombPos.x, y=_bombPos.y, z=_bombPos.z} - - -- Missile coordinate. - missileCoord=COORDINATE:NewFromVec3(_lastBombPos) - - -- Missile velocity in m/s. - local missileVelocity=UTILS.VecNorm(_ordnance:getVelocity()) - - -- Update missile target if necessary. - self:GetMissileTarget(missile) - - if missile.targetUnit then - - ----------------------------------- - -- Missile has a specific target -- - ----------------------------------- - - if missile.targetPlayer then - -- Target is a player. - if missile.targetPlayer.destroy==true then - target=missile.targetUnit - end - else - -- Check if unit is protected. - if self:_IsProtected(missile.targetUnit) then - target=missile.targetUnit - end - end - - else - - ------------------------------------ - -- Missile has NO specific target -- - ------------------------------------ - - -- TODO: This might cause a problem with wingman. Even if the shooter itself is excluded from the check, it's wingmen are not. - -- That would trigger the distance check right after missile launch if things to wrong. - -- - -- Possible solutions: - -- * Time check: enable this check after X seconds after missile was fired. What is X? - -- * Coalition check. But would not work in training situations where blue on blue is valid! - -- * At least enable it for surface-to-air missiles. - - local function _GetTarget(_unit) - local unit=_unit --Wrapper.Unit#UNIT - -- Player position. - local playerCoord=unit:GetCoordinate() - - -- Distance. - local dist=missileCoord:Get3DDistance(playerCoord) - - -- Update mindist if necessary. Only include players in range of missile + 50% safety margin. - if dist<=self.explosiondist then - return unit - end - end - - -- Distance to closest player. - local mindist=nil - - -- Loop over players. - for _,_player in pairs(self.players) do - local player=_player --#FOX.PlayerData - - -- Check that player was not the one who launched the missile. - if player.unitname~=missile.shooterName then - - -- Player position. - local playerCoord=player.unit:GetCoordinate() - - -- Distance. - local dist=missileCoord:Get3DDistance(playerCoord) - - -- Distance from shooter to player. - local Dshooter2player=playerCoord:Get3DDistance(missile.shotCoord) - - -- Update mindist if necessary. Only include players in range of missile + 50% safety margin. - if (mindist==nil or dist=self.bigmissilemass - end - - -- If missile is 150 m from target ==> destroy missile if in safe zone. - if destroymissile and self:_CheckCoordSafe(targetCoord) then - - -- Destroy missile. - self:I(self.lid..string.format("Destroying missile %s(%s) fired by %s aimed at %s [player=%s] at distance %.1f m", - missile.missileType, missile.missileName, missile.shooterName, target:GetName(), tostring(missile.targetPlayer~=nil), distance)) - _ordnance:destroy() - - -- Missile is not active any more. - missile.active=false - - -- Debug smoke. - if self.Debug then - missileCoord:SmokeRed() - targetCoord:SmokeGreen() - end - - -- Create event. - self:MissileDestroyed(missile) - - -- Little explosion for the visual effect. - if self.explosionpower>0 and distance>50 and (distShooter==nil or (distShooter and distShooter>50)) then - missileCoord:Explosion(self.explosionpower) - end - - -- Target was a player. - if missile.targetPlayer then - - -- Message to target. - local text=string.format("Destroying missile. %s", self:_DeadText()) - MESSAGE:New(text, 10):ToGroup(target:GetGroup()) - - -- Increase dead counter. - missile.targetPlayer.dead=missile.targetPlayer.dead+1 - end - - -- Terminate timer. - return nil - - else - - -- Time step. - local dt=1.0 - if distance>50000 then - -- > 50 km - dt=self.dt50 --=5.0 - elseif distance>10000 then - -- 10-50 km - dt=self.dt10 --=1.0 - elseif distance>5000 then - -- 5-10 km - dt=self.dt05 --0.5 - elseif distance>1000 then - -- 1-5 km - dt=self.dt01 --0.1 - else - -- < 1 km - dt=self.dt00 --0.01 - end - - -- Check again in dt seconds. - return timer.getTime()+dt - end - - else - - -- Destroy missile. - self:T(self.lid..string.format("Missile %s(%s) fired by %s has no current target. Checking back in 0.1 sec.", missile.missileType, missile.missileName, missile.shooterName)) - return timer.getTime()+0.1 - - -- No target ==> terminate timer. - --return nil - end - - else - - ------------------------------------- - -- Missile does not exist any more -- - ------------------------------------- - - if target then - - -- Get human player. - local player=self:_GetPlayerFromUnit(target) - - -- Check for player and distance < 10 km. - if player and player.unit:IsAlive() then -- and missileCoord and player.unit:GetCoordinate():Get3DDistance(missileCoord)<10*1000 then - local text=string.format("Missile defeated. Well done, %s!", player.name) - MESSAGE:New(text, 10):ToClient(player.client) - - -- Increase defeated counter. - player.defeated=player.defeated+1 - end - - end - - -- Missile is not active any more. - missile.active=false - - --Terminate the timer. - self:T(FOX.lid..string.format("Terminating missile track timer.")) - return nil - - end -- _status check - - end -- end function trackBomb -- Weapon is not yet "alife" just yet. Start timer with a little delay. self:T(FOX.lid..string.format("Tracking of missile starts in 0.0001 seconds.")) - timer.scheduleFunction(trackMissile, missile.weapon, timer.getTime()+0.0001) + --timer.scheduleFunction(trackMissile, missile.weapon, timer.getTime()+0.0001) + weapon:StartTrack(0.0001) end diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 683271368..34b79bd7d 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -406,9 +406,11 @@ RANGE.TargetType = { -- @field #boolean messages Display info messages. -- @field Wrapper.Client#CLIENT client Client object of player. -- @field #string unitname Name of player aircraft unit. +-- @field Wrapper.Unit#UNIT unit Player unit. -- @field #string playername Name of player. -- @field #string airframe Aircraft type name. -- @field #boolean inzone If true, player is inside the range zone. +-- @field #boolean targeton Target on. --- Bomb target data. -- @type RANGE.BombTarget @@ -1721,6 +1723,7 @@ function RANGE:OnEventBirth( EventData ) self.PlayerSettings[_playername].messages = true self.PlayerSettings[_playername].client = CLIENT:FindByName( _unitName, nil, true ) self.PlayerSettings[_playername].unitname = _unitName + self.PlayerSettings[_playername].unit = _unit self.PlayerSettings[_playername].playername = _playername self.PlayerSettings[_playername].airframe = EventData.IniUnit:GetTypeName() self.PlayerSettings[_playername].inzone = false @@ -1829,10 +1832,141 @@ function RANGE:OnEventHit( EventData ) end end ---- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). +--- Function called on impact of a tracked weapon. -- @param #RANGE self --- @param #table weapon Weapon -function RANGE:_TrackWeapon(weapon) +-- @param Wrapper.Weapon#WEAPON weapon The weapon object. +-- @param #RANGE.PlayerData playerData Player data table. +-- @param #number attackHdg Attack heading. +-- @param #number attackAlt Attack altitude. +-- @param #number attackVel Attack velocity. +function RANGE:_OnImpact(weapon, playerData, attackHdg, attackAlt, attackVel) + + -- Get closet target to last position. + local _closetTarget = nil -- #RANGE.BombTarget + local _distance = nil + local _closeCoord = nil --Core.Point#COORDINATE + local _hitquality = "POOR" + + -- Get callsign. + local _callsign = self:_myname( playerData.unitname ) + + local _playername=playerData.playername + + local _unit=playerData.unit + + -- Coordinate of impact point. + local impactcoord = weapon:GetImpactCoordinate() + + -- Check if impact happened in range zone. + local insidezone = self.rangezone:IsCoordinateInZone( impactcoord ) + + + -- Smoke impact point of bomb. + if playerData.smokebombimpact and insidezone then + if playerData.delaysmoke then + timer.scheduleFunction( self._DelayedSmoke, { coord = impactcoord, color = playerData.smokecolor }, timer.getTime() + self.TdelaySmoke ) + else + impactcoord:Smoke( playerData.smokecolor ) + end + end + + -- Loop over defined bombing targets. + for _, _bombtarget in pairs( self.bombingTargets ) do + local bombtarget=_bombtarget --#RANGE.BombTarget + + -- Get target coordinate. + local targetcoord = self:_GetBombTargetCoordinate( _bombtarget ) + + if targetcoord then + + -- Distance between bomb and target. + local _temp = impactcoord:Get2DDistance( targetcoord ) + + -- Find closest target to last known position of the bomb. + if _distance == nil or _temp < _distance then + _distance = _temp + _closetTarget = bombtarget + _closeCoord = targetcoord + if _distance <= 1.53 then -- Rangeboss Edit + _hitquality = "SHACK" -- Rangeboss Edit + elseif _distance <= 0.5 * bombtarget.goodhitrange then -- Rangeboss Edit + _hitquality = "EXCELLENT" + elseif _distance <= bombtarget.goodhitrange then + _hitquality = "GOOD" + elseif _distance <= 2 * bombtarget.goodhitrange then + _hitquality = "INEFFECTIVE" + else + _hitquality = "POOR" + end + + end + end + end + + -- Count if bomb fell less than ~1 km away from the target. + if _distance and _distance <= self.scorebombdistance then + -- Init bomb player results. + if not self.bombPlayerResults[_playername] then + self.bombPlayerResults[_playername] = {} + end + + -- Local results. + local _results = self.bombPlayerResults[_playername] + + local result = {} -- #RANGE.BombResult + result.command=SOCKET.DataType.BOMBRESULT + result.name = _closetTarget.name or "unknown" + result.distance = _distance + result.radial = _closeCoord:HeadingTo( impactcoord ) + result.weapon = weapon:GetTypeName() or "unknown" + result.quality = _hitquality + result.player = playerData.playername + result.time = timer.getAbsTime() + result.clock = UTILS.SecondsToClock(result.time, true) + result.midate = UTILS.GetDCSMissionDate() + result.theatre = env.mission.theatre + result.airframe = playerData.airframe + result.roundsFired = 0 -- Rangeboss Edit + result.roundsHit = 0 -- Rangeboss Edit + result.roundsQuality = "N/A" -- Rangeboss Edit + result.rangename = self.rangename + result.attackHdg = attackHdg + result.attackVel = attackVel + result.attackAlt = attackAlt + + -- Add to table. + table.insert( _results, result ) + + -- Call impact. + self:Impact( result, playerData ) + + elseif insidezone then + + -- Send message. + -- DONE SRS message + local _message = string.format( "%s, weapon impacted too far from nearest range target (>%.1f km). No score!", _callsign, self.scorebombdistance / 1000 ) + if self.useSRS then + local ttstext = string.format( "%s, weapon impacted too far from nearest range target, mor than %.1f kilometer. No score!", _callsign, self.scorebombdistance / 1000 ) + self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2) + end + self:_DisplayMessageToGroup( _unit, _message, nil, false ) + + if self.rangecontrol then + -- weapon impacted too far from the nearest target! No Score! + if self.useSRS then + self.controlsrsQ:NewTransmission(_message,nil,self.controlmsrs,nil,1) + else + self.rangecontrol:NewTransmission( RANGE.Sound.RCWeaponImpactedTooFar.filename, RANGE.Sound.RCWeaponImpactedTooFar.duration, self.soundpath, nil, nil, _message, self.subduration ) + end + end + + else + self:T( self.lid .. "Weapon impacted outside range zone." ) + end + + -- Terminate the timer + self:T( self.lid .. string.format( "Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername ) ) + return nil end @@ -1847,32 +1981,11 @@ function RANGE:OnEventShot( EventData ) return end - -- Weapon data. - local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName - local _weaponStrArray = UTILS.Split( _weapon, "%." ) - local _weaponName = _weaponStrArray[#_weaponStrArray] - - -- Weapon descriptor. - local desc = EventData.Weapon:getDesc() - - -- Weapon category: 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X) - local weaponcategory = desc.category - - -- Debug info. - self:T( self.lid .. "EVENT SHOT: Range " .. self.rangename ) - self:T( self.lid .. "EVENT SHOT: Ini unit = " .. EventData.IniUnitName ) - self:T( self.lid .. "EVENT SHOT: Ini group = " .. EventData.IniGroupName ) - self:T( self.lid .. "EVENT SHOT: Weapon type = " .. _weapon ) - self:T( self.lid .. "EVENT SHOT: Weapon name = " .. _weaponName ) - self:T( self.lid .. "EVENT SHOT: Weapon cate = " .. weaponcategory ) - - -- Tracking conditions for bombs, rockets and missiles. - local _bombs = weaponcategory == Weapon.Category.BOMB -- string.match(_weapon, "weapons.bombs") - local _rockets = weaponcategory == Weapon.Category.ROCKET -- string.match(_weapon, "weapons.nurs") - local _missiles = weaponcategory == Weapon.Category.MISSILE -- string.match(_weapon, "weapons.missiles") or _viggen + -- Create weapon object. + local weapon=WEAPON:New(EventData.weapon) -- Check if any condition applies here. - local _track = (_bombs and self.trackbombs) or (_rockets and self.trackrockets) or (_missiles and self.trackmissiles) + local _track = (weapon:IsBomb() and self.trackbombs) or (weapon:IsRocket() and self.trackrockets) or (weapon:IsMissile() and self.trackmissiles) -- Get unit name. local _unitName = EventData.IniUnitName @@ -1880,11 +1993,6 @@ function RANGE:OnEventShot( EventData ) -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) - -- Attack parameters. - local attackHdg=_unit:GetHeading() - local attackAlt=_unit:GetHeight() - local attackVel=_unit:GetVelocityKNOTS() - -- Set this to larger value than the threshold. local dPR = self.BombtrackThreshold * 2 @@ -1899,175 +2007,23 @@ function RANGE:OnEventShot( EventData ) -- Player data. local playerData = self.PlayerSettings[_playername] -- #RANGE.PlayerData + + -- Attack parameters. + local attackHdg=_unit:GetHeading() + local attackAlt=_unit:GetHeight() + local attackVel=_unit:GetVelocityKNOTS() -- Tracking info and init of last bomb position. - self:T( self.lid .. string.format( "RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName() ) ) + self:T( self.lid .. string.format( "RANGE %s: Tracking %s - %s.", self.rangename, weapon:GetTypeName(), weapon:GetName())) + + -- Set callback function on impact. + weapon:SetFuncImpact(RANGE._OnImpact, self, playerData, attackHdg, attackAlt, attackVel) - -- Init bomb position. - local _lastBombPos = { x = 0, y = 0, z = 0 } -- DCS#Vec3 - - -- Function monitoring the position of a bomb until impact. - local function trackBomb( _ordnance ) - - -- When the pcall returns a failure the weapon has hit. - local _status, _bombPos = pcall( function() - return _ordnance:getPoint() - end ) - - self:T2( self.lid .. string.format( "Range %s: Bomb still in air: %s", self.rangename, tostring( _status ) ) ) - if _status then - - ---------------------------- - -- Weapon is still in air -- - ---------------------------- - - -- Remember this position. - _lastBombPos = { x = _bombPos.x, y = _bombPos.y, z = _bombPos.z } - - -- Check again in ~0.005 seconds ==> 200 checks per second. - return timer.getTime() + self.dtBombtrack - else - - ----------------------------- - -- Bomb did hit the ground -- - ----------------------------- - - -- Get closet target to last position. - local _closetTarget = nil -- #RANGE.BombTarget - local _distance = nil - local _closeCoord = nil --Core.Point#COORDINATE - local _hitquality = "POOR" - - -- Get callsign. - local _callsign = self:_myname( _unitName ) - - -- Coordinate of impact point. - local impactcoord = COORDINATE:NewFromVec3( _lastBombPos ) - - -- Check if impact happened in range zone. - local insidezone = self.rangezone:IsCoordinateInZone( impactcoord ) - - -- Impact point of bomb. - if self.Debug then - impactcoord:MarkToAll( "Bomb impact point" ) - end - - -- Smoke impact point of bomb. - if playerData.smokebombimpact and insidezone then - if playerData.delaysmoke then - timer.scheduleFunction( self._DelayedSmoke, { coord = impactcoord, color = playerData.smokecolor }, timer.getTime() + self.TdelaySmoke ) - else - impactcoord:Smoke( playerData.smokecolor ) - end - end - - -- Loop over defined bombing targets. - for _, _bombtarget in pairs( self.bombingTargets ) do - local bombtarget=_bombtarget --#RANGE.BombTarget - - -- Get target coordinate. - local targetcoord = self:_GetBombTargetCoordinate( _bombtarget ) - - if targetcoord then - - -- Distance between bomb and target. - local _temp = impactcoord:Get2DDistance( targetcoord ) - - -- Find closest target to last known position of the bomb. - if _distance == nil or _temp < _distance then - _distance = _temp - _closetTarget = bombtarget - _closeCoord = targetcoord - if _distance <= 1.53 then -- Rangeboss Edit - _hitquality = "SHACK" -- Rangeboss Edit - elseif _distance <= 0.5 * bombtarget.goodhitrange then -- Rangeboss Edit - _hitquality = "EXCELLENT" - elseif _distance <= bombtarget.goodhitrange then - _hitquality = "GOOD" - elseif _distance <= 2 * bombtarget.goodhitrange then - _hitquality = "INEFFECTIVE" - else - _hitquality = "POOR" - end - - end - end - end - - -- Count if bomb fell less than ~1 km away from the target. - if _distance and _distance <= self.scorebombdistance then - -- Init bomb player results. - if not self.bombPlayerResults[_playername] then - self.bombPlayerResults[_playername] = {} - end - - -- Local results. - local _results = self.bombPlayerResults[_playername] - - local result = {} -- #RANGE.BombResult - result.command=SOCKET.DataType.BOMBRESULT - result.name = _closetTarget.name or "unknown" - result.distance = _distance - result.radial = _closeCoord:HeadingTo( impactcoord ) - result.weapon = _weaponName or "unknown" - result.quality = _hitquality - result.player = playerData.playername - result.time = timer.getAbsTime() - result.clock = UTILS.SecondsToClock(result.time, true) - result.midate = UTILS.GetDCSMissionDate() - result.theatre = env.mission.theatre - result.airframe = playerData.airframe - result.roundsFired = 0 -- Rangeboss Edit - result.roundsHit = 0 -- Rangeboss Edit - result.roundsQuality = "N/A" -- Rangeboss Edit - result.rangename = self.rangename - result.attackHdg = attackHdg - result.attackVel = attackVel - result.attackAlt = attackAlt - - -- Add to table. - table.insert( _results, result ) - - -- Call impact. - self:Impact( result, playerData ) - - elseif insidezone then - - -- Send message. - -- DONE SRS message - local _message = string.format( "%s, weapon impacted too far from nearest range target (>%.1f km). No score!", _callsign, self.scorebombdistance / 1000 ) - if self.useSRS then - local ttstext = string.format( "%s, weapon impacted too far from nearest range target, mor than %.1f kilometer. No score!", _callsign, self.scorebombdistance / 1000 ) - self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2) - end - self:_DisplayMessageToGroup( _unit, _message, nil, false ) - - if self.rangecontrol then - -- weapon impacted too far from the nearest target! No Score! - if self.useSRS then - self.controlsrsQ:NewTransmission(_message,nil,self.controlmsrs,nil,1) - else - self.rangecontrol:NewTransmission( RANGE.Sound.RCWeaponImpactedTooFar.filename, RANGE.Sound.RCWeaponImpactedTooFar.duration, self.soundpath, nil, nil, _message, self.subduration ) - end - end - - else - self:T( self.lid .. "Weapon impacted outside range zone." ) - end - - -- Terminate the timer - self:T( self.lid .. string.format( "Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername ) ) - return nil - - end -- _status check - - end -- end function trackBomb - - -- Weapon is not yet "alife" just yet. Start timer in one second. + -- Weapon is not yet "alife" just yet. Start timer in 0.1 seconds. self:T( self.lid .. string.format( "Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername ) ) - timer.scheduleFunction( trackBomb, EventData.weapon, timer.getTime() + 0.1 ) + weapon:StartTrack(0.1) - end -- if _track (string.match) and player-range distance < threshold. + end end diff --git a/Moose Development/Moose/Wrapper/Weapon.lua b/Moose Development/Moose/Wrapper/Weapon.lua index 52a3c86ba..b34000a09 100644 --- a/Moose Development/Moose/Wrapper/Weapon.lua +++ b/Moose Development/Moose/Wrapper/Weapon.lua @@ -1,10 +1,11 @@ ---- **Wrapper** - Weapon. +--- **Wrapper** - Weapon functions. -- -- ## Main Features: -- --- * Convenient access to all DCS API functions +-- * Convenient access to DCS API functions -- * Track weapon and get impact position -- * Get launcher and target of weapon +-- * Define callback function when weapon impacts -- * Destroy weapon before impact -- -- === @@ -28,13 +29,30 @@ -- @field #number verbose Verbosity level. -- @field #string lid Class id string for output to DCS log file. -- @field DCS#Weapon weapon The DCS weapon object. +-- @field #string name Name of the weapon object. +-- @field #string typeName Type name of the weapon. +-- @field #number category Weapon category 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X). +-- @field #number coalition Coalition ID. +-- @field #number country Country ID. +-- @field DCS#Desc desc Descriptor table. +-- @field DCS#Unit launcher Launcher DCS unit. +-- @field Wrapper.Unit#UNIT launcherUnit Launcher Unit. +-- @field #string launcherName Name of launcher unit. +-- @field #number dtTrack Time step in seconds for tracking scheduler. -- @field #function impactFunc Callback function for weapon impact. -- @field #table impactArg Optional arguments for the impact callback function. +-- @field #function trackFunc Callback function when weapon is tracked and alive. +-- @field #table trackArg Optional arguments for the track callback function. -- @field DCS#Vec3 vec3 Last known 3D position vector of the tracked weapon. -- @field DCS#Position3 pos3 Last known 3D position and direction vector of the tracked weapon. +-- @field DCS#Vec3 impactVec3 Impact 3D vector. +-- @field Core.Point#COORDINATE impactCoord Impact coordinate. +-- @field #number trackScheduleID Tracking scheduler ID. Can be used to remove/destroy the scheduler function. +-- @field #boolean tracking If `true`, scheduler will keep tracking. Otherwise, function will return nil and stop tracking. +-- @field #boolean markImpact If `true`, the impact point is marked on the F10 map. Requires tracking to be started. -- @extends Wrapper.Positionable#POSITIONABLE ---- *Before this time tomorrow I shall have gained a peerage, or Westminster Abbey.* -- Horatio Nelson +--- *In the long run, the sharpest weapon of all is a kind and gentle spirit.* -- Anne Frank -- -- === -- @@ -48,6 +66,26 @@ -- **Note** that this wrapper class is different from most others as weapon objects cannot be found with a DCS API function like `getByName()`. -- They can only be found in DCS events like the "Shot" event, where the weapon object is contained in the event data. -- +-- # Tracking +-- +-- The status of the weapon can be tracked with the @{#WEAPON.StartTrack}() function. This function will try to determin the position of the weapon in (normally) relatively +-- small time steps. The time step can be set via the @{#WEAPON.SetTimeStepTrack} function and is by default set to 0.01 secons. +-- +-- Once the position cannot be retrieved any more, the weapon has impacted (or was destroyed otherwise) and the last known position is safed as the impact point. +-- The impact point can be accessed with the @{#WEAPON.GetImpactVec3} or @{#WEAPON.GetImpactCoordinate} functions. +-- +-- ## Callback functions +-- +-- It is possible to define functions that are called during the tracking of the weapon and upon impact. +-- +-- ### Callback on Impact +-- +-- The function called on impact can be set with @{#WEAPON.SetFuncImpact} +-- +-- ### Callback when Tracking +-- +-- The function called each time the weapon status is tracked can be set with @{#WEAPON.SetFuncTrack} +-- -- # Dependencies -- -- This class is used (at least) in the MOOSE classes: @@ -62,27 +100,6 @@ WEAPON = { verbose = 0, } ---- Target data. --- @type WEAPON.Target --- @field #number uid Unique ID of the phase. --- @field #string name Name of the phase. --- @field Core.Condition#CONDITION conditionOver Conditions when the phase is over. --- @field #string status Phase status. --- @field #number Tstart Abs. mission time when the phase was started. --- @field #number nActive Number of times the phase was active. --- @field #number duration Duration in seconds how long the phase should be active after it started. --- @field #WEAPON.Branch branch The branch this phase belongs to. - ---- Operation phase. --- @type WEAPON.PhaseStatus --- @field #string PLANNED Planned. --- @field #string ACTIVE Active phase. --- @field #string OVER Phase is over. -WEAPON.PhaseStatus={ - PLANNED="Planned", - ACTIVE="Active", - OVER="Over", -} --- WEAPON class version. -- @field #string version @@ -115,27 +132,42 @@ function WEAPON:New(WeaponObject) -- Inherit everything from FSM class. local self=BASE:Inherit(self, POSITIONABLE:New("Weapon")) -- #WEAPON + -- Set DCS weapon object. self.weapon=WeaponObject + -- Descriptors containing a lot of info. self.desc=WeaponObject:getDesc() + + -- This gives the object category which is always Object.Category.WEAPON! + --self.category=WeaponObject:getCategory() - self.category=WeaponObject:getCategory() - + -- Weapon category: 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X) + self.category = self.desc.category + + if self:IsMissile() and self.desc.missileCategory then + self.categoryMissile=self.desc.missileCategory + end + + -- Get type name. self.typeName=WeaponObject:getTypeName() + -- Get name of object. Usually a number like "1234567". self.name=WeaponObject:getName() + -- Get coaliton of weapon. self.coalition=WeaponObject:getCoalition() + -- Get country of weapon. self.country=WeaponObject:getCountry() -- Get DCS unit of the launcher. - local launcher=WeaponObject:getLauncher() + self.launcher=WeaponObject:getLauncher() + -- Get launcher of weapon. self.launcherName="Unknown Launcher" - if launcher then - self.launcherName=launcher:getName() - self.launcher=UNIT:Find(launcher) + if self.launcher then + self.launcherName=self.launcher:getName() + self.launcherUnit=UNIT:Find(self.launcher) end -- Set log ID. @@ -172,33 +204,108 @@ end -- @param #number TimeStep Time step in seconds when the position is updated. Default 0.01 sec ==> 100 evaluations per second. -- @return #WEAPON self function WEAPON:SetTimeStepTrack(TimeStep) - self.dtTrackPos=TimeStep or 0.01 + self.dtTrack=TimeStep or 0.01 + return self +end + +--- Mark impact point on the F10 map. This requires that the tracking has been started. +-- @param #WEAPON self +-- @param #boolean Switch If `true` or nil, impact is marked. +-- @return #WEAPON self +function WEAPON:SetMarkImpact(Switch) + + if Switch==false then + self.markImpact=false + else + self.markImpact=true + end + + return self +end + +--- Set callback function when weapon is tracked and still alive. The first argument will be the WEAPON object. +-- Note that this can be called many times per second. So be careful for performance reasons. +-- @param #WEAPON self +-- @param #function FuncTrack Function called during tracking. +-- @param ... Optional function arguments. +-- @return #WEAPON self +function WEAPON:SetFuncTrack(FuncTrack, ...) + self.trackFunc=FuncTrack + self.trackArg=arg or {} + return self +end + +--- Set callback function when weapon impacted or was destroyed otherwise, *i.e.* cannot be tracked any more. +-- @param #WEAPON self +-- @param #function FuncImpact Function called once the weapon impacted. +-- @param ... Optional function arguments. +-- @return #WEAPON self +-- +-- @usage +-- -- Function called on impact. +-- local function OnImpact(Weapon) +-- Weapon:GetImpactCoordinate():MarkToAll("Impact Coordinate of weapon") +-- end +-- +-- -- Set which function to call. +-- myweapon:SetFuncImpact(OnImpact) +-- +-- -- Start tracking. +-- myweapon:Track() +-- +function WEAPON:SetFuncImpact(FuncImpact, ...) + self.impactFunc=FuncImpact + self.impactArg=arg or {} return self end --- Get the unit that launched the weapon. -- @param #WEAPON self --- @return Wrapper.Unit#UNIT Laucher +-- @return Wrapper.Unit#UNIT Laucher unit. function WEAPON:GetLauncher() return self.launcherUnit end --- Get the target, which the weapon is guiding to. -- @param #WEAPON self --- @return Wrapper.Unit#UNIT Laucher +-- @return Wrapper.Object#OBJECT The target object, which can be a UNIT or STATIC object. function WEAPON:GetTarget() - local target=nil + local target=nil --Wrapper.Object#OBJECT + if self.weapon then -- Get the DCS target object, which can be a Unit, Weapon, Static, Scenery, Airbase. local object=self.weapon:getTarget() - DCStarget:getCategory() + if object then - target=UNIT:Find(DCStarget) - + -- Get object category. + local category=object:getCategory() + + -- Get object name. + local name=object:getName() + + -- Debug info. + self:I(self.lid..string.format("Got Target Object %s, category=%d", name, category)) + + + if category==Object.Category.UNIT then + + target=UNIT:Find(object) + + elseif category==Object.Category.STATIC then + + target=STATIC:Find(object) + + elseif category==Object.Category.SCENERY then + self:E(self.lid..string.format("ERROR: Scenery target not implemented yet!")) + else + self:E(self.lid..string.format("ERROR: Object category=%d is not implemented yet!", category)) + end + + end end @@ -212,7 +319,7 @@ end function WEAPON:GetVelocityVec3() local Vvec3=nil if self.weapon then - Vvec3=self.weapon:getVelocity() + Vvec3=self.weapon:getVelocity() end return Vvec3 end @@ -265,7 +372,7 @@ end --- Get country. -- @param #WEAPON self -- @return #number Country ID. -function WEAPON:GetCoalition() +function WEAPON:GetCountry() return self.country end @@ -277,13 +384,19 @@ function WEAPON:GetDCSObject() return self.weapon end ---- Get the impact position. Note that this might not exist if the weapon has not impacted yet! +--- Get the impact position vector. Note that this might not exist if the weapon has not impacted yet! -- @param #WEAPON self -- @return DCS#Vec3 Impact position vector (if any). function WEAPON:GetImpactVec3() return self.impactVec3 end +--- Get the impact coordinate. Note that this might not exist if the weapon has not impacted yet! +-- @param #WEAPON self +-- @return Core.Point#COORDINATE Impact coordinate (if any). +function WEAPON:GetImpactCoordinate() + return self.impactCoord +end --- Check if weapon is in the air. Obviously not really useful for torpedos. Well, then again, this is DCS... -- @param #WEAPON self @@ -309,6 +422,41 @@ function WEAPON:IsExist() end +--- Check if weapon is a bomb. +-- @param #WEAPON self +-- @return #boolean If `true`, is a bomb. +function WEAPON:IsBomb() + return self.category==Weapon.Category.BOMB +end + +--- Check if weapon is a missile. +-- @param #WEAPON self +-- @return #boolean If `true`, is a missile. +function WEAPON:IsMissile() + return self.category==Weapon.Category.MISSILE +end + +--- Check if weapon is a rocket. +-- @param #WEAPON self +-- @return #boolean If `true`, is a missile. +function WEAPON:IsRocket() + return self.category==Weapon.Category.ROCKET +end + +--- Check if weapon is a shell. +-- @param #WEAPON self +-- @return #boolean If `true`, is a shell. +function WEAPON:IsShell() + return self.category==Weapon.Category.SHELL +end + +--- Check if weapon is a torpedo. +-- @param #WEAPON self +-- @return #boolean If `true`, is a torpedo. +function WEAPON:IsTorpedo() + return self.category==Weapon.Category.TORPEDO +end + --- Destroy the weapon object. -- @param #WEAPON self @@ -320,6 +468,7 @@ function WEAPON:Destroy(Delay) self:ScheduleOnce(Delay, WEAPON.Destroy, self, 0) else if self.weapon then + self:T(self.lid.."Destroying Weapon NOW!") self.weapon:destroy() end end @@ -327,34 +476,45 @@ function WEAPON:Destroy(Delay) return self end ---- Start tracking the position of the weapon until it impacts. +--- Start tracking the weapon until it impacts or is destroyed otherwise. -- The position of the weapon is monitored in small time steps. Once the position cannot be determined anymore, the monitoring is stopped and the last known position is -- the (approximate) impact point. Of course, the smaller the time step, the better the position can be determined. However, this can hit the performance as many -- calculations per second need to be carried out. -- @param #WEAPON self --- @param #function FuncImpact Function called when weapon has impacted. First argument is the impact coordinate Core.Point#COORDINATE. --- @param ... Optional arguments passed to the impact function after the impact coordinate. +-- @param #number Delay Delay in seconds before the tracking starts. Default 0.001 sec. This is also the minimum. -- @return #WEAPON self --- --- @usage --- -- Function called on impact. --- local function impactfunc(Coordinate, Weapon) --- Coordinate:MarkToAll("Impact Coordinate of weapon") --- end --- --- myweapon:Track(impactfunc) --- -function WEAPON:TrackPosition(FuncImpact, ...) +function WEAPON:StartTrack(Delay) + + Delay=math.max(Delay or 0.001, 0.001) -- Debug info. - self:T(self.lid..string.format("Tracking weapon")) - - -- Callback function on impact. - self.impactFunc=FuncImpact - self.impactArg=arg or {} + self:T(self.lid..string.format("Start tracking weapon in %.4f sec", Delay)) -- Weapon is not yet "alife" just yet. Start timer in 0.001 seconds. - timer.scheduleFunction(WEAPON._TrackPosition, self, timer.getTime() + 0.001) + self.trackScheduleID=timer.scheduleFunction(WEAPON._TrackWeapon, self, timer.getTime() + Delay) + + return self +end + + +--- Stop tracking the weapon by removing the scheduler function. +-- @param #WEAPON self +-- @param #number Delay (Optional) Delay in seconds before the tracking is stopped. +-- @return #WEAPON self +function WEAPON:StopTrack(Delay) + + if Delay and Delay>0 then + -- Delayed call. + self:ScheduleOnce(Delay, WEAPON.StopTrack, self, 0) + else + + if self.trackScheduleID then + + timer.removeFunction(self.trackScheduleID) + + end + + end return self end @@ -367,12 +527,12 @@ end -- @param #WEAPON self -- @param DCS#Time time Time in seconds. -- @return #number Time when called next or nil if not called again. -function WEAPON:_TrackPosition(time) +function WEAPON:_TrackWeapon(time) -- Debug info. - --self:I(string.format("Tracking at T=%.5f", time)) + self:T3(self.lid..string.format("Tracking at T=%.5f", time)) - -- When the pcall returns a failure the weapon has hit. + -- Protected call to get the weapon position. If the position cannot be determined any more, the weapon has impacted and status is nil. local status, pos3= pcall( function() local point=self.weapon:getPosition() @@ -392,6 +552,14 @@ function WEAPON:_TrackPosition(time) -- Update last known vec3. self.vec3 = self.pos3.p + -- Keep on tracking by returning the next time below. + self.tracking=true + + -- Callback function. + if self.trackFunc then + self.trackFunc(self, unpack(self.trackArg or {})) + end + if self.verbose>=5 then local vec2={x=self.vec3.x, y=self.vec3.z} @@ -413,16 +581,14 @@ function WEAPON:_TrackPosition(time) end - -- Check again in ~0.01 seconds ==> 100 checks per second. - return time+(self.dtTrackPos or 0.01) else --------------------------- -- Weapon does NOT exist -- --------------------------- - -- Get intercept point from position (p) and direction (x) in 20 meters. - local ip = land.getIP(self.pos3.p, self.pos3.x, 20) --DCS#Vec3 + -- Get intercept point from position (p) and direction (x) in 50 meters. + local ip = self:_GetIP(50) if ip then env.info("FF Got intercept point!") @@ -441,22 +607,37 @@ function WEAPON:_TrackPosition(time) end - -- Set impact vec3. + -- Safe impact vec3. self.impactVec3=ip or self.vec3 - -- Set impact coordinate. + -- Safe impact coordinate. self.impactCoord=COORDINATE:NewFromVec3(self.vec3) - --self.impactCoord:MarkToAll("Impact point") + -- Mark impact point on F10 map. + if self.markImpact then + self.impactCoord:MarkToAll(string.format("Impact point of weapon %s\ntype=%s\nlauncher=%s", self.name, self.typeName, self.launcherName)) + end -- Call callback function. if self.impactFunc then - self.impactFunc(self.impactCoord, self, self.impactArg) + self.impactFunc(self, unpack(self.impactArg or {})) end + + -- Stop tracking by returning nil below. + self.tracking=false + + end - return nil + -- Return next time the function is called or nil to stop the scheduler. + if self.tracking then + if self.dtTrack and self.dtTrack>0.001 then + return time+self.dtTrack + else + return nil + end end + return nil end --- Compute estimated intercept/impact point (IP) based on last known position and direction. From 6d967358da28d066b81e4f24630033ed288d4c89 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 4 Feb 2023 23:28:45 +0100 Subject: [PATCH 08/15] WEAPON v0.1.0 **WEAPON** - Improments of class - ARTY works - FOX works - RANGE should work, not tested. --- Moose Development/Moose/Core/Point.lua | 9 +- .../Moose/Functional/Artillery.lua | 254 +++++++++-------- Moose Development/Moose/Functional/Fox.lua | 145 +++++----- Moose Development/Moose/Functional/Range.lua | 18 +- Moose Development/Moose/Utilities/Utils.lua | 14 + Moose Development/Moose/Wrapper/Unit.lua | 2 +- Moose Development/Moose/Wrapper/Weapon.lua | 256 +++++++++++++++--- 7 files changed, 462 insertions(+), 236 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index a52f4a9da..de6378a22 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1133,12 +1133,15 @@ do -- COORDINATE --- Return the 3D distance in meters between the target COORDINATE and the COORDINATE. -- @param #COORDINATE self - -- @param #COORDINATE TargetCoordinate The target COORDINATE. + -- @param #COORDINATE TargetCoordinate The target COORDINATE. Can also be a DCS#Vec3. -- @return DCS#Distance Distance The distance in meters. function COORDINATE:Get3DDistance( TargetCoordinate ) - local TargetVec3 = TargetCoordinate:GetVec3() + --local TargetVec3 = TargetCoordinate:GetVec3() + local TargetVec3 = {x=TargetCoordinate.x, y=TargetCoordinate.y, z=TargetCoordinate.z} local SourceVec3 = self:GetVec3() - return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.y - SourceVec3.y ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 + --local dist=( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.y - SourceVec3.y ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5 + local dist=UTILS.VecDist3D(TargetVec3, SourceVec3) + return dist end diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 990f0e8a3..52ec23e15 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -103,6 +103,7 @@ -- @field #number coalition The coalition of the arty group. -- @field #boolean respawnafterdeath Respawn arty group after all units are dead. -- @field #number respawndelay Respawn delay in seconds. +-- @field #number dtTrack Time interval in seconds for weapon tracking. -- @extends Core.Fsm#FSM_CONTROLLABLE --- Enables mission designers easily to assign targets for artillery units. Since the implementation is based on a Finite State Model (FSM), the mission designer can @@ -693,7 +694,7 @@ ARTY.db={ --- Arty script version. -- @field #string version -ARTY.version="1.2.0" +ARTY.version="1.3.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -801,6 +802,9 @@ function ARTY:New(group, alias) else self.ismobile=false end + + -- Set track time interval. + self.dtTrack=0.2 -- Set speed to 0.7 of maximum. self.Speed=self.SpeedMax * 0.7 @@ -1497,6 +1501,15 @@ function ARTY:SetStatusInterval(interval) return self end +--- Set time interval for weapon tracking. +-- @param #ARTY self +-- @param #number interval Time interval in seconds. Default 0.2 seconds. +-- @return self +function ARTY:SetTrackInterval(interval) + self.dtTrack=interval or 0.2 + return self +end + --- Set time how it is waited a unit the first shot event happens. If no shot is fired after this time, the task to fire is aborted and the target removed. -- @param #ARTY self -- @param #number waittime Time in seconds. Default 300 seconds. @@ -2129,6 +2142,95 @@ end -- Event Handling ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Function called during tracking of weapon. +-- @param Wrapper.Weapon#WEAPON weapon Weapon object. +-- @param #ARTY self ARTY object. +-- @param #ARTY.Target target Target of the weapon. +function ARTY._FuncTrack(weapon, self, target) + + -- Coordinate and distance to target. + local _coord=weapon.coordinate + local _dist=_coord:Get2DDistance(target.coord) + local _destroyweapon=false + + -- Debug + self:T3(self.lid..string.format("ARTY %s weapon to target dist = %d m", self.groupname,_dist)) + + if target.weapontype==ARTY.WeaponType.IlluminationShells then + + -- Check if within distace. + if _dist0 local _trackillu = self.currentTarget.weapontype==ARTY.WeaponType.IlluminationShells and self.Nillu>0 local _tracksmoke = self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells and self.Nsmoke>0 + + if _tracknuke or _trackillu or _tracksmoke then - self:T(self.lid..string.format("ARTY %s: Tracking of weapon starts in two seconds.", self.groupname)) - - local _peter={} - _peter.weapon=EventData.weapon - _peter.target=UTILS.DeepCopy(self.currentTarget) - - timer.scheduleFunction(_TrackWeapon, _peter, timer.getTime() + 2.0) + -- Debug info. + self:T(self.lid..string.format("ARTY %s: Tracking of weapon starts in two seconds.", self.groupname)) + + -- Create a weapon object. + local weapon=WEAPON:New(EventData.weapon) + + -- Set time step for tracking. + weapon:SetTimeStepTrack(self.dtTrack) + + -- Copy target. We need a copy because it might already be overwritten with the next target during flight of weapon. + local target=UTILS.DeepCopy(self.currentTarget) + + -- Set callback functions. + weapon:SetFuncTrack(ARTY._FuncTrack, self, target) + weapon:SetFuncImpact(ARTY._FuncImpact, self, target) + + -- Start tracking in 2 sec (arty ammo should fly a bit). + weapon:StartTrack(2) end -- Get current ammo. @@ -2842,6 +2856,7 @@ function ARTY:onafterStatus(Controllable, From, Event, To) end for _,targetname in pairs(notpossible) do self:E(self.lid..string.format("%s: Removing target %s because requested weapon is not possible with this type of unit.", self.groupname, targetname)) + env.info("FF 1000",showMessageBox) self:RemoveTarget(targetname) end @@ -3931,9 +3946,10 @@ function ARTY:GetAmmo(display) return nammo, nshells, nrockets, nmissiles end - for _,unit in pairs(units) do + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT - if unit and unit:IsAlive() then + if unit then -- Output. local text=string.format("ARTY group %s - unit %s:\n", self.groupname, unit:GetName()) diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index 13ce14a41..5d2e35865 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -26,6 +26,7 @@ --- FOX class. -- @type FOX -- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. -- @field #boolean Debug Debug mode. Messages to all about status. -- @field #string lid Class id string for output to DCS log file. -- @field #table menuadded Table of groups the menu was added for. @@ -124,6 +125,7 @@ -- @field #FOX FOX = { ClassName = "FOX", + verbose = 0, Debug = false, lid = nil, menuadded = {}, @@ -185,6 +187,8 @@ FOX = { -- @field #string targetName Name of the target unit or "unknown". -- @field #string targetOrig Name of the "original" target, i.e. the one right after launched. -- @field #FOX.PlayerData targetPlayer Player that was targeted or nil. +-- @field Core.Point#COORDINATE missileCoord Missile coordinate during tracking. +-- @field Wrapper.Weapon#WEAPON Weapon Weapon object. --- Main radio menu on group level. -- @field #table MenuF10 Root menu table on group level. @@ -196,7 +200,7 @@ FOX.MenuF10Root=nil --- FOX class version. -- @field #string version -FOX.version="0.7.0" +FOX.version="0.8.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -500,6 +504,7 @@ function FOX:SetDisableF10Menu() return self end + --- Enable F10 menu for all players. -- @param #FOX self -- @return #FOX self @@ -510,6 +515,15 @@ function FOX:SetEnableF10Menu() return self end +--- Set verbosity level. +-- @param #FOX self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #FOX self +function FOX:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + --- Set default player setting for missile destruction. -- @param #FOX self -- @param #boolean switch If true missiles are destroyed. If false/nil missiles are not destroyed. @@ -605,7 +619,9 @@ function FOX:onafterStatus(From, Event, To) local clock=UTILS.SecondsToClock(time) -- Status. - self:I(self.lid..string.format("Missile trainer status %s: %s", clock, fsmstate)) + if self.verbose>=1 then + self:I(self.lid..string.format("Missile trainer status %s: %s", clock, fsmstate)) + end -- Check missile status. self:_CheckMissileStatus() @@ -713,7 +729,9 @@ function FOX:_CheckMissileStatus() if #self.missiles==0 then text=text.." none" end - self:I(self.lid..text) + if self.verbose>=2 then + self:I(self.lid..text) + end -- Remove inactive missiles. for i=#self.missiles,1,-1 do @@ -743,7 +761,7 @@ function FOX:_IsProtected(targetunit) if targetgroup then local targetname=targetgroup:GetName() - for _,_group in pairs(self.protectedset:GetSetObjects()) do + for _,_group in pairs(self.protectedset:GetSet()) do local group=_group --Wrapper.Group#GROUP if group then @@ -763,21 +781,24 @@ function FOX:_IsProtected(targetunit) end ---- Missle launch event. --- @param #FOX self +--- Function called from weapon tracking. -- @param Wrapper.Weapon#WEAPON weapon Weapon object. +-- @param #FOX self FOX object. -- @param #FOX.MissileData missile Fired missile -function FOX:_FuncTrack(weapon, missile) +function FOX._FuncTrack(weapon, self, missile) -- Missile coordinate. - missileCoord=COORDINATE:NewFromVec3(_lastBombPos) + local missileCoord= missile.missileCoord:UpdateFromVec3(weapon.vec3) --COORDINATE:NewFromVec3(_lastBombPos) -- Missile velocity in m/s. - local missileVelocity=UTILS.VecNorm(_ordnance:getVelocity()) + local missileVelocity=weapon:GetSpeed() --UTILS.VecNorm(_ordnance:getVelocity()) -- Update missile target if necessary. self:GetMissileTarget(missile) + -- Target unit of the missile. + local target=nil --Wrapper.Unit#UNIT + if missile.targetUnit then ----------------------------------- @@ -868,13 +889,13 @@ function FOX:_FuncTrack(weapon, missile) if unit:GetName()~=missile.shooterName then -- Player position. - local playerCoord=unit:GetCoordinate() - - -- Distance. - local dist=missileCoord:Get3DDistance(playerCoord) + local playerVec3=unit:GetVec3() + -- Distance. + local dist=missileCoord:Get3DDistance(playerVec3) + -- Distance from shooter to player. - local Dshooter2player=playerCoord:Get3DDistance(missile.shotCoord) + local Dshooter2player=missile.shotCoord:Get3DDistance(playerVec3) -- Update mindist if necessary. Only include players in range of missile + 50% safety margin. if (mindist==nil or dist destroy missile if in safe zone. - if destroymissile and self:_CheckCoordSafe(targetCoord) then + if destroymissile and self:_CheckCoordSafe(targetVec3) then -- Destroy missile. self:I(self.lid..string.format("Destroying missile %s(%s) fired by %s aimed at %s [player=%s] at distance %.1f m", missile.missileType, missile.missileName, missile.shooterName, target:GetName(), tostring(missile.targetPlayer~=nil), distance)) - _ordnance:destroy() + weapon:Destroy() -- Missile is not active any more. missile.active=false @@ -941,7 +961,6 @@ function FOX:_FuncTrack(weapon, missile) -- Debug smoke. if self.Debug then missileCoord:SmokeRed() - targetCoord:SmokeGreen() end -- Create event. @@ -962,9 +981,9 @@ function FOX:_FuncTrack(weapon, missile) -- Increase dead counter. missile.targetPlayer.dead=missile.targetPlayer.dead+1 end - - -- Terminate timer. - return nil + + -- We could disable the tracking here but then the impact function would not be called. + --weapon.tracking=false else @@ -987,30 +1006,30 @@ function FOX:_FuncTrack(weapon, missile) dt=self.dt00 --0.01 end - -- Check again in dt seconds. - return timer.getTime()+dt + -- Set time step. + weapon:SetTimeStepTrack(dt) end else - -- Destroy missile. + -- No current target. self:T(self.lid..string.format("Missile %s(%s) fired by %s has no current target. Checking back in 0.1 sec.", missile.missileType, missile.missileName, missile.shooterName)) - return timer.getTime()+0.1 + weapon:SetTimeStepTrack(0.1) end end --- Callback function on impact or destroy otherwise. --- @param #FOX self -- @param Wrapper.Weapon#WEAPON weapon Weapon object. --- @param #FOX.MissileData missile Fired missile -function FOX:_FuncImpact(weapon, missile) +-- @param #FOX self FOX object. +-- @param #FOX.MissileData missile Fired missile. +function FOX._FuncImpact(weapon, self, missile) - if target then + if missile.targetPlayer then -- Get human player. - local player=self:_GetPlayerFromUnit(target) + local player=missile.targetPlayer -- Check for player and distance < 10 km. if player and player.unit:IsAlive() then -- and missileCoord and player.unit:GetCoordinate():Get3DDistance(missileCoord)<10*1000 then @@ -1028,7 +1047,7 @@ function FOX:_FuncImpact(weapon, missile) --Terminate the timer. self:T(FOX.lid..string.format("Terminating missile track timer.")) - return nil + weapon.tracking=false end @@ -1090,18 +1109,17 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) end end - local weapon=WEAPON:New(missile.weapon) + -- Set callback function for tracking. + missile.Weapon:SetFuncTrack(FOX._FuncTrack, self, missile) - weapon:SetFuncTrack(FuncTrack) + -- Set callback function for impact. + missile.Weapon:SetFuncImpact(FOX._FuncImpact, self, missile) - weapon:SetFuncImpact(FuncImpact) - - -- Weapon is not yet "alife" just yet. Start timer with a little delay. self:T(FOX.lid..string.format("Tracking of missile starts in 0.0001 seconds.")) --timer.scheduleFunction(trackMissile, missile.weapon, timer.getTime()+0.0001) - weapon:StartTrack(0.0001) + missile.Weapon:StartTrack(0.0001) end @@ -1232,30 +1250,29 @@ end -- @param Core.Event#EVENTDATA EventData function FOX:OnEventShot(EventData) self:T2({eventshot=EventData}) + + -- Nil checks. + if EventData.Weapon==nil or EventData.IniDCSUnit==nil or EventData.weapon==nil then + return + end - if EventData.Weapon==nil then - return - end - if EventData.IniDCSUnit==nil then - return - end + -- Create a weapon object. + local weapon=WEAPON:New(EventData.weapon) -- Weapon data. - local _weapon = EventData.WeaponName + local _weapon = weapon:GetTypeName() local _target = EventData.Weapon:getTarget() local _targetName = "unknown" local _targetUnit = nil --Wrapper.Unit#UNIT -- Weapon descriptor. - local desc=EventData.Weapon:getDesc() + local desc=weapon.desc self:T2({desc=desc}) - -- Weapon category: 0=Shell, 1=Missile, 2=Rocket, 3=BOMB - local weaponcategory=desc.category - -- Missile category: 1=AAM, 2=SAM, 6=OTHER local missilecategory=desc.missileCategory + -- Missile range. local missilerange=nil if missilecategory then missilerange=desc.rangeMaxAltMax @@ -1265,8 +1282,8 @@ function FOX:OnEventShot(EventData) self:T2(FOX.lid.."EVENT SHOT: FOX") self:T2(FOX.lid..string.format("EVENT SHOT: Ini unit = %s", tostring(EventData.IniUnitName))) self:T2(FOX.lid..string.format("EVENT SHOT: Ini group = %s", tostring(EventData.IniGroupName))) - self:T2(FOX.lid..string.format("EVENT SHOT: Weapon type = %s", tostring(_weapon))) - self:T2(FOX.lid..string.format("EVENT SHOT: Weapon categ = %s", tostring(weaponcategory))) + self:T2(FOX.lid..string.format("EVENT SHOT: Weapon type = %s", tostring(weapon:GetTypeName()))) + self:T2(FOX.lid..string.format("EVENT SHOT: Weapon categ = %s", tostring(weapon:GetCategory()))) self:T2(FOX.lid..string.format("EVENT SHOT: Missil categ = %s", tostring(missilecategory))) self:T2(FOX.lid..string.format("EVENT SHOT: Missil range = %s", tostring(missilerange))) @@ -1278,7 +1295,7 @@ function FOX:OnEventShot(EventData) end -- Track missiles of type AAM=1, SAM=2 or OTHER=6 - local _track = weaponcategory==1 and missilecategory and (missilecategory==1 or missilecategory==2 or missilecategory==6) + local _track = weapon:IsMissile() and missilecategory and (missilecategory==1 or missilecategory==2 or missilecategory==6) -- Only track missiles if _track then @@ -1287,6 +1304,7 @@ function FOX:OnEventShot(EventData) missile.active=true missile.weapon=EventData.weapon + missile.Weapon=weapon missile.missileType=_weapon missile.missileRange=missilerange missile.missileName=EventData.weapon:getName() @@ -1299,6 +1317,7 @@ function FOX:OnEventShot(EventData) missile.fuseDist=desc.fuseDist missile.explosive=desc.warhead.explosiveMass or desc.warhead.shapedExplosiveMass missile.targetOrig=missile.targetName + missile.missileCoord=COORDINATE:New(0,0,0) -- Set missile target name, unit and player. self:GetMissileTarget(missile) @@ -1617,7 +1636,7 @@ end --- Check if a coordinate lies within a safe training zone. -- @param #FOX self --- @param Core.Point#COORDINATE coord Coordinate to check. +-- @param Core.Point#COORDINATE coord Coordinate to check. Can also be a DCS#Vec3. -- @return #boolean True if safe. function FOX:_CheckCoordSafe(coord) @@ -1629,7 +1648,9 @@ function FOX:_CheckCoordSafe(coord) -- Loop over all zones. for _,_zone in pairs(self.safezones) do local zone=_zone --Core.Zone#ZONE - local inzone=zone:IsCoordinateInZone(coord) + local Vec2={x=coord.x, y=coord.z} + local inzone=zone:IsVec2InZone(Vec2) + --local inzone=zone:IsCoordinateInZone(coord) if inzone then return true end @@ -1640,7 +1661,7 @@ end --- Check if a coordinate lies within a launch zone. -- @param #FOX self --- @param Core.Point#COORDINATE coord Coordinate to check. +-- @param Core.Point#COORDINATE coord Coordinate to check. Can also be a DCS#Vec2. -- @return #boolean True if in launch zone. function FOX:_CheckCoordLaunch(coord) @@ -1652,7 +1673,9 @@ function FOX:_CheckCoordLaunch(coord) -- Loop over all zones. for _,_zone in pairs(self.launchzones) do local zone=_zone --Core.Zone#ZONE - local inzone=zone:IsCoordinateInZone(coord) + local Vec2={x=coord.x, y=coord.z} + local inzone=zone:IsVec2InZone(Vec2) + --local inzone=zone:IsCoordinateInZone(coord) if inzone then return true end diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 34b79bd7d..878bef1df 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -1833,13 +1833,13 @@ function RANGE:OnEventHit( EventData ) end --- Function called on impact of a tracked weapon. --- @param #RANGE self -- @param Wrapper.Weapon#WEAPON weapon The weapon object. +-- @param #RANGE self RANGE object. -- @param #RANGE.PlayerData playerData Player data table. -- @param #number attackHdg Attack heading. -- @param #number attackAlt Attack altitude. -- @param #number attackVel Attack velocity. -function RANGE:_OnImpact(weapon, playerData, attackHdg, attackAlt, attackVel) +function RANGE._OnImpact(weapon, self, playerData, attackHdg, attackAlt, attackVel) -- Get closet target to last position. local _closetTarget = nil -- #RANGE.BombTarget @@ -1964,10 +1964,6 @@ function RANGE:_OnImpact(weapon, playerData, attackHdg, attackAlt, attackVel) self:T( self.lid .. "Weapon impacted outside range zone." ) end - -- Terminate the timer - self:T( self.lid .. string.format( "Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername ) ) - return nil - end --- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). @@ -1993,7 +1989,7 @@ function RANGE:OnEventShot( EventData ) -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) - -- Set this to larger value than the threshold. + -- Distance Player-to-Range. Set this to larger value than the threshold. local dPR = self.BombtrackThreshold * 2 -- Distance player to range. @@ -2027,14 +2023,6 @@ function RANGE:OnEventShot( EventData ) end ---- Check spawn queue and spawn aircraft if necessary. --- @param #RANGE self --- @param #string PlayerName Name of player. --- @return #RANGE.BombResult -function RANGE:_GetBombResults(PlayerName) - -end - ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM Functions ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index d81856f3b..26b5555fe 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1197,6 +1197,20 @@ function UTILS.HdgDiff(h1, h2) return math.abs(delta) end +--- Returns the heading from one vec3 to another vec3. +-- @param DCS#Vec3 a From vec3. +-- @param DCS#Vec3 b To vec3. +-- @return #number Heading in degrees. +function UTILS.HdgTo(a, b) + local dz=b.z-a.z + local dx=b.x-a.x + local heading=math.deg(math.atan2(dz, dx)) + if heading < 0 then + heading = 360 + heading + end + return heading +end + --- Translate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 596f670e4..1b582a546 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -167,7 +167,7 @@ end --- Get the DCS unit object. -- @param #UNIT self --- @return DCS#Unit +-- @return DCS#Unit The DCS unit object. function UNIT:GetDCSObject() local DCSUnit = Unit.getByName( self.UnitName ) diff --git a/Moose Development/Moose/Wrapper/Weapon.lua b/Moose Development/Moose/Wrapper/Weapon.lua index b34000a09..b8437d7ca 100644 --- a/Moose Development/Moose/Wrapper/Weapon.lua +++ b/Moose Development/Moose/Wrapper/Weapon.lua @@ -3,10 +3,12 @@ -- ## Main Features: -- -- * Convenient access to DCS API functions --- * Track weapon and get impact position +-- * Track weapon and get impact position -- * Get launcher and target of weapon -- * Define callback function when weapon impacts --- * Destroy weapon before impact +-- * Define callback function when tracking weapon +-- * Mark impact points on F10 map +-- * Put coloured smoke on impact points -- -- === -- @@ -31,7 +33,8 @@ -- @field DCS#Weapon weapon The DCS weapon object. -- @field #string name Name of the weapon object. -- @field #string typeName Type name of the weapon. --- @field #number category Weapon category 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X). +-- @field #number category Weapon category 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB, 4=TORPEDO (Weapon.Category.X). +-- @field #number categoryMissile Missile category 0=AAM, 1=SAM, 2=BM, 3=ANTI_SHIP, 4=CRUISE, 5=OTHER (Weapon.MissileCategory.X). -- @field #number coalition Coalition ID. -- @field #number country Country ID. -- @field DCS#Desc desc Descriptor table. @@ -45,11 +48,18 @@ -- @field #table trackArg Optional arguments for the track callback function. -- @field DCS#Vec3 vec3 Last known 3D position vector of the tracked weapon. -- @field DCS#Position3 pos3 Last known 3D position and direction vector of the tracked weapon. +-- @field Core.Point#COORDINATE coordinate Coordinate object of the weapon. Can be used in other classes. -- @field DCS#Vec3 impactVec3 Impact 3D vector. -- @field Core.Point#COORDINATE impactCoord Impact coordinate. -- @field #number trackScheduleID Tracking scheduler ID. Can be used to remove/destroy the scheduler function. -- @field #boolean tracking If `true`, scheduler will keep tracking. Otherwise, function will return nil and stop tracking. --- @field #boolean markImpact If `true`, the impact point is marked on the F10 map. Requires tracking to be started. +-- @field #boolean impactMark If `true`, the impact point is marked on the F10 map. Requires tracking to be started. +-- @field #boolean impactSmoke If `true`, the impact point is marked by smoke. Requires tracking to be started. +-- @field #number impactSmokeColor Colour of impact point smoke. +-- @field #boolean impactDestroy If `true`, destroy weapon before impact. Requires tracking to be started and sufficiently small time step. +-- @field #number impactDestroyDist Distance in meters to the estimated impact point. If smaller, then weapon is destroyed. +-- @field #number distIP Distance in meters for the intercept point estimation. +-- @field Wrapper.Unit#UNIT target Last known target. -- @extends Wrapper.Positionable#POSITIONABLE --- *In the long run, the sharpest weapon of all is a kind and gentle spirit.* -- Anne Frank @@ -68,15 +78,21 @@ -- -- # Tracking -- --- The status of the weapon can be tracked with the @{#WEAPON.StartTrack}() function. This function will try to determin the position of the weapon in (normally) relatively --- small time steps. The time step can be set via the @{#WEAPON.SetTimeStepTrack} function and is by default set to 0.01 secons. +-- The status of the weapon can be tracked with the @{#WEAPON.StartTrack} function. This function will try to determin the position of the weapon in (normally) relatively +-- small time steps. The time step can be set via the @{#WEAPON.SetTimeStepTrack} function and is by default set to 0.01 seconds. -- -- Once the position cannot be retrieved any more, the weapon has impacted (or was destroyed otherwise) and the last known position is safed as the impact point. -- The impact point can be accessed with the @{#WEAPON.GetImpactVec3} or @{#WEAPON.GetImpactCoordinate} functions. -- +-- ## Impact Point Marking +-- +-- You can mark the impact point on the F10 map with @{#WEAPON.SetMarkImpact}. +-- +-- You can also trigger coloured smoke at the impact point via @{#WEAPON.SetSmokeImpact}. +-- -- ## Callback functions -- --- It is possible to define functions that are called during the tracking of the weapon and upon impact. +-- It is possible to define functions that are called during the tracking of the weapon and upon impact, which help you to customize further actions. -- -- ### Callback on Impact -- @@ -86,6 +102,38 @@ -- -- The function called each time the weapon status is tracked can be set with @{#WEAPON.SetFuncTrack} -- +-- # Target +-- +-- If the weapon has a specific target, you can get it with the @{#WEAPON.GetTarget} function. Note that the object, which is returned can vary. Normally, it is a UNIT +-- but it could also be a STATIC object. +-- +-- Also note that the weapon does not always have a target, it can loose a target and re-aquire it and the target might change to another unit. +-- +-- You can get the target name with the @{#WEAPON.GetTargetName} function. +-- +-- The distance to the target is returned by the @{#WEAPON.GetTargetDistance} function. +-- +-- # Category +-- +-- The category (bomb, rocket, missile, shell, torpedo) of the weapon can be retrieved with the @{#WEAPON.GetCategory} function. +-- +-- You can check if the weapon is a +-- +-- * bomb with @{#WEAPON.IsBomb} +-- * rocket with @{#WEAPON.IsRocket} +-- * missile with @{#WEAPON.IsMissile} +-- * shell with @{#WEAPON.IsShell} +-- * torpedo with @{#WEAPON.IsTorpedo} +-- +-- # Parameters +-- +-- You can get various parameters of the weapon, *e.g.* +-- +-- * position: @{#WEAPON.GetVec3}, @{#WEAPON.GetVec2 }, @{#WEAPON.GetCoordinate} +-- * speed: @{#WEAPON.GetSpeed} +-- * coalition: @{#WEAPON.GetCoalition} +-- * country: @{#WEAPON.GetCountry} +-- -- # Dependencies -- -- This class is used (at least) in the MOOSE classes: @@ -103,7 +151,7 @@ WEAPON = { --- WEAPON class version. -- @field #string version -WEAPON.version="0.0.1" +WEAPON.version="0.1.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -149,7 +197,7 @@ function WEAPON:New(WeaponObject) end -- Get type name. - self.typeName=WeaponObject:getTypeName() + self.typeName=WeaponObject:getTypeName() or "Unknown Type" -- Get name of object. Usually a number like "1234567". self.name=WeaponObject:getName() @@ -170,18 +218,23 @@ function WEAPON:New(WeaponObject) self.launcherUnit=UNIT:Find(self.launcher) end + -- Init the coordinate of the weapon from that of the launcher. + self.coordinate=COORDINATE:NewFromVec3(self.launcher:getPoint()) + -- Set log ID. self.lid=string.format("[%s] %s | ", self.typeName, self.name) -- Set default parameters self:SetTimeStepTrack() + self:SetDistanceInterceptPoint() -- Debug info. - local text=string.format("FF Weapon: Name=%s, TypeName=%s, Category=%s, Coalition=%d, Country=%d, Launcher=%s", - self.name, self.typeName, self.category, self.coalition, self.country, self.launcherName) - env.info(text) + local text=string.format("Weapon v%s\nName=%s, TypeName=%s, Category=%s, Coalition=%d, Country=%d, Launcher=%s", + self.version, self.name, self.typeName, self.category, self.coalition, self.country, self.launcherName) + self:T(self.lid..text) - self:I(self.desc) + -- Descriptors. + self:T2(self.desc) return self end @@ -208,6 +261,19 @@ function WEAPON:SetTimeStepTrack(TimeStep) return self end +--- Set distance of intercept point for estimated impact point. +-- If the weapon cannot be tracked any more, the intercept point from its last known position and direction is used to get +-- a better approximation of the impact point. Can be useful when using longer time steps in the tracking and still achieve +-- a good result on the impact point. +-- It uses the DCS function [getIP](https://wiki.hoggitworld.com/view/DCS_func_getIP). +-- @param #WEAPON self +-- @param #number Distance Distance in meters. Default is 50 m. Set to 0 to deactivate. +-- @return #WEAPON self +function WEAPON:SetDistanceInterceptPoint(Distance) + self.distIP=Distance or 50 + return self +end + --- Mark impact point on the F10 map. This requires that the tracking has been started. -- @param #WEAPON self -- @param #boolean Switch If `true` or nil, impact is marked. @@ -215,14 +281,33 @@ end function WEAPON:SetMarkImpact(Switch) if Switch==false then - self.markImpact=false + self.impactMark=false else - self.markImpact=true + self.impactMark=true end return self end + +--- Put smoke on impact point. This requires that the tracking has been started. +-- @param #WEAPON self +-- @param #boolean Switch If `true` or nil, impact is smoked. +-- @param #number SmokeColor Color of smoke. Default is `SMOKECOLOR.Red`. +-- @return #WEAPON self +function WEAPON:SetSmokeImpact(Switch, SmokeColor) + + if Switch==false then + self.impactSmoke=false + else + self.impactSmoke=true + end + + self.impactSmokeColor=SmokeColor or SMOKECOLOR.Red + + return self +end + --- Set callback function when weapon is tracked and still alive. The first argument will be the WEAPON object. -- Note that this can be called many times per second. So be careful for performance reasons. -- @param #WEAPON self @@ -284,20 +369,19 @@ function WEAPON:GetTarget() -- Get object category. local category=object:getCategory() - -- Get object name. + --Target name local name=object:getName() - + -- Debug info. - self:I(self.lid..string.format("Got Target Object %s, category=%d", name, category)) - + self:T(self.lid..string.format("Got Target Object %s, category=%d", object:getName(), category)) if category==Object.Category.UNIT then - target=UNIT:Find(object) + target=UNIT:FindByName(name) elseif category==Object.Category.STATIC then - target=STATIC:Find(object) + target=STATIC:FindByName(name, false) elseif category==Object.Category.SCENERY then self:E(self.lid..string.format("ERROR: Scenery target not implemented yet!")) @@ -308,10 +392,57 @@ function WEAPON:GetTarget() end end - return target end +--- Get the distance to the current target the weapon is guiding to. +-- @param #WEAPON self +-- @param #function ConversionFunction (Optional) Conversion function from meters to desired unit, *e.g.* `UTILS.MpsToKmph`. +-- @return #number Distance from weapon to target in meters. +function WEAPON:GetTargetDistance(ConversionFunction) + + -- Get the target of the weapon. + local target=self:GetTarget() --Wrapper.Unit#UNIT + + local distance=nil + if target then + + -- Current position of target. + local tv3=target:GetVec3() + + -- Current position of weapon. + local wv3=self:GetVec3() + + if tv3 and wv3 then + distance=UTILS.VecDist3D(tv3, wv3) + + if ConversionFunction then + distance=ConversionFunction(distance) + end + + end + + end + + return distance +end + + +--- Get name the current target the weapon is guiding to. +-- @param #WEAPON self +-- @return #string Name of the target or "None" if no target. +function WEAPON:GetTargetName() + + -- Get the target of the weapon. + local target=self:GetTarget() --Wrapper.Unit#UNIT + + local name="None" + if target then + name=target:GetName() + end + + return name +end --- Get velocity vector of weapon. -- @param #WEAPON self @@ -326,8 +457,9 @@ end --- Get speed of weapon. -- @param #WEAPON self +-- @param #function ConversionFunction (Optional) Conversion function from m/s to desired unit, *e.g.* `UTILS.MpsToKmph`. -- @return #number Speed in meters per second. -function WEAPON:GetSpeed() +function WEAPON:GetSpeed(ConversionFunction) local speed=nil @@ -337,6 +469,10 @@ function WEAPON:GetSpeed() speed=UTILS.VecNorm(v) + if ConversionFunction then + speed=ConversionFunction(speed) + end + end return speed @@ -344,7 +480,7 @@ end --- Get the current 3D position vector. -- @param #WEAPON self --- @return DCS#Vec3 +-- @return DCS#Vec3 Current position vector in 3D. function WEAPON:GetVec3() local vec3=nil @@ -355,6 +491,24 @@ function WEAPON:GetVec3() return vec3 end + +--- Get the current 2D position vector. +-- @param #WEAPON self +-- @return DCS#Vec2 Current position vector in 2D. +function WEAPON:GetVec2() + + local vec3=self:GetVec3() + + if vec3 then + + local vec2={x=vec3.x, y=vec3.z} + + return vec2 + end + + return nil +end + --- Get type name. -- @param #WEAPON self -- @return #string The type name. @@ -469,6 +623,7 @@ function WEAPON:Destroy(Delay) else if self.weapon then self:T(self.lid.."Destroying Weapon NOW!") + self:StopTrack() self.weapon:destroy() end end @@ -481,10 +636,11 @@ end -- the (approximate) impact point. Of course, the smaller the time step, the better the position can be determined. However, this can hit the performance as many -- calculations per second need to be carried out. -- @param #WEAPON self --- @param #number Delay Delay in seconds before the tracking starts. Default 0.001 sec. This is also the minimum. +-- @param #number Delay Delay in seconds before the tracking starts. Default 0.001 sec. -- @return #WEAPON self function WEAPON:StartTrack(Delay) + -- Set delay before start. Delay=math.max(Delay or 0.001, 0.001) -- Debug info. @@ -530,7 +686,9 @@ end function WEAPON:_TrackWeapon(time) -- Debug info. - self:T3(self.lid..string.format("Tracking at T=%.5f", time)) + if self.verbose>=20 then + self:I(self.lid..string.format("Tracking at T=%.5f", time)) + end -- Protected call to get the weapon position. If the position cannot be determined any more, the weapon has impacted and status is nil. local status, pos3= pcall( @@ -550,33 +708,41 @@ function WEAPON:_TrackWeapon(time) self.pos3 = pos3 -- Update last known vec3. - self.vec3 = self.pos3.p + self.vec3 = UTILS.DeepCopy(self.pos3.p) + + -- Update coordinate. + self.coordinate:UpdateFromVec3(self.vec3) -- Keep on tracking by returning the next time below. self.tracking=true -- Callback function. if self.trackFunc then - self.trackFunc(self, unpack(self.trackArg or {})) + self.trackFunc(self, unpack(self.trackArg)) end + -- Verbose output. if self.verbose>=5 then + -- Get vec2 of current position. local vec2={x=self.vec3.x, y=self.vec3.z} + -- Land hight. local height=land.getHeight(vec2) -- Current height above ground level. local agl=self.vec3.y-height -- Estimated IP (if any) - local ip=self:_GetIP(100) + local ip=self:_GetIP(self.distIP) + -- Distance between positon and estimated impact. local d=0 if ip then d=UTILS.VecDist3D(self.vec3, ip) end + -- Output. self:I(self.lid..string.format("T=%.3f: Height=%.3f m AGL=%.3f m, dIP=%.3f", time, height, agl, d)) end @@ -588,10 +754,12 @@ function WEAPON:_TrackWeapon(time) --------------------------- -- Get intercept point from position (p) and direction (x) in 50 meters. - local ip = self:_GetIP(50) + local ip = self:_GetIP(self.distIP) - if ip then - env.info("FF Got intercept point!") + if self.verbose>=10 and ip then + + -- Output. + self:I(self.lid.."Got intercept point!") -- Coordinate of the impact point. local coord=COORDINATE:NewFromVec3(ip) @@ -603,7 +771,8 @@ function WEAPON:_TrackWeapon(time) -- Distance to last known pos. local d=UTILS.VecDist3D(ip, self.vec3) - env.info(string.format("FF d(ip, vec3)=%.3f meters", d)) + -- Output. + self:I(self.lid..string.format("FF d(ip, vec3)=%.3f meters", d)) end @@ -614,10 +783,15 @@ function WEAPON:_TrackWeapon(time) self.impactCoord=COORDINATE:NewFromVec3(self.vec3) -- Mark impact point on F10 map. - if self.markImpact then + if self.impactMark then self.impactCoord:MarkToAll(string.format("Impact point of weapon %s\ntype=%s\nlauncher=%s", self.name, self.typeName, self.launcherName)) end + -- Smoke on impact point. + if self.impactSmoke then + self.impactCoord:Smoke(self.impactSmokeColor) + end + -- Call callback function. if self.impactFunc then self.impactFunc(self, unpack(self.impactArg or {})) @@ -642,12 +816,20 @@ end --- Compute estimated intercept/impact point (IP) based on last known position and direction. -- @param #WEAPON self --- @param #number Distance Distance in meters. Default 20 m. +-- @param #number Distance Distance in meters. Default 50 m. -- @return DCS#Vec3 Estimated intercept/impact point. Can also return `nil`, if no IP can be determined. function WEAPON:_GetIP(Distance) - -- Get intercept point from position (p) and direction (x) in 20 meters. - local ip = land.getIP(self.pos3.p, self.pos3.x, Distance or 20) --DCS#Vec3 + Distance=Distance or 50 + + local ip=nil --DCS#Vec3 + + if Distance>0 and self.pos3 then + + -- Get intercept point from position (p) and direction (x) in 20 meters. + ip = land.getIP(self.pos3.p, self.pos3.x, Distance or 20) --DCS#Vec3 + + end return ip end From 709fccd96c485205c821fa75dd263c87d6bfb573 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 9 Feb 2023 01:06:36 +0100 Subject: [PATCH 09/15] OSPTRANSPORT --- Moose Development/Moose/Ops/OpsGroup.lua | 21 ++++++--- Moose Development/Moose/Ops/OpsTransport.lua | 49 +++++++++++++++++--- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 24cb5da82..df746311f 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -7955,7 +7955,7 @@ function OPSGROUP:_CheckCargoTransport() end -- Boarding finished ==> Transport cargo. - if gotcargo and self.cargoTransport:_CheckRequiredCargos(self.cargoTZC) and not boarding then + if gotcargo and self.cargoTransport:_CheckRequiredCargos(self.cargoTZC, self) and not boarding then self:T(self.lid.."Boarding finished ==> Loaded") self:LoadingDone() else @@ -9329,6 +9329,8 @@ function OPSGROUP:onafterUnloading(From, Event, To) -- Set carrier status to UNLOADING. self:_NewCarrierStatus(OPSGROUP.CarrierStatus.UNLOADING) + + self:I(self.lid.."FF Unloading..") -- Deploy zone. local zone=self.cargoTZC.DisembarkZone or self.cargoTZC.DeployZone --Core.Zone#ZONE @@ -9341,7 +9343,6 @@ function OPSGROUP:onafterUnloading(From, Event, To) if cargo.opsgroup:IsLoaded(self.groupname) and not cargo.opsgroup:IsDead() then -- Disembark to carrier. - local needscarrier=false --#boolean local carrier=nil --Ops.OpsGroup#OPSGROUP.Element local carrierGroup=nil --Ops.OpsGroup#OPSGROUP @@ -9354,17 +9355,21 @@ function OPSGROUP:onafterUnloading(From, Event, To) carrier=carrierGroup:GetElementByName(shipname) end - if self.cargoTZC.DisembarkCarriers and #self.cargoTZC.DisembarkCarriers>0 then - - needscarrier=true + if self.cargoTZC.disembarkToCarriers then + + self:I(self.lid.."FF Unloading 100") + self:I(zone:GetName()) + -- Try to find a carrier that can take the cargo. carrier, carrierGroup=self.cargoTransport:FindTransferCarrierForCargo(cargo.opsgroup, zone, self.cargoTZC) --TODO: max unloading time if transfer carrier does not arrive in the zone. end - if needscarrier==false or (needscarrier and carrier and carrierGroup) then + if (self.cargoTZC.disembarkToCarriers and carrier and carrierGroup) or (not self.cargoTZC.disembarkToCarriers) then + + self:I(self.lid.."FF Unloading 200") -- Cargo was delivered (somehow). cargo.delivered=true @@ -9383,7 +9388,7 @@ function OPSGROUP:onafterUnloading(From, Event, To) elseif zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then --- - -- Delivered to a ship via helo or VTOL + -- Delivered to a ship via helo that landed on its platform --- -- Issue warning. @@ -9398,6 +9403,8 @@ function OPSGROUP:onafterUnloading(From, Event, To) -- Delivered to deploy zone --- + self:I(self.lid.."FF Unloading 400") + if self.cargoTransport:GetDisembarkInUtero(self.cargoTZC) then -- Unload but keep "in utero" (no coordinate provided). diff --git a/Moose Development/Moose/Ops/OpsTransport.lua b/Moose Development/Moose/Ops/OpsTransport.lua index 3db1707d1..41b7fd4c2 100644 --- a/Moose Development/Moose/Ops/OpsTransport.lua +++ b/Moose Development/Moose/Ops/OpsTransport.lua @@ -172,6 +172,7 @@ OPSTRANSPORT.Status={ -- @field #table TransportPaths Path for Transport. Each elment of the table is of type `#OPSTRANSPORT.Path`. -- @field #table RequiredCargos Required cargos. -- @field #table DisembarkCarriers Carriers where the cargo is directly disembarked to. +-- @field #boolean disembarkToCarriers If `true`, cargo is supposed to embark to another carrier. -- @field #boolean disembarkActivation If true, troops are spawned in late activated state when disembarked from carrier. -- @field #boolean disembarkInUtero If true, troops are disembarked "in utero". -- @field #boolean assets Cargo assets. @@ -722,7 +723,7 @@ function OPSTRANSPORT:GetDisembarkActivation(TransportZoneCombo) return TransportZoneCombo.disembarkActivation end ---- Set transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. +--- Set/add transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. -- @param #OPSTRANSPORT self -- @param Core.Set#SET_GROUP Carriers Carrier set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object. -- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. @@ -734,6 +735,9 @@ function OPSTRANSPORT:SetDisembarkCarriers(Carriers, TransportZoneCombo) -- Use default TZC if no transport zone combo is provided. TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- Set that we want to disembark to carriers. + TransportZoneCombo.disembarkToCarriers=true if Carriers:IsInstanceOf("GROUP") or Carriers:IsInstanceOf("OPSGROUP") then @@ -1928,31 +1932,61 @@ end --- Check if all required cargos are loaded. -- @param #OPSTRANSPORT self -- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. --- @return #boolean If true, all required cargos are loaded or there is no required cargo. -function OPSTRANSPORT:_CheckRequiredCargos(TransportZoneCombo) +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup The carrier group asking. +-- @return #boolean If true, all required cargos are loaded or there is no required cargo or asking carrier is full. +function OPSTRANSPORT:_CheckRequiredCargos(TransportZoneCombo, CarrierGroup) -- Use default TZC if no transport zone combo is provided. TransportZoneCombo=TransportZoneCombo or self.tzcDefault - local requiredCargos=TransportZoneCombo.RequiredCargos + -- Use input or take all cargos. + local requiredCargos=TransportZoneCombo.RequiredCargos or TransportZoneCombo.Cargos if requiredCargos==nil or #requiredCargos==0 then return true end + -- All carrier names. local carrierNames=self:_GetCarrierNames() - local gotit=true + -- Cargo groups not loaded yet. + local weightmin=nil + for _,_cargo in pairs(requiredCargos) do local cargo=_cargo --Ops.OpsGroup#OPSGROUP + -- Is this cargo loaded into any carrier? + local isLoaded=cargo:IsLoaded(carrierNames) - if not cargo:IsLoaded(carrierNames) then - return false + if not isLoaded then + local weight=cargo:GetWeightTotal() + + if weightmin==nil or weight Date: Sat, 11 Feb 2023 00:21:15 +0100 Subject: [PATCH 10/15] OPSTRANSPORT --- Moose Development/Moose/Ops/OpsGroup.lua | 36 ++++--- Moose Development/Moose/Ops/OpsTransport.lua | 99 +++++++++++++++----- 2 files changed, 101 insertions(+), 34 deletions(-) diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index df746311f..3cb2f38eb 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -496,6 +496,8 @@ OPSGROUP.CargoStatus={ -- @field #OPSGROUP opsgroup The cargo opsgroup. -- @field #boolean delivered If `true`, group was delivered. -- @field #boolean disembarkActivation If `true`, group is activated. If `false`, group is late activated. +-- @field Core.Zone#ZONE disembarkZone Zone where this group is disembarked to. +-- @field Core.Set#SET_OPSGROUP disembarkCarriers Carriers where this group is directly disembared to. -- @field #string status Status of the cargo group. Not used yet. --- OpsGroup version. @@ -5311,7 +5313,8 @@ function OPSGROUP:onafterMissionStart(From, Event, To, Mission) -- IMMOBILE Group --- - env.info(self.lid.."FF Immobile GROUP") + -- Debug info. + self:T(self.lid.."Immobile GROUP!") -- Add waypoint task. UpdateRoute is called inside. local Clock=Mission.Tpush and UTILS.SecondsToClock(Mission.Tpush) or 5 @@ -9330,7 +9333,7 @@ function OPSGROUP:onafterUnloading(From, Event, To) -- Set carrier status to UNLOADING. self:_NewCarrierStatus(OPSGROUP.CarrierStatus.UNLOADING) - self:I(self.lid.."FF Unloading..") + self:T(self.lid.."Unloading..") -- Deploy zone. local zone=self.cargoTZC.DisembarkZone or self.cargoTZC.DeployZone --Core.Zone#ZONE @@ -9345,6 +9348,14 @@ function OPSGROUP:onafterUnloading(From, Event, To) -- Disembark to carrier. local carrier=nil --Ops.OpsGroup#OPSGROUP.Element local carrierGroup=nil --Ops.OpsGroup#OPSGROUP + local disembarkToCarriers=cargo.disembarkCarriers~=nil or self.cargoTZC.disembarkToCarriers + + -- Set specifc zone for this cargo. + if cargo.disembarkZone then + zone=cargo.disembarkZone + end + + self:T(self.lid..string.format("Unloading cargo %s to zone %s", cargo.opsgroup:GetName(), zone and zone:GetName() or "No Zone Found!")) -- Try to get the OPSGROUP if deploy zone is a ship. if zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then @@ -9355,21 +9366,22 @@ function OPSGROUP:onafterUnloading(From, Event, To) carrier=carrierGroup:GetElementByName(shipname) end - if self.cargoTZC.disembarkToCarriers then + if disembarkToCarriers then - self:I(self.lid.."FF Unloading 100") - self:I(zone:GetName()) + -- Debug info. + self:T(self.lid..string.format("Trying to find disembark carriers in zone %s", zone:GetName())) + + -- Disembarkcarriers. + local disembarkCarriers=cargo.disembarkCarriers or self.cargoTZC.DisembarkCarriers -- Try to find a carrier that can take the cargo. - carrier, carrierGroup=self.cargoTransport:FindTransferCarrierForCargo(cargo.opsgroup, zone, self.cargoTZC) + carrier, carrierGroup=self.cargoTransport:FindTransferCarrierForCargo(cargo.opsgroup, zone, disembarkCarriers, self.cargoTZC.DeployAirbase) --TODO: max unloading time if transfer carrier does not arrive in the zone. end - if (self.cargoTZC.disembarkToCarriers and carrier and carrierGroup) or (not self.cargoTZC.disembarkToCarriers) then - - self:I(self.lid.."FF Unloading 200") + if (disembarkToCarriers and carrier and carrierGroup) or (not disembarkToCarriers) then -- Cargo was delivered (somehow). cargo.delivered=true @@ -9403,8 +9415,6 @@ function OPSGROUP:onafterUnloading(From, Event, To) -- Delivered to deploy zone --- - self:I(self.lid.."FF Unloading 400") - if self.cargoTransport:GetDisembarkInUtero(self.cargoTZC) then -- Unload but keep "in utero" (no coordinate provided). @@ -9413,7 +9423,7 @@ function OPSGROUP:onafterUnloading(From, Event, To) else -- Get disembark zone of this TZC. - local DisembarkZone=self.cargoTransport:GetDisembarkZone(self.cargoTZC) + local DisembarkZone=cargo.disembarkZone or self.cargoTransport:GetDisembarkZone(self.cargoTZC) local Coordinate=nil @@ -9436,7 +9446,7 @@ function OPSGROUP:onafterUnloading(From, Event, To) Coordinate=zoneCarrier:GetRandomCoordinate() else - env.info(string.format("FF ERROR carrier element nil!")) + self:E(self.lid..string.format("ERROR carrier element nil!")) end end diff --git a/Moose Development/Moose/Ops/OpsTransport.lua b/Moose Development/Moose/Ops/OpsTransport.lua index 41b7fd4c2..56613fbd0 100644 --- a/Moose Development/Moose/Ops/OpsTransport.lua +++ b/Moose Development/Moose/Ops/OpsTransport.lua @@ -196,7 +196,7 @@ _OPSTRANSPORTID=0 --- Army Group version. -- @field #string version -OPSTRANSPORT.version="0.6.1" +OPSTRANSPORT.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -205,6 +205,7 @@ OPSTRANSPORT.version="0.6.1" -- TODO: Trains. -- TODO: Stop transport. -- TODO: Improve pickup and transport paths. +-- DONE: Disembark parameters per cargo group. -- DONE: Special transport cohorts/legions. Similar to mission. -- DONE: Cancel transport. -- DONE: Allow multiple pickup/depoly zones. @@ -516,9 +517,11 @@ end -- @param #OPSTRANSPORT self -- @param Core.Set#SET_GROUP GroupSet Set of groups to be transported. Can also be passed as a single GROUP or OPSGROUP object. -- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. --- @param #boolean DisembarkActivation If `true`, cargo group is activated when disembarked. If `false`, cargo groups are late activated when disembarked. Default `nil` (usually activated). +-- @param #boolean DisembarkActivation If `true`, cargo group is activated when disembarked. If `false`, cargo groups are late activated when disembarked. Default `nil` (usually activated). +-- @param Core.Zone#ZONE DisembarkZone Zone where the groups disembark to. +-- @param Core.Set#SET_OPSGROUP DisembarkCarriers Carrier groups where the cargo directly disembarks to. -- @return #OPSTRANSPORT self -function OPSTRANSPORT:AddCargoGroups(GroupSet, TransportZoneCombo, DisembarkActivation) +function OPSTRANSPORT:AddCargoGroups(GroupSet, TransportZoneCombo, DisembarkActivation, DisembarkZone, DisembarkCarriers) -- Use default TZC if no transport zone combo is provided. TransportZoneCombo=TransportZoneCombo or self.tzcDefault @@ -527,7 +530,7 @@ function OPSTRANSPORT:AddCargoGroups(GroupSet, TransportZoneCombo, DisembarkActi if GroupSet:IsInstanceOf("GROUP") or GroupSet:IsInstanceOf("OPSGROUP") then -- We got a single GROUP or OPSGROUP object. - local cargo=self:_CreateCargoGroupData(GroupSet, TransportZoneCombo, DisembarkActivation) + local cargo=self:_CreateCargoGroupData(GroupSet, TransportZoneCombo, DisembarkActivation, DisembarkZone, DisembarkCarriers) if cargo then @@ -738,12 +741,24 @@ function OPSTRANSPORT:SetDisembarkCarriers(Carriers, TransportZoneCombo) -- Set that we want to disembark to carriers. TransportZoneCombo.disembarkToCarriers=true + + self:_AddDisembarkCarriers(Carriers, TransportZoneCombo.DisembarkCarriers) + + return self +end + +--- Set/add transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP Carriers Carrier set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object. +-- @param #table Table the table to add. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:_AddDisembarkCarriers(Carriers, Table) if Carriers:IsInstanceOf("GROUP") or Carriers:IsInstanceOf("OPSGROUP") then local carrier=self:_GetOpsGroupFromObject(Carriers) if carrier then - table.insert(TransportZoneCombo.DisembarkCarriers, carrier) + table.insert(Table, carrier) end elseif Carriers:IsInstanceOf("SET_GROUP") or Carriers:IsInstanceOf("SET_OPSGROUP") then @@ -751,7 +766,7 @@ function OPSTRANSPORT:SetDisembarkCarriers(Carriers, TransportZoneCombo) for _,object in pairs(Carriers:GetSet()) do local carrier=self:_GetOpsGroupFromObject(object) if carrier then - table.insert(TransportZoneCombo.DisembarkCarriers, carrier) + table.insert(Table, carrier) end end @@ -759,7 +774,7 @@ function OPSTRANSPORT:SetDisembarkCarriers(Carriers, TransportZoneCombo) self:E(self.lid.."ERROR: Carriers must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!") end - return self + end --- Get transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. @@ -1572,13 +1587,21 @@ end -- @return #boolean If true, all possible cargo was delivered. function OPSTRANSPORT:IsDelivered(Nmin) local is=self:is(OPSTRANSPORT.Status.DELIVERED) - Nmin=Nmin or 0 - if Nmin>self.Ncargo then - Nmin=self.Ncargo - end - if self.Ndeliveredself.Ncargo then +-- Nmin=self.Ncargo +-- end +-- +-- if self.Ndelivered=math.min(self.Ncargo, Nmin) then + is=true end + return is end @@ -1940,7 +1963,18 @@ function OPSTRANSPORT:_CheckRequiredCargos(TransportZoneCombo, CarrierGroup) TransportZoneCombo=TransportZoneCombo or self.tzcDefault -- Use input or take all cargos. - local requiredCargos=TransportZoneCombo.RequiredCargos or TransportZoneCombo.Cargos + local requiredCargos=TransportZoneCombo.Cargos + + -- Check if required cargos was set by user. + if TransportZoneCombo.RequiredCargos and #TransportZoneCombo.RequiredCargos>0 then + requiredCargos=TransportZoneCombo.RequiredCargos + else + requiredCargos={} + for _,_cargo in pairs(TransportZoneCombo.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + table.insert(requiredCargos, cargo.opsgroup) + end + end if requiredCargos==nil or #requiredCargos==0 then return true @@ -2020,24 +2054,25 @@ end -- @param #OPSTRANSPORT self -- @param Ops.OpsGroup#OPSGROUP CargoGroup The cargo group that needs to be loaded into a carrier unit/element of the carrier group. -- @param Core.Zone#ZONE Zone (Optional) Zone where the carrier must be in. --- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @param #table DisembarkCarriers Disembark carriers. +-- @param Wrapper.Airbase#AIRBASE DeployAirbase Airbase where to deploy. -- @return Ops.OpsGroup#OPSGROUP.Element New carrier element for cargo or nil. -- @return Ops.OpsGroup#OPSGROUP New carrier group for cargo or nil. -function OPSTRANSPORT:FindTransferCarrierForCargo(CargoGroup, Zone, TransportZoneCombo) +function OPSTRANSPORT:FindTransferCarrierForCargo(CargoGroup, Zone, DisembarkCarriers, DeployAirbase) -- Use default TZC if no transport zone combo is provided. - TransportZoneCombo=TransportZoneCombo or self.tzcDefault + --TransportZoneCombo=TransportZoneCombo or self.tzcDefault local carrier=nil --Ops.OpsGroup#OPSGROUP.Element local carrierGroup=nil --Ops.OpsGroup#OPSGROUP --TODO: maybe sort the carriers wrt to largest free cargo bay. Or better smallest free cargo bay that can take the cargo group weight. - for _,_carrier in pairs(TransportZoneCombo.DisembarkCarriers) do + for _,_carrier in pairs(DisembarkCarriers or {}) do local carrierGroup=_carrier --Ops.OpsGroup#OPSGROUP -- First check if carrier is alive and loading cargo. - if carrierGroup and carrierGroup:IsAlive() and (carrierGroup:IsLoading() or TransportZoneCombo.DeployAirbase) then + if carrierGroup and carrierGroup:IsAlive() and (carrierGroup:IsLoading() or DeployAirbase) then -- Find an element of the group that has enough free space. carrier=carrierGroup:FindCarrierForCargo(CargoGroup) @@ -2064,8 +2099,10 @@ end -- @param Wrapper.Group#GROUP group The GROUP or OPSGROUP object. -- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. -- @param #boolean DisembarkActivation If `true`, cargo group is activated when disembarked. +-- @param Core.Zone#ZONE DisembarkZone Disembark zone, where the cargo is spawned when delivered. +-- @param Core.Set#SET_OPSGROUP DisembarkCarriers Disembark carriers cargo is directly loaded into when delivered. -- @return Ops.OpsGroup#OPSGROUP.CargoGroup Cargo group data. -function OPSTRANSPORT:_CreateCargoGroupData(group, TransportZoneCombo, DisembarkActivation) +function OPSTRANSPORT:_CreateCargoGroupData(group, TransportZoneCombo, DisembarkActivation, DisembarkZone, DisembarkCarriers) -- Get ops group. local opsgroup=self:_GetOpsGroupFromObject(group) @@ -2086,8 +2123,12 @@ function OPSTRANSPORT:_CreateCargoGroupData(group, TransportZoneCombo, Disembark cargo.opsgroup=opsgroup cargo.delivered=false cargo.status="Unknown" - cargo.disembarkActivation=DisembarkActivation cargo.tzcUID=TransportZoneCombo + cargo.disembarkZone=DisembarkZone + if DisembarkCarriers then + cargo.disembarkCarriers={} + self:_AddDisembarkCarriers(DisembarkCarriers, cargo.disembarkCarriers) + end return cargo end @@ -2112,8 +2153,10 @@ function OPSTRANSPORT:_CountCargosInZone(Zone, Delivered, Carrier, TransportZone if mycarrier and mycarrier:IsUnloading() then + -- Get disembark carriers. local carriers=mycarrier.cargoTransport:GetDisembarkCarriers(mycarrier.cargoTZC) + -- Check if carrier is in the list. for _,_carrier in pairs(carriers) do local carrier=_carrier --Ops.OpsGroup#OPSGROUP if Carrier:GetName()==carrier:GetName() then @@ -2121,6 +2164,20 @@ function OPSTRANSPORT:_CountCargosInZone(Zone, Delivered, Carrier, TransportZone end end + if mycarrier.cargoTZC and mycarrier.cargoTZC.Cargos then + for _,_cargodata in pairs(mycarrier.cargoTZC.Cargos) do + local cargodata=_cargodata --Ops.OpsGroup#OPSGROUP.CargoGroup + if cargo:GetName()==cargodata.opsgroup:GetName() then + for _,_carrier in pairs(cargodata.disembarkCarriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if Carrier:GetName()==carrier:GetName() then + return true + end + end + end + end + end + end return false From 9ec66912c8d156df24d84d46d68bef523f7be29f Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 12 Feb 2023 09:36:35 +0100 Subject: [PATCH 11/15] Update OpsGroup.lua - Added SetCargoBayLimit function --- Moose Development/Moose/Ops/OpsGroup.lua | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 3cb2f38eb..34b81397d 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -1605,6 +1605,31 @@ function OPSGROUP:SetReturnOnOutOfAmmo() return self end +--- Set max weight that each unit of the group can handle. +-- @param #OPSGROUP self +-- @param #number Weight Max weight of cargo in kg the unit can carry. +-- @param #string UnitName Name of the Unit. If not given, weight is set for all units of the group. +-- @return #OPSGROUP self +function OPSGROUP:SetCargoBayLimit(Weight, UnitName) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if UnitName==nil or UnitName==element.name then + + element.weightMaxCargo=Weight + + if element.unit then + element.unit:SetCargoBayWeightLimit(Weight) + end + + end + + end + + return self +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. Can also be a DCS#Vec3. From dc0a2bccd6b291482f94ac9c0139074bf95df3c6 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 12 Feb 2023 15:19:34 +0100 Subject: [PATCH 12/15] Update Legion.lua - Increase score for nearby ground groups --- Moose Development/Moose/Ops/Legion.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index 94821c516..25af27b3f 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -3147,10 +3147,18 @@ function LEGION.CalculateAssetMissionScore(asset, MissionType, TargetVec2, Inclu -- Distance factor. local distance=0 if TargetVec2 and OrigVec2 then + -- Distance in NM. distance=UTILS.MetersToNM(UTILS.VecDist2D(OrigVec2, TargetVec2)) - -- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6 - distance=UTILS.Round(distance/10, 0) + + if asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then + -- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6 + distance=UTILS.Round(distance/10, 0) + else + -- For ground units the distance is a more important factor + distance=UTILS.Round(distance, 0) + end + end -- Reduce score for legions that are futher away. From baa1dcbf8fa35f96cc7fe46c61e85e68196bcfc7 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 15 Feb 2023 23:06:34 +0100 Subject: [PATCH 13/15] OPSGROUP - mission nothing is cancelled if transport is due --- Moose Development/Moose/Ops/OpsGroup.lua | 7 ++++++- Moose Development/Moose/Wrapper/Positionable.lua | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 34b81397d..19b62b238 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -7906,9 +7906,14 @@ function OPSGROUP:_CheckCargoTransport() self.cargoTransport=nil self.cargoTZC=nil end + + local mission=self:GetMissionCurrent() + if mission and mission.type==AUFTRAG.Type.NOTHING then + self:MissionCancel(mission) + end -- Check if there is anything in the queue. - if not self.cargoTransport and not self:IsOnMission() then + if not self.cargoTransport and not self:IsOnMission() then --(mission==nil or (mission and mission.type==AUFTRAG.Type.NOTHING)) then --not self:IsOnMission() then self.cargoTransport=self:_GetNextCargoTransport() if self.cargoTransport and not self:IsActive() then self:Activate() diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 72f9a9fce..fc917bef5 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -1696,6 +1696,7 @@ do -- Cargo ["tt_DSHK"] = 6, ["HL_KORD"] = 6, ["HL_DSHK"] = 6, + ["CCKW_353"] = 16, --GMC CCKW 2½-ton 6×6 truck, estimating 16 soldiers } -- Assuming that each passenger weighs 95 kg on average. From 462e29da6e51915fc504495941347bf9ac946b37 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 22 Feb 2023 22:16:42 +0100 Subject: [PATCH 14/15] RANGE and OPS ** RANGE** - Added scenery as target **OPSGROUP** - Added option that group does not return to its legion **AUFTRAG** - Added option that assets do not return to its legion --- Moose Development/Moose/Functional/Range.lua | 51 +++++++++++++++++--- Moose Development/Moose/Ops/ArmyGroup.lua | 2 +- Moose Development/Moose/Ops/Auftrag.lua | 30 +++++++++--- Moose Development/Moose/Ops/FlightGroup.lua | 2 +- Moose Development/Moose/Ops/Legion.lua | 4 +- Moose Development/Moose/Ops/NavyGroup.lua | 17 ++++--- Moose Development/Moose/Ops/OpsGroup.lua | 43 +++++++++++++---- 7 files changed, 113 insertions(+), 36 deletions(-) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 00abc0922..a6b6606e9 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -591,14 +591,14 @@ RANGE.MenuF10Root = nil --- Range script version. -- @field #string version -RANGE.version = "2.6.0" +RANGE.version = "2.7.0" -- TODO list: --- TODO: Scenery as targets. -- TODO: Verbosity level for messages. -- TODO: Add option for default settings such as smoke off. -- TODO: Add custom weapons, which can be specified by the user. -- TODO: Check if units are still alive. +-- DONE: Scenery as targets. -- DONE: Add statics for strafe pits. -- DONE: Add missiles. -- DONE: Convert env.info() to self:T() @@ -1612,6 +1612,42 @@ function RANGE:AddBombingTargetCoordinate( coord, name, goodhitrange ) return self end +--- Add a scenery object as bombing target. +-- @param #RANGE self +-- @param Wrapper.Scenery#SCENERY scenery Scenary object. +-- @param #number goodhitrange Max distance from unit which is considered as a good hit. +-- @return #RANGE self +function RANGE:AddBombingTargetScenery( scenery, goodhitrange) + + -- Get name of positionable. + local name = scenery:GetName() + + -- Default range is 25 m. + goodhitrange = goodhitrange or RANGE.Defaults.goodhitrange + + -- Debug or error output. + if name then + self:I( self.lid .. string.format( "Adding SCENERY bombing target %s with good hit range %d", name, goodhitrange) ) + else + self:E( self.lid .. string.format( "ERROR! No bombing target with name %s could be found!", name ) ) + end + + + local target = {} -- #RANGE.BombTarget + target.name = name + target.target = scenery + target.goodhitrange = goodhitrange + target.move = false + target.speed = 0 + target.coordinate = scenery:GetCoordinate() + target.type = RANGE.TargetType.SCENERY + + -- Insert target to table. + table.insert( self.bombingTargets, target ) + + return self +end + --- Add all units of a group as bombing targets. -- @param #RANGE self -- @param Wrapper.Group#GROUP group Group of bombing targets. @@ -1933,6 +1969,7 @@ function RANGE._OnImpact(weapon, self, playerData, attackHdg, attackAlt, attackV result.attackHdg = attackHdg result.attackVel = attackVel result.attackAlt = attackAlt + result.date=os and os.date() or "n/a" -- Add to table. table.insert( _results, result ) @@ -2292,10 +2329,7 @@ function RANGE:onafterSave( From, Event, To ) local quality = result.quality local time = UTILS.SecondsToClock(result.time, true) local airframe = result.airframe - local date = "n/a" - if os then - date = os.date() - end + local date = result.date or "n/a" scores = scores .. string.format( "\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time, date ) end end @@ -3399,6 +3433,11 @@ function RANGE:_GetBombTargetCoordinate( target ) -- Coordinates dont move. coord = target.coordinate + + elseif target.type == RANGE.TargetType.SCENERY then + + -- Coordinates dont move. + coord = target.coordinate else self:E( self.lid .. "ERROR: Unknown target type." ) diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index fef3e35f0..57ed7b84d 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -68,7 +68,7 @@ ARMYGROUP = { --- Army Group version. -- @field #string version -ARMYGROUP.version="0.9.0" +ARMYGROUP.version="1.0.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index ff4e95d56..cf646d6f6 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -158,6 +158,7 @@ -- @field Core.Point#COORDINATE missionWaypointCoord Mission waypoint coordinate. -- @field Core.Point#COORDINATE missionEgressCoord Mission egress waypoint coordinate. -- @field #number missionWaypointRadius Random radius in meters. +-- @field #boolean legionReturn If `true`, assets return to their legion (default). If `false`, they will stay alive. -- -- @field #table enrouteTasks Mission enroute tasks. -- @@ -639,7 +640,7 @@ AUFTRAG.Category={ --- AUFTRAG class version. -- @field #string version -AUFTRAG.version="1.0.0" +AUFTRAG.version="1.1.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -648,6 +649,7 @@ AUFTRAG.version="1.0.0" -- TODO: Replace engageRange by missionRange. Here and in other classes. CTRL+H is your friend! -- TODO: Mission success options damaged, destroyed. -- TODO: F10 marker to create new missions. +-- DONE: Add option that assets do not return to their legion. -- DONE: Add Capture zone task. -- DONE: Add orbit mission for moving anker points. -- DONE: Add recovery tanker mission for boat ops. @@ -1039,7 +1041,7 @@ end -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. -- @param #number Altitude Orbit altitude in feet above sea level. Default is y component of `Coordinate`. --- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS. -- @param #number Heading Heading of race-track pattern in degrees. If not specified, a circular orbit is performed. -- @param #number Leg Length of race-track in NM. If not specified, a circular orbit is performed. -- @return #AUFTRAG self @@ -1058,7 +1060,8 @@ function AUFTRAG:NewORBIT(Coordinate, Altitude, Speed, Heading, Leg) end -- Orbit speed in m/s. - mission.orbitSpeed = UTILS.KnotsToMps(UTILS.KnotsToAltKIAS(Speed or 350, UTILS.MetersToFeet(mission.orbitAltitude))) + --mission.orbitSpeed = UTILS.KnotsToMps(UTILS.KnotsToAltKIAS(Speed or 350, UTILS.MetersToFeet(mission.orbitAltitude))) + mission.orbitSpeed = UTILS.TasToIas(UTILS.KnotsToMps(Speed or 350), mission.orbitAltitude) -- Mission speed in km/h. mission.missionSpeed = UTILS.KnotsToKmph(Speed or 350) @@ -1093,7 +1096,7 @@ end -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Position where to orbit around. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. --- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS. -- @return #AUFTRAG self function AUFTRAG:NewORBIT_CIRCLE(Coordinate, Altitude, Speed) @@ -1106,7 +1109,7 @@ end -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. --- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS. -- @param #number Heading Heading of race-track pattern in degrees. Default random in [0, 360) degrees. -- @param #number Leg Length of race-track in NM. Default 10 NM. -- @return #AUFTRAG self @@ -1124,7 +1127,7 @@ end -- @param #AUFTRAG self -- @param Wrapper.Group#GROUP Group Group where to orbit around. Can also be a UNIT object. -- @param #number Altitude Orbit altitude in feet. Default is 6,000 ft. --- @param #number Speed Orbit speed in knots. Default 350 KIAS. +-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS. -- @param #number Leg Length of race-track in NM. Default nil. -- @param #number Heading Heading of race-track pattern in degrees. Default is heading of the group. -- @param DCS#Vec2 OffsetVec2 Offset 2D-vector {x=0, y=0} in NM with respect to the group. Default directly overhead. Can also be given in polar coordinates `{r=5, phi=45}`. @@ -1172,7 +1175,7 @@ end -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. --- @param #number Speed Orbit speed in knots. Default 350 kts. +-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS. -- @param #number Heading Heading of race-track pattern in degrees. Default random in [0, 360) degrees. -- @param #number Leg Length of race-track in NM. Default 10 NM. -- @return #AUFTRAG self @@ -1199,7 +1202,7 @@ end -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. --- @param #number Speed Orbit speed in knots. Default 350 kts. +-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS. -- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). -- @param #number Leg Length of race-track in NM. Default 10 NM. -- @param #number RefuelSystem Refueling system (0=boom, 1=probe). This info is *only* for AIRWINGs so they launch the right tanker type. @@ -2657,6 +2660,17 @@ function AUFTRAG:SetTeleport(Switch) return self end +--- **[LEGION, COMMANDER, CHIEF]** Set whether assigned assets return to their legion once the mission is over. This is only applicable to **army** and **navy** groups, *i.e.* aircraft +-- will always return. +-- @param #AUFTRAG self +-- @param #boolean Switch If `true`, assets will return. If `false`, assets will not return and stay where it finishes its last mission. If `nil`, let asset decide. +-- @return #AUFTRAG self +function AUFTRAG:SetReturnToLegion(Switch) + self.legionReturn=Switch + self:T(self.lid..string.format("Setting ReturnToLetion=%s", tostring(self.legionReturn))) + return self +end + --- Set mission push time. This is the time the mission is executed. If the push time is not passed, the group will wait at the mission execution waypoint. -- @param #AUFTRAG self diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index f89d142c7..9ff16c279 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -216,7 +216,7 @@ FLIGHTGROUP.Players={} --- FLIGHTGROUP class version. -- @field #string version -FLIGHTGROUP.version="0.8.4" +FLIGHTGROUP.version="1.0.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index 25af27b3f..a64e6afa4 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -1984,9 +1984,9 @@ end --- Count total number of assets of the legion. -- @param #LEGION self --- @param #boolean InStock If `true`, only assets that are in the warehouse stock/inventory are counted. +-- @param #boolean InStock If `true`, only assets that are in the warehouse stock/inventory are counted. If `false`, only assets that are NOT in stock (i.e. spawned) are counted. If `nil`, all assets are counted. -- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. --- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `GROUP.Attribute.AIR_BOMBER`. -- @return #number Amount of asset groups in stock. function LEGION:CountAssets(InStock, MissionTypes, Attributes) diff --git a/Moose Development/Moose/Ops/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua index 3c52c0258..640815a2a 100644 --- a/Moose Development/Moose/Ops/NavyGroup.lua +++ b/Moose Development/Moose/Ops/NavyGroup.lua @@ -90,7 +90,7 @@ NAVYGROUP = { --- NavyGroup version. -- @field #string version -NAVYGROUP.version="0.7.9" +NAVYGROUP.version="1.0.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -199,11 +199,6 @@ function NAVYGROUP:New(group) -- @param #number Speed Speed in knots until next waypoint is reached. - - - - - --- Triggers the FSM event "TurnIntoWind". -- @function [parent=#NAVYGROUP] TurnIntoWind -- @param #NAVYGROUP self @@ -1038,6 +1033,7 @@ end -- @param #number Speed Speed in knots to the next waypoint. -- @param #number Depth Depth in meters to the next waypoint. function NAVYGROUP:onbeforeUpdateRoute(From, Event, To, n, Speed, Depth) + -- Is transition allowed? We assume yes until proven otherwise. local allowed=true local trepeat=nil @@ -1057,6 +1053,9 @@ function NAVYGROUP:onbeforeUpdateRoute(From, Event, To, n, Speed, Depth) elseif self:IsHolding() then self:T(self.lid.."Update route denied. Group is holding position!") return false + elseif self:IsEngaging() then + self:T(self.lid.."Update route allowed. Group is engaging!") + return true end -- Check for a current task. @@ -1074,7 +1073,10 @@ function NAVYGROUP:onbeforeUpdateRoute(From, Event, To, n, Speed, Depth) self:T2(self.lid.."Allowing update route for Task: ReconMission") elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then -- For relocate - self:T2(self.lid.."Allowing update route for Task: Relocate Cohort") + self:T2(self.lid.."Allowing update route for Task: Relocate Cohort") + elseif task.dcstask.id==AUFTRAG.SpecialTask.REARMING then + -- For rearming + self:T2(self.lid.."Allowing update route for Task: Rearming") else local taskname=task and task.description or "No description" self:T(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s", self.taskcurrent, tostring(taskname))) @@ -1106,7 +1108,6 @@ function NAVYGROUP:onbeforeUpdateRoute(From, Event, To, n, Speed, Depth) end return allowed - end --- On after "UpdateRoute" event. diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 19b62b238..210c7b90f 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -502,7 +502,7 @@ OPSGROUP.CargoStatus={ --- OpsGroup version. -- @field #string version -OPSGROUP.version="0.9.0" +OPSGROUP.version="1.0.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -625,6 +625,9 @@ function OPSGROUP:New(group) -- Set Default altitude. self:SetDefaultAltitude() + + -- Group will return to its legion when done. + self:SetReturnToLegion() -- Laser. self.spot={} @@ -1011,6 +1014,20 @@ function OPSGROUP:_SetLegion(Legion) return self end +--- **[GROUND, NAVAL]** Set whether this group should return to its legion once all mission etc are finished. Only for ground and naval groups. Aircraft will +-- @param #OPSGROUP self +-- @param #boolean Switch If `true` or `nil`, group will return. If `false`, group will not return and stay where it finishes its last mission. +-- @return #OPSGROUP self +function OPSGROUP:SetReturnToLegion(Switch) + if Switch==false then + self.legionReturn=false + else + self.legionReturn=true + end + self:T(self.lid..string.format("Setting ReturnToLetion=%s", tostring(self.legionReturn))) + return self +end + --- Set default cruise speed. -- @param #OPSGROUP self -- @param #number Speed Speed in knots. @@ -4385,7 +4402,7 @@ function OPSGROUP:_UpdateTask(Task, Mission) if self:IsArmygroup() or self:IsNavygroup() then -- Especially NAVYGROUP needs a full stop as patrol ad infinitum - self:FullStop() + self:__FullStop(0.1) else -- FLIGHTGROUP not implemented (intended!) for this AUFTRAG type. end @@ -5639,6 +5656,11 @@ function OPSGROUP:onafterMissionDone(From, Event, To, Mission) if Mission.icls then self:_SwitchICLS() end + + -- Return to legion? + if self.legion and Mission.legionReturn~=nil then + self:SetReturnToLegion(Mission.legionReturn) + end -- Delay before check if group is done. local delay=1 @@ -5966,7 +5988,7 @@ function OPSGROUP:RouteToMission(mission, delay) formation=ENUMS.Formation.Vehicle.OffRoad end - waypoint=ARMYGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, formation, false) + waypoint=ARMYGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, formation, false) elseif self:IsNavygroup() then @@ -7906,15 +7928,16 @@ function OPSGROUP:_CheckCargoTransport() self.cargoTransport=nil self.cargoTZC=nil end - + + -- Get current mission (if any). local mission=self:GetMissionCurrent() - if mission and mission.type==AUFTRAG.Type.NOTHING then - self:MissionCancel(mission) - end -- Check if there is anything in the queue. - if not self.cargoTransport and not self:IsOnMission() then --(mission==nil or (mission and mission.type==AUFTRAG.Type.NOTHING)) then --not self:IsOnMission() then + if (not self.cargoTransport) and (mission==nil or mission.type==AUFTRAG.Type.NOTHING) then self.cargoTransport=self:_GetNextCargoTransport() + if self.cargoTransport and mission then + self:MissionCancel(mission) + end if self.cargoTransport and not self:IsActive() then self:Activate() end @@ -9000,7 +9023,7 @@ function OPSGROUP:onafterLoading(From, Event, To) -- Check if current mission is using this ops transport. if isOnMission then local mission=cargo.opsgroup:GetMissionCurrent() - if mission and mission.opstransport and mission.opstransport.uid==self.cargoTransport.uid then + if mission and ((mission.opstransport and mission.opstransport.uid==self.cargoTransport.uid) or mission.type==AUFTRAG.Type.NOTHING) then isOnMission=not isHolding end end @@ -10303,7 +10326,7 @@ function OPSGROUP:_CheckGroupDone(delay) -- Passed FINAL waypoint --- - if self.legion then + if self.legion and self.legionReturn then self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE, LEGION set ==> RTZ")) if self.isArmygroup then From ffef3183c76323aebe333ebe75f87180c70b6d1a Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 22 Feb 2023 23:01:47 +0100 Subject: [PATCH 15/15] OpsGroup - Fixed bug in checklos --- Moose Development/Moose/Functional/Artillery.lua | 9 --------- Moose Development/Moose/Ops/OpsGroup.lua | 4 +++- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 52ec23e15..1a96ee165 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -2249,20 +2249,12 @@ function ARTY:OnEventShot(EventData) self:T3(self.lid.."EVENT SHOT: Weapon name = ".._weaponName) local group = EventData.IniGroup --Wrapper.Group#GROUP - - env.info("FF 100") if group and group:IsAlive() then - - env.info("FF 200") if EventData.IniGroupName == self.groupname then - - env.info("FF 300") if self.currentTarget then - - env.info("FF 400") -- Increase number of shots fired by this group on this target. self.Nshots=self.Nshots+1 @@ -2856,7 +2848,6 @@ function ARTY:onafterStatus(Controllable, From, Event, To) end for _,targetname in pairs(notpossible) do self:E(self.lid..string.format("%s: Removing target %s because requested weapon is not possible with this type of unit.", self.groupname, targetname)) - env.info("FF 1000",showMessageBox) self:RemoveTarget(targetname) end diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 210c7b90f..d39721a86 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -1683,7 +1683,7 @@ function OPSGROUP:HasLoS(Coordinate, Element, OffsetElement, OffsetCoordinate) -- Check los for the given element. if Element.unit and Element.unit:IsAlive() then local vec3=Element.unit:GetVec3() - local los=checklos(Element) + local los=checklos(vec3) return los end else @@ -7091,6 +7091,8 @@ function OPSGROUP:SetLaserTarget(Target) -- Set coordinate. self.spot.Coordinate:UpdateFromVec3(self.spot.vec3) + + self.spot.Coordinate:MarkToAll("Target Laser",ReadOnly,Text) end end