From 4b4708e2a83108863894a29fb2f5bb372df40b5a Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 2 Feb 2023 23:50:00 +0100 Subject: [PATCH] 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.