This commit is contained in:
Frank 2023-02-02 23:50:00 +01:00
parent 9af242854a
commit 4b4708e2a8
4 changed files with 687 additions and 563 deletions

View File

@ -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).

View File

@ -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<mindist) and (Dshooter2player<=missile.missileRange*1.5 or dist<=self.explosiondist) then
mindist=dist
target=player.unit
end
end
end
if self.protectedset then
-- Distance to closest protected unit.
mindist=nil
for _,_group in pairs(self.protectedset:GetSet()) do
local group=_group --Wrapper.Group#GROUP
for _,_unit in pairs(group:GetUnits()) do
local unit=_unit --Wrapper.Unit#UNIT
if unit and unit:IsAlive() then
-- Check that player was not the one who launched the missile.
if unit:GetName()~=missile.shooterName then
-- Player position.
local playerCoord=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<mindist) and (Dshooter2player<=missile.missileRange*1.5 or dist<=self.explosiondist) then
mindist=dist
target=unit
end
end
end
end
end
end
if target then
self:T(self.lid..string.format("Missile %s with NO explicit target got closest unit to missile as target %s. Dist=%s m", missile.missileType, target:GetName(), tostring(mindist)))
end
end
-- Check if missile has a valid target.
if target then
-- Target coordinate.
local targetCoord=target:GetCoordinate()
-- Distance from missile to target.
local distance=missileCoord:Get3DDistance(targetCoord)
-- Distance missile to shooter.
local distShooter=nil
if missile.shooterUnit and missile.shooterUnit:IsAlive() then
distShooter=missileCoord:Get3DDistance(missile.shooterUnit:GetCoordinate())
end
-- Debug output.
if self.Debug then
local bearing=targetCoord:HeadingTo(missileCoord)
local eta=distance/missileVelocity
-- Debug distance check.
self:I(self.lid..string.format("Missile %s Target %s: Distance = %.1f m, v=%.1f m/s, bearing=%03d°, ETA=%.1f sec", missile.missileType, target:GetName(), distance, missileVelocity, bearing, eta))
end
-- Distroy missile if it's getting too close.
local destroymissile=distance<=self.explosiondist
-- Check BIG missiles.
if self.explosiondist2 and distance<=self.explosiondist2 and not destroymissile then
destroymissile=missile.explosive>=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<mindist) and (Dshooter2player<=missile.missileRange*1.5 or dist<=self.explosiondist) then
mindist=dist
target=player.unit
end
end
end
if self.protectedset then
-- Distance to closest protected unit.
mindist=nil
for _,_group in pairs(self.protectedset:GetSet()) do
local group=_group --Wrapper.Group#GROUP
for _,_unit in pairs(group:GetUnits()) do
local unit=_unit --Wrapper.Unit#UNIT
if unit and unit:IsAlive() then
-- Check that player was not the one who launched the missile.
if unit:GetName()~=missile.shooterName then
-- Player position.
local playerCoord=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<mindist) and (Dshooter2player<=missile.missileRange*1.5 or dist<=self.explosiondist) then
mindist=dist
target=unit
end
end
end
end
end
end
if target then
self:T(self.lid..string.format("Missile %s with NO explicit target got closest unit to missile as target %s. Dist=%s m", missile.missileType, target:GetName(), tostring(mindist)))
end
end
-- Check if missile has a valid target.
if target then
-- Target coordinate.
local targetCoord=target:GetCoordinate()
-- Distance from missile to target.
local distance=missileCoord:Get3DDistance(targetCoord)
-- Distance missile to shooter.
local distShooter=nil
if missile.shooterUnit and missile.shooterUnit:IsAlive() then
distShooter=missileCoord:Get3DDistance(missile.shooterUnit:GetCoordinate())
end
-- Debug output.
if self.Debug then
local bearing=targetCoord:HeadingTo(missileCoord)
local eta=distance/missileVelocity
-- Debug distance check.
self:I(self.lid..string.format("Missile %s Target %s: Distance = %.1f m, v=%.1f m/s, bearing=%03d°, ETA=%.1f sec", missile.missileType, target:GetName(), distance, missileVelocity, bearing, eta))
end
-- Distroy missile if it's getting too close.
local destroymissile=distance<=self.explosiondist
-- Check BIG missiles.
if self.explosiondist2 and distance<=self.explosiondist2 and not destroymissile then
destroymissile=missile.explosive>=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

View File

@ -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

View File

@ -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.