Last Updates

This commit is contained in:
FlightControl
2016-09-12 12:29:20 +02:00
parent dac29f6356
commit cd4d4af559
241 changed files with 6314 additions and 1827 deletions

View File

@@ -0,0 +1,253 @@
--- This module contains the AIBALANCER class.
--
-- ===
--
-- 1) @{AIBalancer#AIBALANCER} class, extends @{StateMachine#STATEMACHINE_SET}
-- ===================================================================================
-- The @{AIBalancer#AIBALANCER} class monitors and manages as many AI GROUPS as there are
-- CLIENTS in a SET_CLIENT collection not occupied by players.
-- The AIBALANCER class manages internally a collection of AI management objects, which govern the behaviour
-- of the underlying AI GROUPS.
--
-- The parent class @{StateMachine#STATEMACHINE_SET} manages the functionality to control the Finite State Machine (FSM)
-- and calls for each event the state transition methods providing the internal @{StateMachine#STATEMACHINE_SET.Set} object containing the
-- SET_GROUP and additional event parameters provided during the event.
--
-- 1.1) AIBALANCER construction method
-- ---------------------------------------
-- Create a new AIBALANCER object with the @{#AIBALANCER.New} method:
--
-- * @{#AIBALANCER.New}: Creates a new AIBALANCER object.
--
-- 1.2)
-- ----
-- * Add
-- * Remove
--
-- 1.2) AIBALANCER returns AI to Airbases
-- ------------------------------------------
-- You can configure to have the AI to return to:
--
-- * @{#AIBALANCER.ReturnToHomeAirbase}: Returns the AI to the home @{Airbase#AIRBASE}.
-- * @{#AIBALANCER.ReturnToNearestAirbases}: Returns the AI to the nearest friendly @{Airbase#AIRBASE}.
-- --
-- ===
--
-- **API CHANGE HISTORY**
-- ======================
--
-- The underlying change log documents the API changes. Please read this carefully. The following notation is used:
--
-- * **Added** parts are expressed in bold type face.
-- * _Removed_ parts are expressed in italic type face.
--
-- Hereby the change log:
--
-- 2016-08-17: SPAWN:**InitCleanUp**( SpawnCleanUpInterval ) replaces SPAWN:_CleanUp_( SpawnCleanUpInterval )
--
-- * Want to ensure that the methods starting with **Init** are the first called methods before any _Spawn_ method is called!
-- * This notation makes it now more clear which methods are initialization methods and which methods are Spawn enablement methods.
--
-- ===
--
-- AUTHORS and CONTRIBUTIONS
-- =========================
--
-- ### Contributions:
--
-- * **Dutch_Baron (James)**: Who you can search on the Eagle Dynamics Forums.
-- Working together with James has resulted in the creation of the AIBALANCER class.
-- James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-)
--
-- * **SNAFU**:
-- Had a couple of mails with the guys to validate, if the same concept in the GCI/CAP script could be reworked within MOOSE.
-- None of the script code has been used however within the new AIBALANCER moose class.
--
-- ### Authors:
--
-- * FlightControl: Framework Design & Programming
--
-- @module AIBalancer
--- AIBALANCER class
-- @type AIBALANCER
-- @field Set#SET_CLIENT SetClient
-- @extends StateMachine#STATEMACHINE_SET
AIBALANCER = {
ClassName = "AIBALANCER",
PatrolZones = {},
AIGroups = {},
}
--- Creates a new AIBALANCER object
-- @param #AIBALANCER self
-- @param Set#SET_CLIENT SetClient A SET\_CLIENT object that will contain the CLIENT objects to be monitored if they are alive or not (joined by a player).
-- @param Spawn#SPAWN SpawnAI The default Spawn object to spawn new AI Groups when needed.
-- @return #AIBALANCER
-- @usage
-- -- Define a new AIBALANCER Object.
function AIBALANCER:New( SetClient, SpawnAI )
local FSMT = {
initial = 'None',
events = {
{ name = 'Start', from = '*', to = 'Monitoring' },
{ name = 'Monitor', from = '*', to = 'Monitoring' },
{ name = 'Spawn', from = '*', to = 'Spawning' },
{ name = 'Destroy', from = '*', to = 'Destroying' },
{ name = 'Return', from = '*', to = 'Returning' },
{ name = 'End', from = '*', to = 'End' },
{ name = 'Dead', from = '*', to = 'End' },
},
}
-- Inherits from BASE
local self = BASE:Inherit( self, STATEMACHINE_SET:New( FSMT, SET_GROUP:New() ) )
self.SetClient = SetClient
self.SpawnAI = SpawnAI
self.ToNearestAirbase = false
self.ToHomeAirbase = false
self:__Start( 1 )
return self
end
--- Returns the AI to the nearest friendly @{Airbase#AIRBASE}.
-- @param #AIBALANCER self
-- @param DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}.
-- @param Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Set#SET_AIRBASE}s to evaluate where to return to.
function AIBALANCER:ReturnToNearestAirbases( ReturnTresholdRange, ReturnAirbaseSet )
self.ToNearestAirbase = true
self.ReturnTresholdRange = ReturnTresholdRange
self.ReturnAirbaseSet = ReturnAirbaseSet
end
--- Returns the AI to the home @{Airbase#AIRBASE}.
-- @param #AIBALANCER self
-- @param DCSTypes#Distance ReturnTresholdRange If there is an enemy @{Client#CLIENT} within the ReturnTresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}.
function AIBALANCER:ReturnToHomeAirbase( ReturnTresholdRange )
self.ToHomeAirbase = true
self.ReturnTresholdRange = ReturnTresholdRange
end
--- @param #AIBALANCER self
-- @param Set#SET_GROUP SetGroup
-- @param #string ClientName
-- @param Group#GROUP AIGroup
function AIBALANCER:onenterSpawning( SetGroup, ClientName )
-- OK, Spawn a new group from the default SpawnAI object provided.
local AIGroup = self.SpawnAI:Spawn()
AIGroup:E( "Spawning new AIGroup" )
--TODO: need to rework UnitName thing ...
SetGroup:Add( ClientName, AIGroup )
end
--- @param #AIBALANCER self
-- @param Set#SET_GROUP SetGroup
-- @param Group#GROUP AIGroup
function AIBALANCER:onenterDestroying( SetGroup, AIGroup )
AIGroup:Destroy()
end
--- @param #AIBALANCER self
-- @param Set#SET_GROUP SetGroup
-- @param Group#GROUP AIGroup
function AIBALANCER:onenterReturning( SetGroup, AIGroup )
local AIGroupTemplate = AIGroup:GetTemplate()
if self.ToHomeAirbase == true then
local WayPointCount = #AIGroupTemplate.route.points
local SwitchWayPointCommand = AIGroup:CommandSwitchWayPoint( 1, WayPointCount, 1 )
AIGroup:SetCommand( SwitchWayPointCommand )
AIGroup:MessageToRed( "Returning to home base ...", 30 )
else
-- Okay, we need to send this Group back to the nearest base of the Coalition of the AI.
--TODO: i need to rework the POINT_VEC2 thing.
local PointVec2 = POINT_VEC2:New( AIGroup:GetVec2().x, AIGroup:GetVec2().y )
local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 )
self:T( ClosestAirbase.AirbaseName )
AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 )
local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase )
AIGroupTemplate.route = RTBRoute
AIGroup:Respawn( AIGroupTemplate )
end
end
--- @param #AIBALANCER self
function AIBALANCER:onenterMonitoring( SetGroup )
self.SetClient:ForEachClient(
--- @param Client#CLIENT Client
function( Client )
self:E(Client.ClientName)
local AIGroup = self.Set:Get( Client.UnitName ) -- Group#GROUP
if Client:IsAlive() then
if AIGroup and AIGroup:IsAlive() == true then
if self.ToNearestAirbase == false and self.ToHomeAirbase == false then
self:Destroy( AIGroup )
else
-- We test if there is no other CLIENT within the self.ReturnTresholdRange of the first unit of the AI group.
-- If there is a CLIENT, the AI stays engaged and will not return.
-- If there is no CLIENT within the self.ReturnTresholdRange, then the unit will return to the Airbase return method selected.
local PlayerInRange = { Value = false }
local RangeZone = ZONE_RADIUS:New( 'RangeZone', AIGroup:GetVec2(), self.ReturnTresholdRange )
self:E( RangeZone )
_DATABASE:ForEachPlayer(
--- @param Unit#UNIT RangeTestUnit
function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange )
self:E( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } )
if RangeTestUnit:IsInZone( RangeZone ) == true then
self:E( "in zone" )
if RangeTestUnit:GetCoalition() ~= AIGroup:GetCoalition() then
self:E( "in range" )
PlayerInRange.Value = true
end
end
end,
--- @param Zone#ZONE_RADIUS RangeZone
-- @param Group#GROUP AIGroup
function( RangeZone, AIGroup, PlayerInRange )
if PlayerInRange.Value == false then
self:Return( AIGroup )
end
end
, RangeZone, AIGroup, PlayerInRange
)
end
self.Set:Remove( Client.UnitName )
end
else
if not AIGroup or not AIGroup:IsAlive() == true then
self:E("client not alive")
self:Spawn( Client.UnitName )
self:E("text after spawn")
end
end
return true
end
)
self:__Monitor( 10 )
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,320 @@
--- The CLEANUP class keeps an area clean of crashing or colliding airplanes. It also prevents airplanes from firing within this area.
-- @module CleanUp
-- @author Flightcontrol
--- The CLEANUP class.
-- @type CLEANUP
-- @extends Base#BASE
CLEANUP = {
ClassName = "CLEANUP",
ZoneNames = {},
TimeInterval = 300,
CleanUpList = {},
}
--- Creates the main object which is handling the cleaning of the debris within the given Zone Names.
-- @param #CLEANUP self
-- @param #table ZoneNames Is a table of zone names where the debris should be cleaned. Also a single string can be passed with one zone name.
-- @param #number TimeInterval The interval in seconds when the clean activity takes place. The default is 300 seconds, thus every 5 minutes.
-- @return #CLEANUP
-- @usage
-- -- Clean these Zones.
-- CleanUpAirports = CLEANUP:New( { 'CLEAN Tbilisi', 'CLEAN Kutaisi' }, 150 )
-- or
-- CleanUpTbilisi = CLEANUP:New( 'CLEAN Tbilisi', 150 )
-- CleanUpKutaisi = CLEANUP:New( 'CLEAN Kutaisi', 600 )
function CLEANUP:New( ZoneNames, TimeInterval ) local self = BASE:Inherit( self, BASE:New() )
self:F( { ZoneNames, TimeInterval } )
if type( ZoneNames ) == 'table' then
self.ZoneNames = ZoneNames
else
self.ZoneNames = { ZoneNames }
end
if TimeInterval then
self.TimeInterval = TimeInterval
end
_EVENTDISPATCHER:OnBirth( self._OnEventBirth, self )
self.CleanUpScheduler = SCHEDULER:New( self, self._CleanUpScheduler, {}, 1, TimeInterval )
return self
end
--- Destroys a group from the simulator, but checks first if it is still existing!
-- @param #CLEANUP self
-- @param DCSGroup#Group GroupObject The object to be destroyed.
-- @param #string CleanUpGroupName The groupname...
function CLEANUP:_DestroyGroup( GroupObject, CleanUpGroupName )
self:F( { GroupObject, CleanUpGroupName } )
if GroupObject then -- and GroupObject:isExist() then
trigger.action.deactivateGroup(GroupObject)
self:T( { "GroupObject Destroyed", GroupObject } )
end
end
--- Destroys a @{DCSUnit#Unit} from the simulator, but checks first if it is still existing!
-- @param #CLEANUP self
-- @param DCSUnit#Unit CleanUpUnit The object to be destroyed.
-- @param #string CleanUpUnitName The Unit name ...
function CLEANUP:_DestroyUnit( CleanUpUnit, CleanUpUnitName )
self:F( { CleanUpUnit, CleanUpUnitName } )
if CleanUpUnit then
local CleanUpGroup = Unit.getGroup(CleanUpUnit)
-- TODO Client bug in 1.5.3
if CleanUpGroup and CleanUpGroup:isExist() then
local CleanUpGroupUnits = CleanUpGroup:getUnits()
if #CleanUpGroupUnits == 1 then
local CleanUpGroupName = CleanUpGroup:getName()
--self:CreateEventCrash( timer.getTime(), CleanUpUnit )
CleanUpGroup:destroy()
self:T( { "Destroyed Group:", CleanUpGroupName } )
else
CleanUpUnit:destroy()
self:T( { "Destroyed Unit:", CleanUpUnitName } )
end
self.CleanUpList[CleanUpUnitName] = nil -- Cleaning from the list
CleanUpUnit = nil
end
end
end
-- TODO check DCSTypes#Weapon
--- Destroys a missile from the simulator, but checks first if it is still existing!
-- @param #CLEANUP self
-- @param DCSTypes#Weapon MissileObject
function CLEANUP:_DestroyMissile( MissileObject )
self:F( { MissileObject } )
if MissileObject and MissileObject:isExist() then
MissileObject:destroy()
self:T( "MissileObject Destroyed")
end
end
function CLEANUP:_OnEventBirth( Event )
self:F( { Event } )
self.CleanUpList[Event.IniDCSUnitName] = {}
self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit
self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup
self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName
self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName
_EVENTDISPATCHER:OnEngineShutDownForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self )
_EVENTDISPATCHER:OnEngineStartUpForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self )
_EVENTDISPATCHER:OnHitForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self )
_EVENTDISPATCHER:OnPilotDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self )
_EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self )
_EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self._EventCrash, self )
_EVENTDISPATCHER:OnShotForUnit( Event.IniDCSUnitName, self._EventShot, self )
--self:AddEvent( world.event.S_EVENT_ENGINE_SHUTDOWN, self._EventAddForCleanUp )
--self:AddEvent( world.event.S_EVENT_ENGINE_STARTUP, self._EventAddForCleanUp )
-- self:AddEvent( world.event.S_EVENT_HIT, self._EventAddForCleanUp ) -- , self._EventHitCleanUp )
-- self:AddEvent( world.event.S_EVENT_CRASH, self._EventCrash ) -- , self._EventHitCleanUp )
-- --self:AddEvent( world.event.S_EVENT_DEAD, self._EventCrash )
-- self:AddEvent( world.event.S_EVENT_SHOT, self._EventShot )
--
-- self:EnableEvents()
end
--- Detects if a crash event occurs.
-- Crashed units go into a CleanUpList for removal.
-- @param #CLEANUP self
-- @param DCSTypes#Event event
function CLEANUP:_EventCrash( Event )
self:F( { Event } )
--TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed.
-- self:T("before getGroup")
-- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired
-- self:T("after getGroup")
-- _grp:destroy()
-- self:T("after deactivateGroup")
-- event.initiator:destroy()
self.CleanUpList[Event.IniDCSUnitName] = {}
self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit
self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup
self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName
self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName
end
--- Detects if a unit shoots a missile.
-- If this occurs within one of the zones, then the weapon used must be destroyed.
-- @param #CLEANUP self
-- @param DCSTypes#Event event
function CLEANUP:_EventShot( Event )
self:F( { Event } )
-- Test if the missile was fired within one of the CLEANUP.ZoneNames.
local CurrentLandingZoneID = 0
CurrentLandingZoneID = routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames )
if ( CurrentLandingZoneID ) then
-- Okay, the missile was fired within the CLEANUP.ZoneNames, destroy the fired weapon.
--_SEADmissile:destroy()
SCHEDULER:New( self, CLEANUP._DestroyMissile, { Event.Weapon }, 0.1 )
end
end
--- Detects if the Unit has an S_EVENT_HIT within the given ZoneNames. If this is the case, destroy the unit.
-- @param #CLEANUP self
-- @param DCSTypes#Event event
function CLEANUP:_EventHitCleanUp( Event )
self:F( { Event } )
if Event.IniDCSUnit then
if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then
self:T( { "Life: ", Event.IniDCSUnitName, ' = ', Event.IniDCSUnit:getLife(), "/", Event.IniDCSUnit:getLife0() } )
if Event.IniDCSUnit:getLife() < Event.IniDCSUnit:getLife0() then
self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName )
SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.IniDCSUnit }, 0.1 )
end
end
end
if Event.TgtDCSUnit then
if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then
self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtDCSUnit:getLife(), "/", Event.TgtDCSUnit:getLife0() } )
if Event.TgtDCSUnit:getLife() < Event.TgtDCSUnit:getLife0() then
self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName )
SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 )
end
end
end
end
--- Add the @{DCSUnit#Unit} to the CleanUpList for CleanUp.
function CLEANUP:_AddForCleanUp( CleanUpUnit, CleanUpUnitName )
self:F( { CleanUpUnit, CleanUpUnitName } )
self.CleanUpList[CleanUpUnitName] = {}
self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit
self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName
self.CleanUpList[CleanUpUnitName].CleanUpGroup = Unit.getGroup(CleanUpUnit)
self.CleanUpList[CleanUpUnitName].CleanUpGroupName = Unit.getGroup(CleanUpUnit):getName()
self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime()
self.CleanUpList[CleanUpUnitName].CleanUpMoved = false
self:T( { "CleanUp: Add to CleanUpList: ", Unit.getGroup(CleanUpUnit):getName(), CleanUpUnitName } )
end
--- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given ZoneNames. If this is the case, add the Group to the CLEANUP List.
-- @param #CLEANUP self
-- @param DCSTypes#Event event
function CLEANUP:_EventAddForCleanUp( Event )
if Event.IniDCSUnit then
if self.CleanUpList[Event.IniDCSUnitName] == nil then
if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then
self:_AddForCleanUp( Event.IniDCSUnit, Event.IniDCSUnitName )
end
end
end
if Event.TgtDCSUnit then
if self.CleanUpList[Event.TgtDCSUnitName] == nil then
if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then
self:_AddForCleanUp( Event.TgtDCSUnit, Event.TgtDCSUnitName )
end
end
end
end
local CleanUpSurfaceTypeText = {
"LAND",
"SHALLOW_WATER",
"WATER",
"ROAD",
"RUNWAY"
}
--- At the defined time interval, CleanUp the Groups within the CleanUpList.
-- @param #CLEANUP self
function CLEANUP:_CleanUpScheduler()
self:F( { "CleanUp Scheduler" } )
local CleanUpCount = 0
for CleanUpUnitName, UnitData in pairs( self.CleanUpList ) do
CleanUpCount = CleanUpCount + 1
self:T( { CleanUpUnitName, UnitData } )
local CleanUpUnit = Unit.getByName(UnitData.CleanUpUnitName)
local CleanUpGroupName = UnitData.CleanUpGroupName
local CleanUpUnitName = UnitData.CleanUpUnitName
if CleanUpUnit then
self:T( { "CleanUp Scheduler", "Checking:", CleanUpUnitName } )
if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then
local CleanUpUnitVec3 = CleanUpUnit:getPoint()
--self:T( CleanUpUnitVec3 )
local CleanUpUnitVec2 = {}
CleanUpUnitVec2.x = CleanUpUnitVec3.x
CleanUpUnitVec2.y = CleanUpUnitVec3.z
--self:T( CleanUpUnitVec2 )
local CleanUpSurfaceType = land.getSurfaceType(CleanUpUnitVec2)
--self:T( CleanUpSurfaceType )
if CleanUpUnit and CleanUpUnit:getLife() <= CleanUpUnit:getLife0() * 0.95 then
if CleanUpSurfaceType == land.SurfaceType.RUNWAY then
if CleanUpUnit:inAir() then
local CleanUpLandHeight = land.getHeight(CleanUpUnitVec2)
local CleanUpUnitHeight = CleanUpUnitVec3.y - CleanUpLandHeight
self:T( { "CleanUp Scheduler", "Height = " .. CleanUpUnitHeight } )
if CleanUpUnitHeight < 30 then
self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } )
self:_DestroyUnit(CleanUpUnit, CleanUpUnitName)
end
else
self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because on runway and damaged." } )
self:_DestroyUnit(CleanUpUnit, CleanUpUnitName)
end
end
end
-- Clean Units which are waiting for a very long time in the CleanUpZone.
if CleanUpUnit then
local CleanUpUnitVelocity = CleanUpUnit:getVelocity()
local CleanUpUnitVelocityTotal = math.abs(CleanUpUnitVelocity.x) + math.abs(CleanUpUnitVelocity.y) + math.abs(CleanUpUnitVelocity.z)
if CleanUpUnitVelocityTotal < 1 then
if UnitData.CleanUpMoved then
if UnitData.CleanUpTime + 180 <= timer.getTime() then
self:T( { "CleanUp Scheduler", "Destroy due to not moving anymore " .. CleanUpUnitName } )
self:_DestroyUnit(CleanUpUnit, CleanUpUnitName)
end
end
else
UnitData.CleanUpTime = timer.getTime()
UnitData.CleanUpMoved = true
end
end
else
-- Do nothing ...
self.CleanUpList[CleanUpUnitName] = nil -- Not anymore in the DCSRTE
end
else
self:T( "CleanUp: Group " .. CleanUpUnitName .. " cannot be found in DCS RTE, removing ..." )
self.CleanUpList[CleanUpUnitName] = nil -- Not anymore in the DCSRTE
end
end
self:T(CleanUpCount)
return true
end

View File

@@ -0,0 +1,985 @@
--- This module contains the DETECTION classes.
--
-- ===
--
-- 1) @{Detection#DETECTION_BASE} class, extends @{Base#BASE}
-- ==========================================================
-- The @{Detection#DETECTION_BASE} class defines the core functions to administer detected objects.
-- The @{Detection#DETECTION_BASE} class will detect objects within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s).
--
-- 1.1) DETECTION_BASE constructor
-- -------------------------------
-- Construct a new DETECTION_BASE instance using the @{Detection#DETECTION_BASE.New}() method.
--
-- 1.2) DETECTION_BASE initialization
-- ----------------------------------
-- By default, detection will return detected objects with all the detection sensors available.
-- However, you can ask how the objects were found with specific detection methods.
-- If you use one of the below methods, the detection will work with the detection method specified.
-- You can specify to apply multiple detection methods.
--
-- Use the following functions to report the objects it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK:
--
-- * @{Detection#DETECTION_BASE.InitDetectVisual}(): Detected using Visual.
-- * @{Detection#DETECTION_BASE.InitDetectOptical}(): Detected using Optical.
-- * @{Detection#DETECTION_BASE.InitDetectRadar}(): Detected using Radar.
-- * @{Detection#DETECTION_BASE.InitDetectIRST}(): Detected using IRST.
-- * @{Detection#DETECTION_BASE.InitDetectRWR}(): Detected using RWR.
-- * @{Detection#DETECTION_BASE.InitDetectDLINK}(): Detected using DLINK.
--
-- 1.3) Obtain objects detected by DETECTION_BASE
-- ----------------------------------------------
-- DETECTION_BASE builds @{Set}s of objects detected. These @{Set#SET_BASE}s can be retrieved using the method @{Detection#DETECTION_BASE.GetDetectedSets}().
-- The method will return a list (table) of @{Set#SET_BASE} objects.
--
-- ===
--
-- 2) @{Detection#DETECTION_AREAS} class, extends @{Detection#DETECTION_BASE}
-- ===============================================================================
-- The @{Detection#DETECTION_AREAS} class will detect units within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s),
-- and will build a list (table) of @{Set#SET_UNIT}s containing the @{Unit#UNIT}s detected.
-- The class is group the detected units within zones given a DetectedZoneRange parameter.
-- A set with multiple detected zones will be created as there are groups of units detected.
--
-- 2.1) Retrieve the Detected Unit sets and Detected Zones
-- -------------------------------------------------------
-- The DetectedUnitSets methods are implemented in @{Detection#DECTECTION_BASE} and the DetectedZones methods is implemented in @{Detection#DETECTION_AREAS}.
--
-- Retrieve the DetectedUnitSets with the method @{Detection#DETECTION_BASE.GetDetectedSets}(). A table will be return of @{Set#SET_UNIT}s.
-- To understand the amount of sets created, use the method @{Detection#DETECTION_BASE.GetDetectedSetCount}().
-- If you want to obtain a specific set from the DetectedSets, use the method @{Detection#DETECTION_BASE.GetDetectedSet}() with a given index.
--
-- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Detection#DETECTION_BASE.GetDetectionZones}().
-- To understand the amount of zones created, use the method @{Detection#DETECTION_BASE.GetDetectionZoneCount}().
-- If you want to obtain a specific zone from the DetectedZones, use the method @{Detection#DETECTION_BASE.GetDetectionZone}() with a given index.
--
-- 1.4) Flare or Smoke detected units
-- ----------------------------------
-- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place.
--
-- 1.5) Flare or Smoke detected zones
-- ----------------------------------
-- Use the methods @{Detection#DETECTION_AREAS.FlareDetectedZones}() or @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to flare or smoke the detected zones when a new detection has taken place.
--
-- ===
--
-- ### Contributions:
--
-- * Mechanist : Concept & Testing
--
-- ### Authors:
--
-- * FlightControl : Design & Programming
--
-- @module Detection
--- DETECTION_BASE class
-- @type DETECTION_BASE
-- @field Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role.
-- @field DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected.
-- @field #DETECTION_BASE.DetectedObjects DetectedObjects The list of detected objects.
-- @field #table DetectedObjectsIdentified Map of the DetectedObjects identified.
-- @field #number DetectionRun
-- @extends Base#BASE
DETECTION_BASE = {
ClassName = "DETECTION_BASE",
DetectionSetGroup = nil,
DetectionRange = nil,
DetectedObjects = {},
DetectionRun = 0,
DetectedObjectsIdentified = {},
}
--- @type DETECTION_BASE.DetectedObjects
-- @list <#DETECTION_BASE.DetectedObject>
--- @type DETECTION_BASE.DetectedObject
-- @field #string Name
-- @field #boolean Visible
-- @field #string Type
-- @field #number Distance
-- @field #boolean Identified
--- DETECTION constructor.
-- @param #DETECTION_BASE self
-- @param Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role.
-- @param DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected.
-- @return #DETECTION_BASE self
function DETECTION_BASE:New( DetectionSetGroup, DetectionRange )
-- Inherits from BASE
local self = BASE:Inherit( self, BASE:New() )
self.DetectionSetGroup = DetectionSetGroup
self.DetectionRange = DetectionRange
self:InitDetectVisual( false )
self:InitDetectOptical( false )
self:InitDetectRadar( false )
self:InitDetectRWR( false )
self:InitDetectIRST( false )
self:InitDetectDLINK( false )
return self
end
--- Detect Visual.
-- @param #DETECTION_BASE self
-- @param #boolean DetectVisual
-- @return #DETECTION_BASE self
function DETECTION_BASE:InitDetectVisual( DetectVisual )
self.DetectVisual = DetectVisual
end
--- Detect Optical.
-- @param #DETECTION_BASE self
-- @param #boolean DetectOptical
-- @return #DETECTION_BASE self
function DETECTION_BASE:InitDetectOptical( DetectOptical )
self:F2()
self.DetectOptical = DetectOptical
end
--- Detect Radar.
-- @param #DETECTION_BASE self
-- @param #boolean DetectRadar
-- @return #DETECTION_BASE self
function DETECTION_BASE:InitDetectRadar( DetectRadar )
self:F2()
self.DetectRadar = DetectRadar
end
--- Detect IRST.
-- @param #DETECTION_BASE self
-- @param #boolean DetectIRST
-- @return #DETECTION_BASE self
function DETECTION_BASE:InitDetectIRST( DetectIRST )
self:F2()
self.DetectIRST = DetectIRST
end
--- Detect RWR.
-- @param #DETECTION_BASE self
-- @param #boolean DetectRWR
-- @return #DETECTION_BASE self
function DETECTION_BASE:InitDetectRWR( DetectRWR )
self:F2()
self.DetectRWR = DetectRWR
end
--- Detect DLINK.
-- @param #DETECTION_BASE self
-- @param #boolean DetectDLINK
-- @return #DETECTION_BASE self
function DETECTION_BASE:InitDetectDLINK( DetectDLINK )
self:F2()
self.DetectDLINK = DetectDLINK
end
--- Determines if a detected object has already been identified during detection processing.
-- @param #DETECTION_BASE self
-- @param #DETECTION_BASE.DetectedObject DetectedObject
-- @return #boolean true if already identified.
function DETECTION_BASE:IsDetectedObjectIdentified( DetectedObject )
self:F3( DetectedObject.Name )
local DetectedObjectName = DetectedObject.Name
local DetectedObjectIdentified = self.DetectedObjectsIdentified[DetectedObjectName] == true
self:T3( DetectedObjectIdentified )
return DetectedObjectIdentified
end
--- Identifies a detected object during detection processing.
-- @param #DETECTION_BASE self
-- @param #DETECTION_BASE.DetectedObject DetectedObject
function DETECTION_BASE:IdentifyDetectedObject( DetectedObject )
self:F( DetectedObject.Name )
local DetectedObjectName = DetectedObject.Name
self.DetectedObjectsIdentified[DetectedObjectName] = true
end
--- UnIdentify a detected object during detection processing.
-- @param #DETECTION_BASE self
-- @param #DETECTION_BASE.DetectedObject DetectedObject
function DETECTION_BASE:UnIdentifyDetectedObject( DetectedObject )
local DetectedObjectName = DetectedObject.Name
self.DetectedObjectsIdentified[DetectedObjectName] = false
end
--- UnIdentify all detected objects during detection processing.
-- @param #DETECTION_BASE self
function DETECTION_BASE:UnIdentifyAllDetectedObjects()
self.DetectedObjectsIdentified = {} -- Table will be garbage collected.
end
--- Gets a detected object with a given name.
-- @param #DETECTION_BASE self
-- @param #string ObjectName
-- @return #DETECTION_BASE.DetectedObject
function DETECTION_BASE:GetDetectedObject( ObjectName )
self:F3( ObjectName )
if ObjectName then
local DetectedObject = self.DetectedObjects[ObjectName]
-- Only return detected objects that are alive!
local DetectedUnit = UNIT:FindByName( ObjectName )
if DetectedUnit and DetectedUnit:IsAlive() then
if self:IsDetectedObjectIdentified( DetectedObject ) == false then
return DetectedObject
end
end
end
return nil
end
--- Get the detected @{Set#SET_BASE}s.
-- @param #DETECTION_BASE self
-- @return #DETECTION_BASE.DetectedSets DetectedSets
function DETECTION_BASE:GetDetectedSets()
local DetectionSets = self.DetectedSets
return DetectionSets
end
--- Get the amount of SETs with detected objects.
-- @param #DETECTION_BASE self
-- @return #number Count
function DETECTION_BASE:GetDetectedSetCount()
local DetectionSetCount = #self.DetectedSets
return DetectionSetCount
end
--- Get a SET of detected objects using a given numeric index.
-- @param #DETECTION_BASE self
-- @param #number Index
-- @return Set#SET_BASE
function DETECTION_BASE:GetDetectedSet( Index )
local DetectionSet = self.DetectedSets[Index]
if DetectionSet then
return DetectionSet
end
return nil
end
--- Get the detection Groups.
-- @param #DETECTION_BASE self
-- @return Group#GROUP
function DETECTION_BASE:GetDetectionSetGroup()
local DetectionSetGroup = self.DetectionSetGroup
return DetectionSetGroup
end
--- Make a DetectionSet table. This function will be overridden in the derived clsses.
-- @param #DETECTION_BASE self
-- @return #DETECTION_BASE self
function DETECTION_BASE:CreateDetectionSets()
self:F2()
self:E( "Error, in DETECTION_BASE class..." )
end
--- Schedule the DETECTION construction.
-- @param #DETECTION_BASE self
-- @param #number DelayTime The delay in seconds to wait the reporting.
-- @param #number RepeatInterval The repeat interval in seconds for the reporting to happen repeatedly.
-- @return #DETECTION_BASE self
function DETECTION_BASE:Schedule( DelayTime, RepeatInterval )
self:F2()
self.ScheduleDelayTime = DelayTime
self.ScheduleRepeatInterval = RepeatInterval
self.DetectionScheduler = SCHEDULER:New(self, self._DetectionScheduler, { self, "Detection" }, DelayTime, RepeatInterval )
return self
end
--- Form @{Set}s of detected @{Unit#UNIT}s in an array of @{Set#SET_BASE}s.
-- @param #DETECTION_BASE self
function DETECTION_BASE:_DetectionScheduler( SchedulerName )
self:F2( { SchedulerName } )
self.DetectionRun = self.DetectionRun + 1
self:UnIdentifyAllDetectedObjects() -- Resets the DetectedObjectsIdentified table
for DetectionGroupID, DetectionGroupData in pairs( self.DetectionSetGroup:GetSet() ) do
local DetectionGroup = DetectionGroupData -- Group#GROUP
if DetectionGroup:IsAlive() then
local DetectionGroupName = DetectionGroup:GetName()
local DetectionDetectedTargets = DetectionGroup:GetDetectedTargets(
self.DetectVisual,
self.DetectOptical,
self.DetectRadar,
self.DetectIRST,
self.DetectRWR,
self.DetectDLINK
)
for DetectionDetectedTargetID, DetectionDetectedTarget in pairs( DetectionDetectedTargets ) do
local DetectionObject = DetectionDetectedTarget.object -- DCSObject#Object
self:T2( DetectionObject )
if DetectionObject and DetectionObject:isExist() and DetectionObject.id_ < 50000000 then
local DetectionDetectedObjectName = DetectionObject:getName()
local DetectionDetectedObjectPositionVec3 = DetectionObject:getPoint()
local DetectionGroupVec3 = DetectionGroup:GetVec3()
local Distance = ( ( DetectionDetectedObjectPositionVec3.x - DetectionGroupVec3.x )^2 +
( DetectionDetectedObjectPositionVec3.y - DetectionGroupVec3.y )^2 +
( DetectionDetectedObjectPositionVec3.z - DetectionGroupVec3.z )^2
) ^ 0.5 / 1000
self:T2( { DetectionGroupName, DetectionDetectedObjectName, Distance } )
if Distance <= self.DetectionRange then
if not self.DetectedObjects[DetectionDetectedObjectName] then
self.DetectedObjects[DetectionDetectedObjectName] = {}
end
self.DetectedObjects[DetectionDetectedObjectName].Name = DetectionDetectedObjectName
self.DetectedObjects[DetectionDetectedObjectName].Visible = DetectionDetectedTarget.visible
self.DetectedObjects[DetectionDetectedObjectName].Type = DetectionDetectedTarget.type
self.DetectedObjects[DetectionDetectedObjectName].Distance = DetectionDetectedTarget.distance
else
-- if beyond the DetectionRange then nullify...
if self.DetectedObjects[DetectionDetectedObjectName] then
self.DetectedObjects[DetectionDetectedObjectName] = nil
end
end
end
end
self:T2( self.DetectedObjects )
-- okay, now we have a list of detected object names ...
-- Sort the table based on distance ...
table.sort( self.DetectedObjects, function( a, b ) return a.Distance < b.Distance end )
end
end
if self.DetectedObjects then
self:CreateDetectionSets()
end
return true
end
--- DETECTION_AREAS class
-- @type DETECTION_AREAS
-- @field DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target.
-- @field #DETECTION_AREAS.DetectedAreas DetectedAreas A list of areas containing the set of @{Unit}s, @{Zone}s, the center @{Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange.
-- @extends Detection#DETECTION_BASE
DETECTION_AREAS = {
ClassName = "DETECTION_AREAS",
DetectedAreas = { n = 0 },
DetectionZoneRange = nil,
}
--- @type DETECTION_AREAS.DetectedAreas
-- @list <#DETECTION_AREAS.DetectedArea>
--- @type DETECTION_AREAS.DetectedArea
-- @field Set#SET_UNIT Set -- The Set of Units in the detected area.
-- @field Zone#ZONE_UNIT Zone -- The Zone of the detected area.
-- @field #boolean Changed Documents if the detected area has changes.
-- @field #table Changes A list of the changes reported on the detected area. (It is up to the user of the detected area to consume those changes).
-- @field #number AreaID -- The identifier of the detected area.
-- @field #boolean FriendliesNearBy Indicates if there are friendlies within the detected area.
-- @field Unit#UNIT NearestFAC The nearest FAC near the Area.
--- DETECTION_AREAS constructor.
-- @param Detection#DETECTION_AREAS self
-- @param Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role.
-- @param DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected.
-- @param DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target.
-- @return Detection#DETECTION_AREAS self
function DETECTION_AREAS:New( DetectionSetGroup, DetectionRange, DetectionZoneRange )
-- Inherits from DETECTION_BASE
local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup, DetectionRange ) )
self.DetectionZoneRange = DetectionZoneRange
self._SmokeDetectedUnits = false
self._FlareDetectedUnits = false
self._SmokeDetectedZones = false
self._FlareDetectedZones = false
self:Schedule( 0, 30 )
return self
end
--- Add a detected @{#DETECTION_AREAS.DetectedArea}.
-- @param Set#SET_UNIT Set -- The Set of Units in the detected area.
-- @param Zone#ZONE_UNIT Zone -- The Zone of the detected area.
-- @return #DETECTION_AREAS.DetectedArea DetectedArea
function DETECTION_AREAS:AddDetectedArea( Set, Zone )
local DetectedAreas = self:GetDetectedAreas()
DetectedAreas.n = self:GetDetectedAreaCount() + 1
DetectedAreas[DetectedAreas.n] = {}
local DetectedArea = DetectedAreas[DetectedAreas.n]
DetectedArea.Set = Set
DetectedArea.Zone = Zone
DetectedArea.Removed = false
DetectedArea.AreaID = DetectedAreas.n
return DetectedArea
end
--- Remove a detected @{#DETECTION_AREAS.DetectedArea} with a given Index.
-- @param #DETECTION_AREAS self
-- @param #number Index The Index of the detection are to be removed.
-- @return #nil
function DETECTION_AREAS:RemoveDetectedArea( Index )
local DetectedAreas = self:GetDetectedAreas()
local DetectedAreaCount = self:GetDetectedAreaCount()
local DetectedArea = DetectedAreas[Index]
local DetectedAreaSet = DetectedArea.Set
DetectedArea[Index] = nil
return nil
end
--- Get the detected @{#DETECTION_AREAS.DetectedAreas}.
-- @param #DETECTION_AREAS self
-- @return #DETECTION_AREAS.DetectedAreas DetectedAreas
function DETECTION_AREAS:GetDetectedAreas()
local DetectedAreas = self.DetectedAreas
return DetectedAreas
end
--- Get the amount of @{#DETECTION_AREAS.DetectedAreas}.
-- @param #DETECTION_AREAS self
-- @return #number DetectedAreaCount
function DETECTION_AREAS:GetDetectedAreaCount()
local DetectedAreaCount = self.DetectedAreas.n
return DetectedAreaCount
end
--- Get the @{Set#SET_UNIT} of a detecttion area using a given numeric index.
-- @param #DETECTION_AREAS self
-- @param #number Index
-- @return Set#SET_UNIT DetectedSet
function DETECTION_AREAS:GetDetectedSet( Index )
local DetectedSetUnit = self.DetectedAreas[Index].Set
if DetectedSetUnit then
return DetectedSetUnit
end
return nil
end
--- Get the @{Zone#ZONE_UNIT} of a detection area using a given numeric index.
-- @param #DETECTION_AREAS self
-- @param #number Index
-- @return Zone#ZONE_UNIT DetectedZone
function DETECTION_AREAS:GetDetectedZone( Index )
local DetectedZone = self.DetectedAreas[Index].Zone
if DetectedZone then
return DetectedZone
end
return nil
end
--- Background worker function to determine if there are friendlies nearby ...
-- @param #DETECTION_AREAS self
-- @param Unit#UNIT ReportUnit
function DETECTION_AREAS:ReportFriendliesNearBy( ReportGroupData )
self:F2()
local DetectedArea = ReportGroupData.DetectedArea -- Detection#DETECTION_AREAS.DetectedArea
local DetectedSet = ReportGroupData.DetectedArea.Set
local DetectedZone = ReportGroupData.DetectedArea.Zone
local DetectedZoneUnit = DetectedZone.ZoneUNIT
DetectedArea.FriendliesNearBy = false
local SphereSearch = {
id = world.VolumeType.SPHERE,
params = {
point = DetectedZoneUnit:GetVec3(),
radius = 6000,
}
}
--- @param DCSUnit#Unit FoundDCSUnit
-- @param Group#GROUP ReportGroup
-- @param Set#SET_GROUP ReportSetGroup
local FindNearByFriendlies = function( FoundDCSUnit, ReportGroupData )
local DetectedArea = ReportGroupData.DetectedArea -- Detection#DETECTION_AREAS.DetectedArea
local DetectedSet = ReportGroupData.DetectedArea.Set
local DetectedZone = ReportGroupData.DetectedArea.Zone
local DetectedZoneUnit = DetectedZone.ZoneUNIT -- Unit#UNIT
local ReportSetGroup = ReportGroupData.ReportSetGroup
local EnemyCoalition = DetectedZoneUnit:GetCoalition()
local FoundUnitCoalition = FoundDCSUnit:getCoalition()
local FoundUnitName = FoundDCSUnit:getName()
local FoundUnitGroupName = FoundDCSUnit:getGroup():getName()
local EnemyUnitName = DetectedZoneUnit:GetName()
local FoundUnitInReportSetGroup = ReportSetGroup:FindGroup( FoundUnitGroupName ) ~= nil
self:T3( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } )
if FoundUnitCoalition ~= EnemyCoalition and FoundUnitInReportSetGroup == false then
DetectedArea.FriendliesNearBy = true
return false
end
return true
end
world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, ReportGroupData )
end
--- Returns if there are friendlies nearby the FAC units ...
-- @param #DETECTION_AREAS self
-- @return #boolean trhe if there are friendlies nearby
function DETECTION_AREAS:IsFriendliesNearBy( DetectedArea )
self:T3( DetectedArea.FriendliesNearBy )
return DetectedArea.FriendliesNearBy or false
end
--- Calculate the maxium A2G threat level of the DetectedArea.
-- @param #DETECTION_AREAS self
-- @param #DETECTION_AREAS.DetectedArea DetectedArea
function DETECTION_AREAS:CalculateThreatLevelA2G( DetectedArea )
local MaxThreatLevelA2G = 0
for UnitName, UnitData in pairs( DetectedArea.Set:GetSet() ) do
local ThreatUnit = UnitData -- Unit#UNIT
local ThreatLevelA2G = ThreatUnit:GetThreatLevel()
if ThreatLevelA2G > MaxThreatLevelA2G then
MaxThreatLevelA2G = ThreatLevelA2G
end
end
self:T3( MaxThreatLevelA2G )
DetectedArea.MaxThreatLevelA2G = MaxThreatLevelA2G
end
--- Find the nearest FAC of the DetectedArea.
-- @param #DETECTION_AREAS self
-- @param #DETECTION_AREAS.DetectedArea DetectedArea
-- @return Unit#UNIT The nearest FAC unit
function DETECTION_AREAS:NearestFAC( DetectedArea )
local NearestFAC = nil
local MinDistance = 1000000000 -- Units are not further than 1000000 km away from an area :-)
for FACGroupName, FACGroupData in pairs( self.DetectionSetGroup:GetSet() ) do
for FACUnit, FACUnitData in pairs( FACGroupData:GetUnits() ) do
local FACUnit = FACUnitData -- Unit#UNIT
if FACUnit:IsActive() then
local Vec3 = FACUnit:GetVec3()
local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 )
local Distance = PointVec3:Get2DDistance(POINT_VEC3:NewFromVec3( FACUnit:GetVec3() ) )
if Distance < MinDistance then
MinDistance = Distance
NearestFAC = FACUnit
end
end
end
end
DetectedArea.NearestFAC = NearestFAC
end
--- Returns the A2G threat level of the units in the DetectedArea
-- @param #DETECTION_AREAS self
-- @param #DETECTION_AREAS.DetectedArea DetectedArea
-- @return #number a scale from 0 to 10.
function DETECTION_AREAS:GetTreatLevelA2G( DetectedArea )
self:T3( DetectedArea.MaxThreatLevelA2G )
return DetectedArea.MaxThreatLevelA2G
end
--- Smoke the detected units
-- @param #DETECTION_AREAS self
-- @return #DETECTION_AREAS self
function DETECTION_AREAS:SmokeDetectedUnits()
self:F2()
self._SmokeDetectedUnits = true
return self
end
--- Flare the detected units
-- @param #DETECTION_AREAS self
-- @return #DETECTION_AREAS self
function DETECTION_AREAS:FlareDetectedUnits()
self:F2()
self._FlareDetectedUnits = true
return self
end
--- Smoke the detected zones
-- @param #DETECTION_AREAS self
-- @return #DETECTION_AREAS self
function DETECTION_AREAS:SmokeDetectedZones()
self:F2()
self._SmokeDetectedZones = true
return self
end
--- Flare the detected zones
-- @param #DETECTION_AREAS self
-- @return #DETECTION_AREAS self
function DETECTION_AREAS:FlareDetectedZones()
self:F2()
self._FlareDetectedZones = true
return self
end
--- Add a change to the detected zone.
-- @param #DETECTION_AREAS self
-- @param #DETECTION_AREAS.DetectedArea DetectedArea
-- @param #string ChangeCode
-- @return #DETECTION_AREAS self
function DETECTION_AREAS:AddChangeArea( DetectedArea, ChangeCode, AreaUnitType )
DetectedArea.Changed = true
local AreaID = DetectedArea.AreaID
DetectedArea.Changes = DetectedArea.Changes or {}
DetectedArea.Changes[ChangeCode] = DetectedArea.Changes[ChangeCode] or {}
DetectedArea.Changes[ChangeCode].AreaID = AreaID
DetectedArea.Changes[ChangeCode].AreaUnitType = AreaUnitType
self:T( { "Change on Detection Area:", DetectedArea.AreaID, ChangeCode, AreaUnitType } )
return self
end
--- Add a change to the detected zone.
-- @param #DETECTION_AREAS self
-- @param #DETECTION_AREAS.DetectedArea DetectedArea
-- @param #string ChangeCode
-- @param #string ChangeUnitType
-- @return #DETECTION_AREAS self
function DETECTION_AREAS:AddChangeUnit( DetectedArea, ChangeCode, ChangeUnitType )
DetectedArea.Changed = true
local AreaID = DetectedArea.AreaID
DetectedArea.Changes = DetectedArea.Changes or {}
DetectedArea.Changes[ChangeCode] = DetectedArea.Changes[ChangeCode] or {}
DetectedArea.Changes[ChangeCode][ChangeUnitType] = DetectedArea.Changes[ChangeCode][ChangeUnitType] or 0
DetectedArea.Changes[ChangeCode][ChangeUnitType] = DetectedArea.Changes[ChangeCode][ChangeUnitType] + 1
DetectedArea.Changes[ChangeCode].AreaID = AreaID
self:T( { "Change on Detection Area:", DetectedArea.AreaID, ChangeCode, ChangeUnitType } )
return self
end
--- Make text documenting the changes of the detected zone.
-- @param #DETECTION_AREAS self
-- @param #DETECTION_AREAS.DetectedArea DetectedArea
-- @return #string The Changes text
function DETECTION_AREAS:GetChangeText( DetectedArea )
self:F( DetectedArea )
local MT = {}
for ChangeCode, ChangeData in pairs( DetectedArea.Changes ) do
if ChangeCode == "AA" then
MT[#MT+1] = "Detected new area " .. ChangeData.AreaID .. ". The center target is a " .. ChangeData.AreaUnitType .. "."
end
if ChangeCode == "RAU" then
MT[#MT+1] = "Changed area " .. ChangeData.AreaID .. ". Removed the center target."
end
if ChangeCode == "AAU" then
MT[#MT+1] = "Changed area " .. ChangeData.AreaID .. ". The new center target is a " .. ChangeData.AreaUnitType "."
end
if ChangeCode == "RA" then
MT[#MT+1] = "Removed old area " .. ChangeData.AreaID .. ". No more targets in this area."
end
if ChangeCode == "AU" then
local MTUT = {}
for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do
if ChangeUnitType ~= "AreaID" then
MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType
end
end
MT[#MT+1] = "Detected for area " .. ChangeData.AreaID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "."
end
if ChangeCode == "RU" then
local MTUT = {}
for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do
if ChangeUnitType ~= "AreaID" then
MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType
end
end
MT[#MT+1] = "Removed for area " .. ChangeData.AreaID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "."
end
end
return table.concat( MT, "\n" )
end
--- Accepts changes from the detected zone.
-- @param #DETECTION_AREAS self
-- @param #DETECTION_AREAS.DetectedArea DetectedArea
-- @return #DETECTION_AREAS self
function DETECTION_AREAS:AcceptChanges( DetectedArea )
DetectedArea.Changed = false
DetectedArea.Changes = {}
return self
end
--- Make a DetectionSet table. This function will be overridden in the derived clsses.
-- @param #DETECTION_AREAS self
-- @return #DETECTION_AREAS self
function DETECTION_AREAS:CreateDetectionSets()
self:F2()
-- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units.
-- Regroup when needed, split groups when needed.
for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do
local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea
if DetectedArea then
local DetectedSet = DetectedArea.Set
local AreaExists = false -- This flag will determine of the detected area is still existing.
-- First test if the center unit is detected in the detection area.
self:T3( DetectedArea.Zone.ZoneUNIT.UnitName )
local DetectedZoneObject = self:GetDetectedObject( DetectedArea.Zone.ZoneUNIT.UnitName )
self:T3( { "Detecting Zone Object", DetectedArea.AreaID, DetectedArea.Zone, DetectedZoneObject } )
if DetectedZoneObject then
--self:IdentifyDetectedObject( DetectedZoneObject )
AreaExists = true
else
-- The center object of the detected area has not been detected. Find an other unit of the set to become the center of the area.
-- First remove the center unit from the set.
DetectedSet:RemoveUnitsByName( DetectedArea.Zone.ZoneUNIT.UnitName )
self:AddChangeArea( DetectedArea, 'RAU', "Dummy" )
-- Then search for a new center area unit within the set. Note that the new area unit candidate must be within the area range.
for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do
local DetectedUnit = DetectedUnitData -- Unit#UNIT
local DetectedObject = self:GetDetectedObject( DetectedUnit.UnitName )
-- The DetectedObject can be nil when the DetectedUnit is not alive anymore or it is not in the DetectedObjects map.
-- If the DetectedUnit was already identified, DetectedObject will be nil.
if DetectedObject then
self:IdentifyDetectedObject( DetectedObject )
AreaExists = true
-- Assign the Unit as the new center unit of the detected area.
DetectedArea.Zone = ZONE_UNIT:New( DetectedUnit:GetName(), DetectedUnit, self.DetectionZoneRange )
self:AddChangeArea( DetectedArea, "AAU", DetectedArea.Zone.ZoneUNIT:GetTypeName() )
-- We don't need to add the DetectedObject to the area set, because it is already there ...
break
end
end
end
-- Now we've determined the center unit of the area, now we can iterate the units in the detected area.
-- Note that the position of the area may have moved due to the center unit repositioning.
-- If no center unit was identified, then the detected area does not exist anymore and should be deleted, as there are no valid units that can be the center unit.
if AreaExists then
-- ok, we found the center unit of the area, now iterate through the detected area set and see which units are still within the center unit zone ...
-- Those units within the zone are flagged as Identified.
-- If a unit was not found in the set, remove it from the set. This may be added later to other existing or new sets.
for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do
local DetectedUnit = DetectedUnitData -- Unit#UNIT
local DetectedObject = nil
if DetectedUnit:IsAlive() then
--self:E(DetectedUnit:GetName())
DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() )
end
if DetectedObject then
-- Check if the DetectedUnit is within the DetectedArea.Zone
if DetectedUnit:IsInZone( DetectedArea.Zone ) then
-- Yes, the DetectedUnit is within the DetectedArea.Zone, no changes, DetectedUnit can be kept within the Set.
self:IdentifyDetectedObject( DetectedObject )
else
-- No, the DetectedUnit is not within the DetectedArea.Zone, remove DetectedUnit from the Set.
DetectedSet:Remove( DetectedUnitName )
self:AddChangeUnit( DetectedArea, "RU", DetectedUnit:GetTypeName() )
end
else
-- There was no DetectedObject, remove DetectedUnit from the Set.
self:AddChangeUnit( DetectedArea, "RU", "destroyed target" )
DetectedSet:Remove( DetectedUnitName )
-- The DetectedObject has been identified, because it does not exist ...
-- self:IdentifyDetectedObject( DetectedObject )
end
end
else
self:RemoveDetectedArea( DetectedAreaID )
self:AddChangeArea( DetectedArea, "RA" )
end
end
end
-- We iterated through the existing detection areas and:
-- - We checked which units are still detected in each detection area. Those units were flagged as Identified.
-- - We recentered the detection area to new center units where it was needed.
--
-- Now we need to loop through the unidentified detected units and see where they belong:
-- - They can be added to a new detection area and become the new center unit.
-- - They can be added to a new detection area.
for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do
local DetectedObject = self:GetDetectedObject( DetectedUnitName )
if DetectedObject then
-- We found an unidentified unit outside of any existing detection area.
local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Unit#UNIT
local AddedToDetectionArea = false
for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do
local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea
if DetectedArea then
self:T( "Detection Area #" .. DetectedArea.AreaID )
local DetectedSet = DetectedArea.Set
if not self:IsDetectedObjectIdentified( DetectedObject ) and DetectedUnit:IsInZone( DetectedArea.Zone ) then
self:IdentifyDetectedObject( DetectedObject )
DetectedSet:AddUnit( DetectedUnit )
AddedToDetectionArea = true
self:AddChangeUnit( DetectedArea, "AU", DetectedUnit:GetTypeName() )
end
end
end
if AddedToDetectionArea == false then
-- New detection area
local DetectedArea = self:AddDetectedArea(
SET_UNIT:New(),
ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange )
)
--self:E( DetectedArea.Zone.ZoneUNIT.UnitName )
DetectedArea.Set:AddUnit( DetectedUnit )
self:AddChangeArea( DetectedArea, "AA", DetectedUnit:GetTypeName() )
end
end
end
-- Now all the tests should have been build, now make some smoke and flares...
-- We also report here the friendlies within the detected areas.
for DetectedAreaID, DetectedAreaData in ipairs( self.DetectedAreas ) do
local DetectedArea = DetectedAreaData -- #DETECTION_AREAS.DetectedArea
local DetectedSet = DetectedArea.Set
local DetectedZone = DetectedArea.Zone
self:ReportFriendliesNearBy( { DetectedArea = DetectedArea, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table
self:CalculateThreatLevelA2G( DetectedArea ) -- Calculate A2G threat level
self:NearestFAC( DetectedArea )
if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then
DetectedZone.ZoneUNIT:SmokeRed()
end
DetectedSet:ForEachUnit(
--- @param Unit#UNIT DetectedUnit
function( DetectedUnit )
if DetectedUnit:IsAlive() then
self:T( "Detected Set #" .. DetectedArea.AreaID .. ":" .. DetectedUnit:GetName() )
if DETECTION_AREAS._FlareDetectedUnits or self._FlareDetectedUnits then
DetectedUnit:FlareGreen()
end
if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then
DetectedUnit:SmokeGreen()
end
end
end
)
if DETECTION_AREAS._FlareDetectedZones or self._FlareDetectedZones then
DetectedZone:FlareZone( POINT_VEC3.SmokeColor.White, 30, math.random( 0,90 ) )
end
if DETECTION_AREAS._SmokeDetectedZones or self._SmokeDetectedZones then
DetectedZone:SmokeZone( POINT_VEC3.SmokeColor.White, 30 )
end
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,703 @@
--- This module contains the MISSILETRAINER class.
--
-- ===
--
-- 1) @{MissileTrainer#MISSILETRAINER} class, extends @{Base#BASE}
-- ===============================================================
-- The @{#MISSILETRAINER} class uses the DCS world messaging system to be alerted of any missiles fired, and when a missile would hit your aircraft,
-- the class will destroy the missile within a certain range, to avoid damage to your aircraft.
-- It suports the following functionality:
--
-- * Track the missiles fired at you and other players, providing bearing and range information of the missiles towards the airplanes.
-- * Provide alerts of missile launches, including detailed information of the units launching, including bearing, range <20>
-- * Provide alerts when a missile would have killed your aircraft.
-- * Provide alerts when the missile self destructs.
-- * Enable / Disable and Configure the Missile Trainer using the various menu options.
--
-- When running a mission where MISSILETRAINER is used, the following radio menu structure ( 'Radio Menu' -> 'Other (F10)' -> 'MissileTrainer' ) options are available for the players:
--
-- * **Messages**: Menu to configure all messages.
-- * **Messages On**: Show all messages.
-- * **Messages Off**: Disable all messages.
-- * **Tracking**: Menu to configure missile tracking messages.
-- * **To All**: Shows missile tracking messages to all players.
-- * **To Target**: Shows missile tracking messages only to the player where the missile is targetted at.
-- * **Tracking On**: Show missile tracking messages.
-- * **Tracking Off**: Disable missile tracking messages.
-- * **Frequency Increase**: Increases the missile tracking message frequency with one second.
-- * **Frequency Decrease**: Decreases the missile tracking message frequency with one second.
-- * **Alerts**: Menu to configure alert messages.
-- * **To All**: Shows alert messages to all players.
-- * **To Target**: Shows alert messages only to the player where the missile is (was) targetted at.
-- * **Hits On**: Show missile hit alert messages.
-- * **Hits Off**: Disable missile hit alert messages.
-- * **Launches On**: Show missile launch messages.
-- * **Launches Off**: Disable missile launch messages.
-- * **Details**: Menu to configure message details.
-- * **Range On**: Shows range information when a missile is fired to a target.
-- * **Range Off**: Disable range information when a missile is fired to a target.
-- * **Bearing On**: Shows bearing information when a missile is fired to a target.
-- * **Bearing Off**: Disable bearing information when a missile is fired to a target.
-- * **Distance**: Menu to configure the distance when a missile needs to be destroyed when near to a player, during tracking. This will improve/influence hit calculation accuracy, but has the risk of damaging the aircraft when the missile reaches the aircraft before the distance is measured.
-- * **50 meter**: Destroys the missile when the distance to the aircraft is below or equal to 50 meter.
-- * **100 meter**: Destroys the missile when the distance to the aircraft is below or equal to 100 meter.
-- * **150 meter**: Destroys the missile when the distance to the aircraft is below or equal to 150 meter.
-- * **200 meter**: Destroys the missile when the distance to the aircraft is below or equal to 200 meter.
--
--
-- 1.1) MISSILETRAINER construction methods:
-- -----------------------------------------
-- Create a new MISSILETRAINER object with the @{#MISSILETRAINER.New} method:
--
-- * @{#MISSILETRAINER.New}: Creates a new MISSILETRAINER object taking the maximum distance to your aircraft to evaluate when a missile needs to be destroyed.
--
-- MISSILETRAINER will collect each unit declared in the mission with a skill level "Client" and "Player", and will monitor the missiles shot at those.
--
-- 1.2) MISSILETRAINER initialization methods:
-- -------------------------------------------
-- A MISSILETRAINER object will behave differently based on the usage of initialization methods:
--
-- * @{#MISSILETRAINER.InitMessagesOnOff}: Sets by default the display of any message to be ON or OFF.
-- * @{#MISSILETRAINER.InitTrackingToAll}: Sets by default the missile tracking report for all players or only for those missiles targetted to you.
-- * @{#MISSILETRAINER.InitTrackingOnOff}: Sets by default the display of missile tracking report to be ON or OFF.
-- * @{#MISSILETRAINER.InitTrackingFrequency}: Increases, decreases the missile tracking message display frequency with the provided time interval in seconds.
-- * @{#MISSILETRAINER.InitAlertsToAll}: Sets by default the display of alerts to be shown to all players or only to you.
-- * @{#MISSILETRAINER.InitAlertsHitsOnOff}: Sets by default the display of hit alerts ON or OFF.
-- * @{#MISSILETRAINER.InitAlertsLaunchesOnOff}: Sets by default the display of launch alerts ON or OFF.
-- * @{#MISSILETRAINER.InitRangeOnOff}: Sets by default the display of range information of missiles ON of OFF.
-- * @{#MISSILETRAINER.InitBearingOnOff}: Sets by default the display of bearing information of missiles ON of OFF.
-- * @{#MISSILETRAINER.InitMenusOnOff}: Allows to configure the options through the radio menu.
--
-- ===
--
-- CREDITS
-- =======
-- **Stuka (Danny)** Who you can search on the Eagle Dynamics Forums.
-- Working together with Danny has resulted in the MISSILETRAINER class.
-- Danny has shared his ideas and together we made a design.
-- Together with the **476 virtual team**, we tested the MISSILETRAINER class, and got much positive feedback!
--
-- @module MissileTrainer
-- @author FlightControl
--- The MISSILETRAINER class
-- @type MISSILETRAINER
-- @field Set#SET_CLIENT DBClients
-- @extends Base#BASE
MISSILETRAINER = {
ClassName = "MISSILETRAINER",
TrackingMissiles = {},
}
function MISSILETRAINER._Alive( Client, self )
if self.Briefing then
Client:Message( self.Briefing, 15, "Trainer" )
end
if self.MenusOnOff == true then
Client:Message( "Use the 'Radio Menu' -> 'Other (F10)' -> 'Missile Trainer' menu options to change the Missile Trainer settings (for all players).", 15, "Trainer" )
Client.MainMenu = MENU_CLIENT:New( Client, "Missile Trainer", nil ) -- Menu#MENU_CLIENT
Client.MenuMessages = MENU_CLIENT:New( Client, "Messages", Client.MainMenu )
Client.MenuOn = MENU_CLIENT_COMMAND:New( Client, "Messages On", Client.MenuMessages, self._MenuMessages, { MenuSelf = self, MessagesOnOff = true } )
Client.MenuOff = MENU_CLIENT_COMMAND:New( Client, "Messages Off", Client.MenuMessages, self._MenuMessages, { MenuSelf = self, MessagesOnOff = false } )
Client.MenuTracking = MENU_CLIENT:New( Client, "Tracking", Client.MainMenu )
Client.MenuTrackingToAll = MENU_CLIENT_COMMAND:New( Client, "To All", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingToAll = true } )
Client.MenuTrackingToTarget = MENU_CLIENT_COMMAND:New( Client, "To Target", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingToAll = false } )
Client.MenuTrackOn = MENU_CLIENT_COMMAND:New( Client, "Tracking On", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingOnOff = true } )
Client.MenuTrackOff = MENU_CLIENT_COMMAND:New( Client, "Tracking Off", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingOnOff = false } )
Client.MenuTrackIncrease = MENU_CLIENT_COMMAND:New( Client, "Frequency Increase", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingFrequency = -1 } )
Client.MenuTrackDecrease = MENU_CLIENT_COMMAND:New( Client, "Frequency Decrease", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingFrequency = 1 } )
Client.MenuAlerts = MENU_CLIENT:New( Client, "Alerts", Client.MainMenu )
Client.MenuAlertsToAll = MENU_CLIENT_COMMAND:New( Client, "To All", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsToAll = true } )
Client.MenuAlertsToTarget = MENU_CLIENT_COMMAND:New( Client, "To Target", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsToAll = false } )
Client.MenuHitsOn = MENU_CLIENT_COMMAND:New( Client, "Hits On", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsHitsOnOff = true } )
Client.MenuHitsOff = MENU_CLIENT_COMMAND:New( Client, "Hits Off", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsHitsOnOff = false } )
Client.MenuLaunchesOn = MENU_CLIENT_COMMAND:New( Client, "Launches On", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsLaunchesOnOff = true } )
Client.MenuLaunchesOff = MENU_CLIENT_COMMAND:New( Client, "Launches Off", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsLaunchesOnOff = false } )
Client.MenuDetails = MENU_CLIENT:New( Client, "Details", Client.MainMenu )
Client.MenuDetailsDistanceOn = MENU_CLIENT_COMMAND:New( Client, "Range On", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsRangeOnOff = true } )
Client.MenuDetailsDistanceOff = MENU_CLIENT_COMMAND:New( Client, "Range Off", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsRangeOnOff = false } )
Client.MenuDetailsBearingOn = MENU_CLIENT_COMMAND:New( Client, "Bearing On", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsBearingOnOff = true } )
Client.MenuDetailsBearingOff = MENU_CLIENT_COMMAND:New( Client, "Bearing Off", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsBearingOnOff = false } )
Client.MenuDistance = MENU_CLIENT:New( Client, "Set distance to plane", Client.MainMenu )
Client.MenuDistance50 = MENU_CLIENT_COMMAND:New( Client, "50 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 50 / 1000 } )
Client.MenuDistance100 = MENU_CLIENT_COMMAND:New( Client, "100 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 100 / 1000 } )
Client.MenuDistance150 = MENU_CLIENT_COMMAND:New( Client, "150 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 150 / 1000 } )
Client.MenuDistance200 = MENU_CLIENT_COMMAND:New( Client, "200 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 200 / 1000 } )
else
if Client.MainMenu then
Client.MainMenu:Remove()
end
end
local ClientID = Client:GetID()
self:T( ClientID )
if not self.TrackingMissiles[ClientID] then
self.TrackingMissiles[ClientID] = {}
end
self.TrackingMissiles[ClientID].Client = Client
if not self.TrackingMissiles[ClientID].MissileData then
self.TrackingMissiles[ClientID].MissileData = {}
end
end
--- Creates the main object which is handling missile tracking.
-- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed.
-- @param #MISSILETRAINER self
-- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player.
-- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes.
-- @return #MISSILETRAINER
function MISSILETRAINER:New( Distance, Briefing )
local self = BASE:Inherit( self, BASE:New() )
self:F( Distance )
if Briefing then
self.Briefing = Briefing
end
self.Schedulers = {}
self.SchedulerID = 0
self.MessageInterval = 2
self.MessageLastTime = timer.getTime()
self.Distance = Distance / 1000
_EVENTDISPATCHER:OnShot( self._EventShot, self )
self.DBClients = SET_CLIENT:New():FilterStart()
-- for ClientID, Client in pairs( self.DBClients.Database ) do
-- self:E( "ForEach:" .. Client.UnitName )
-- Client:Alive( self._Alive, self )
-- end
--
self.DBClients:ForEachClient(
function( Client )
self:E( "ForEach:" .. Client.UnitName )
Client:Alive( self._Alive, self )
end
)
-- self.DB:ForEachClient(
-- --- @param Client#CLIENT Client
-- function( Client )
--
-- ... actions ...
--
-- end
-- )
self.MessagesOnOff = true
self.TrackingToAll = false
self.TrackingOnOff = true
self.TrackingFrequency = 3
self.AlertsToAll = true
self.AlertsHitsOnOff = true
self.AlertsLaunchesOnOff = true
self.DetailsRangeOnOff = true
self.DetailsBearingOnOff = true
self.MenusOnOff = true
self.TrackingMissiles = {}
self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 )
return self
end
-- Initialization methods.
--- Sets by default the display of any message to be ON or OFF.
-- @param #MISSILETRAINER self
-- @param #boolean MessagesOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff )
self:F( MessagesOnOff )
self.MessagesOnOff = MessagesOnOff
if self.MessagesOnOff == true then
MESSAGE:New( "Messages ON", 15, "Menu" ):ToAll()
else
MESSAGE:New( "Messages OFF", 15, "Menu" ):ToAll()
end
return self
end
--- Sets by default the missile tracking report for all players or only for those missiles targetted to you.
-- @param #MISSILETRAINER self
-- @param #boolean TrackingToAll true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitTrackingToAll( TrackingToAll )
self:F( TrackingToAll )
self.TrackingToAll = TrackingToAll
if self.TrackingToAll == true then
MESSAGE:New( "Missile tracking to all players ON", 15, "Menu" ):ToAll()
else
MESSAGE:New( "Missile tracking to all players OFF", 15, "Menu" ):ToAll()
end
return self
end
--- Sets by default the display of missile tracking report to be ON or OFF.
-- @param #MISSILETRAINER self
-- @param #boolean TrackingOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitTrackingOnOff( TrackingOnOff )
self:F( TrackingOnOff )
self.TrackingOnOff = TrackingOnOff
if self.TrackingOnOff == true then
MESSAGE:New( "Missile tracking ON", 15, "Menu" ):ToAll()
else
MESSAGE:New( "Missile tracking OFF", 15, "Menu" ):ToAll()
end
return self
end
--- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds.
-- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update.
-- @param #MISSILETRAINER self
-- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency.
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency )
self:F( TrackingFrequency )
self.TrackingFrequency = self.TrackingFrequency + TrackingFrequency
if self.TrackingFrequency < 0.5 then
self.TrackingFrequency = 0.5
end
if self.TrackingFrequency then
MESSAGE:New( "Missile tracking frequency is " .. self.TrackingFrequency .. " seconds.", 15, "Menu" ):ToAll()
end
return self
end
--- Sets by default the display of alerts to be shown to all players or only to you.
-- @param #MISSILETRAINER self
-- @param #boolean AlertsToAll true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitAlertsToAll( AlertsToAll )
self:F( AlertsToAll )
self.AlertsToAll = AlertsToAll
if self.AlertsToAll == true then
MESSAGE:New( "Alerts to all players ON", 15, "Menu" ):ToAll()
else
MESSAGE:New( "Alerts to all players OFF", 15, "Menu" ):ToAll()
end
return self
end
--- Sets by default the display of hit alerts ON or OFF.
-- @param #MISSILETRAINER self
-- @param #boolean AlertsHitsOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitAlertsHitsOnOff( AlertsHitsOnOff )
self:F( AlertsHitsOnOff )
self.AlertsHitsOnOff = AlertsHitsOnOff
if self.AlertsHitsOnOff == true then
MESSAGE:New( "Alerts Hits ON", 15, "Menu" ):ToAll()
else
MESSAGE:New( "Alerts Hits OFF", 15, "Menu" ):ToAll()
end
return self
end
--- Sets by default the display of launch alerts ON or OFF.
-- @param #MISSILETRAINER self
-- @param #boolean AlertsLaunchesOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitAlertsLaunchesOnOff( AlertsLaunchesOnOff )
self:F( AlertsLaunchesOnOff )
self.AlertsLaunchesOnOff = AlertsLaunchesOnOff
if self.AlertsLaunchesOnOff == true then
MESSAGE:New( "Alerts Launches ON", 15, "Menu" ):ToAll()
else
MESSAGE:New( "Alerts Launches OFF", 15, "Menu" ):ToAll()
end
return self
end
--- Sets by default the display of range information of missiles ON of OFF.
-- @param #MISSILETRAINER self
-- @param #boolean DetailsRangeOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitRangeOnOff( DetailsRangeOnOff )
self:F( DetailsRangeOnOff )
self.DetailsRangeOnOff = DetailsRangeOnOff
if self.DetailsRangeOnOff == true then
MESSAGE:New( "Range display ON", 15, "Menu" ):ToAll()
else
MESSAGE:New( "Range display OFF", 15, "Menu" ):ToAll()
end
return self
end
--- Sets by default the display of bearing information of missiles ON of OFF.
-- @param #MISSILETRAINER self
-- @param #boolean DetailsBearingOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitBearingOnOff( DetailsBearingOnOff )
self:F( DetailsBearingOnOff )
self.DetailsBearingOnOff = DetailsBearingOnOff
if self.DetailsBearingOnOff == true then
MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll()
else
MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll()
end
return self
end
--- Enables / Disables the menus.
-- @param #MISSILETRAINER self
-- @param #boolean MenusOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitMenusOnOff( MenusOnOff )
self:F( MenusOnOff )
self.MenusOnOff = MenusOnOff
if self.MenusOnOff == true then
MESSAGE:New( "Menus are ENABLED (only when a player rejoins a slot)", 15, "Menu" ):ToAll()
else
MESSAGE:New( "Menus are DISABLED", 15, "Menu" ):ToAll()
end
return self
end
-- Menu functions
function MISSILETRAINER._MenuMessages( MenuParameters )
local self = MenuParameters.MenuSelf
if MenuParameters.MessagesOnOff ~= nil then
self:InitMessagesOnOff( MenuParameters.MessagesOnOff )
end
if MenuParameters.TrackingToAll ~= nil then
self:InitTrackingToAll( MenuParameters.TrackingToAll )
end
if MenuParameters.TrackingOnOff ~= nil then
self:InitTrackingOnOff( MenuParameters.TrackingOnOff )
end
if MenuParameters.TrackingFrequency ~= nil then
self:InitTrackingFrequency( MenuParameters.TrackingFrequency )
end
if MenuParameters.AlertsToAll ~= nil then
self:InitAlertsToAll( MenuParameters.AlertsToAll )
end
if MenuParameters.AlertsHitsOnOff ~= nil then
self:InitAlertsHitsOnOff( MenuParameters.AlertsHitsOnOff )
end
if MenuParameters.AlertsLaunchesOnOff ~= nil then
self:InitAlertsLaunchesOnOff( MenuParameters.AlertsLaunchesOnOff )
end
if MenuParameters.DetailsRangeOnOff ~= nil then
self:InitRangeOnOff( MenuParameters.DetailsRangeOnOff )
end
if MenuParameters.DetailsBearingOnOff ~= nil then
self:InitBearingOnOff( MenuParameters.DetailsBearingOnOff )
end
if MenuParameters.Distance ~= nil then
self.Distance = MenuParameters.Distance
MESSAGE:New( "Hit detection distance set to " .. self.Distance .. " meters", 15, "Menu" ):ToAll()
end
end
--- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME.
-- @param #MISSILETRAINER self
-- @param Event#EVENTDATA Event
function MISSILETRAINER:_EventShot( Event )
self:F( { Event } )
local TrainerSourceDCSUnit = Event.IniDCSUnit
local TrainerSourceDCSUnitName = Event.IniDCSUnitName
local TrainerWeapon = Event.Weapon -- Identify the weapon fired
local TrainerWeaponName = Event.WeaponName -- return weapon type
self:T( "Missile Launched = " .. TrainerWeaponName )
local TrainerTargetDCSUnit = TrainerWeapon:getTarget() -- Identify target
if TrainerTargetDCSUnit then
local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit )
local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill
self:T(TrainerTargetDCSUnitName )
local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName )
if Client then
local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit )
local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit )
if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then
local Message = MESSAGE:New(
string.format( "%s launched a %s",
TrainerSourceUnit:GetTypeName(),
TrainerWeaponName
) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" )
if self.AlertsToAll then
Message:ToAll()
else
Message:ToClient( Client )
end
end
local ClientID = Client:GetID()
self:T( ClientID )
local MissileData = {}
MissileData.TrainerSourceUnit = TrainerSourceUnit
MissileData.TrainerWeapon = TrainerWeapon
MissileData.TrainerTargetUnit = TrainerTargetUnit
MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName()
MissileData.TrainerWeaponLaunched = true
table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData )
--self:T( self.TrackingMissiles )
end
else
-- TODO: some weapons don't know the target unit... Need to develop a workaround for this.
SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 2 )
end
end
function MISSILETRAINER:_AddRange( Client, TrainerWeapon )
local RangeText = ""
if self.DetailsRangeOnOff then
local PositionMissile = TrainerWeapon:getPoint()
local TargetVec3 = Client:GetVec3()
local Range = ( ( PositionMissile.x - TargetVec3.x )^2 +
( PositionMissile.y - TargetVec3.y )^2 +
( PositionMissile.z - TargetVec3.z )^2
) ^ 0.5 / 1000
RangeText = string.format( ", at %4.2fkm", Range )
end
return RangeText
end
function MISSILETRAINER:_AddBearing( Client, TrainerWeapon )
local BearingText = ""
if self.DetailsBearingOnOff then
local PositionMissile = TrainerWeapon:getPoint()
local TargetVec3 = Client:GetVec3()
self:T2( { TargetVec3, PositionMissile })
local DirectionVector = { x = PositionMissile.x - TargetVec3.x, y = PositionMissile.y - TargetVec3.y, z = PositionMissile.z - TargetVec3.z }
local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x )
--DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget )
if DirectionRadians < 0 then
DirectionRadians = DirectionRadians + 2 * math.pi
end
local DirectionDegrees = DirectionRadians * 180 / math.pi
BearingText = string.format( ", %d degrees", DirectionDegrees )
end
return BearingText
end
function MISSILETRAINER:_TrackMissiles()
self:F2()
local ShowMessages = false
if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then
self.MessageLastTime = timer.getTime()
ShowMessages = true
end
-- ALERTS PART
-- Loop for all Player Clients to check the alerts and deletion of missiles.
for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do
local Client = ClientData.Client
self:T2( { Client:GetName() } )
for MissileDataID, MissileData in pairs( ClientData.MissileData ) do
self:T3( MissileDataID )
local TrainerSourceUnit = MissileData.TrainerSourceUnit
local TrainerWeapon = MissileData.TrainerWeapon
local TrainerTargetUnit = MissileData.TrainerTargetUnit
local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName
local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched
if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then
local PositionMissile = TrainerWeapon:getPosition().p
local TargetVec3 = Client:GetVec3()
local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 +
( PositionMissile.y - TargetVec3.y )^2 +
( PositionMissile.z - TargetVec3.z )^2
) ^ 0.5 / 1000
if Distance <= self.Distance then
-- Hit alert
TrainerWeapon:destroy()
if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then
self:T( "killed" )
local Message = MESSAGE:New(
string.format( "%s launched by %s killed %s",
TrainerWeapon:getTypeName(),
TrainerSourceUnit:GetTypeName(),
TrainerTargetUnit:GetPlayerName()
), 15, "Hit Alert" )
if self.AlertsToAll == true then
Message:ToAll()
else
Message:ToClient( Client )
end
MissileData = nil
table.remove( ClientData.MissileData, MissileDataID )
self:T(ClientData.MissileData)
end
end
else
if not ( TrainerWeapon and TrainerWeapon:isExist() ) then
if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then
-- Weapon does not exist anymore. Delete from Table
local Message = MESSAGE:New(
string.format( "%s launched by %s self destructed!",
TrainerWeaponTypeName,
TrainerSourceUnit:GetTypeName()
), 5, "Tracking" )
if self.AlertsToAll == true then
Message:ToAll()
else
Message:ToClient( Client )
end
end
MissileData = nil
table.remove( ClientData.MissileData, MissileDataID )
self:T( ClientData.MissileData )
end
end
end
end
if ShowMessages == true and self.MessagesOnOff == true and self.TrackingOnOff == true then -- Only do this when tracking information needs to be displayed.
-- TRACKING PART
-- For the current client, the missile range and bearing details are displayed To the Player Client.
-- For the other clients, the missile range and bearing details are displayed To the other Player Clients.
-- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information.
-- Main Player Client loop
for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do
local Client = ClientData.Client
self:T2( { Client:GetName() } )
ClientData.MessageToClient = ""
ClientData.MessageToAll = ""
-- Other Players Client loop
for TrackingDataID, TrackingData in pairs( self.TrackingMissiles ) do
for MissileDataID, MissileData in pairs( TrackingData.MissileData ) do
self:T3( MissileDataID )
local TrainerSourceUnit = MissileData.TrainerSourceUnit
local TrainerWeapon = MissileData.TrainerWeapon
local TrainerTargetUnit = MissileData.TrainerTargetUnit
local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName
local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched
if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then
if ShowMessages == true then
local TrackingTo
TrackingTo = string.format( " -> %s",
TrainerWeaponTypeName
)
if ClientDataID == TrackingDataID then
if ClientData.MessageToClient == "" then
ClientData.MessageToClient = "Missiles to You:\n"
end
ClientData.MessageToClient = ClientData.MessageToClient .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. "\n"
else
if self.TrackingToAll == true then
if ClientData.MessageToAll == "" then
ClientData.MessageToAll = "Missiles to other Players:\n"
end
ClientData.MessageToAll = ClientData.MessageToAll .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. " ( " .. TrainerTargetUnit:GetPlayerName() .. " )\n"
end
end
end
end
end
end
-- Once the Player Client and the Other Player Client tracking messages are prepared, show them.
if ClientData.MessageToClient ~= "" or ClientData.MessageToAll ~= "" then
local Message = MESSAGE:New( ClientData.MessageToClient .. ClientData.MessageToAll, 1, "Tracking" ):ToClient( Client )
end
end
end
return true
end

View File

@@ -0,0 +1,128 @@
--- Limit the simultaneous movement of Groups within a running Mission.
-- This module is defined to improve the performance in missions, and to bring additional realism for GROUND vehicles.
-- Performance: If in a DCSRTE there are a lot of moving GROUND units, then in a multi player mission, this WILL create lag if
-- the main DCS execution core of your CPU is fully utilized. So, this class will limit the amount of simultaneous moving GROUND units
-- on defined intervals (currently every minute).
-- @module MOVEMENT
--- the MOVEMENT class
-- @type
MOVEMENT = {
ClassName = "MOVEMENT",
}
--- Creates the main object which is handling the GROUND forces movement.
-- @param table{string,...}|string MovePrefixes is a table of the Prefixes (names) of the GROUND Groups that need to be controlled by the MOVEMENT Object.
-- @param number MoveMaximum is a number that defines the maximum amount of GROUND Units to be moving during one minute.
-- @return MOVEMENT
-- @usage
-- -- Limit the amount of simultaneous moving units on the ground to prevent lag.
-- Movement_US_Platoons = MOVEMENT:New( { 'US Tank Platoon Left', 'US Tank Platoon Middle', 'US Tank Platoon Right', 'US CH-47D Troops' }, 15 )
function MOVEMENT:New( MovePrefixes, MoveMaximum )
local self = BASE:Inherit( self, BASE:New() )
self:F( { MovePrefixes, MoveMaximum } )
if type( MovePrefixes ) == 'table' then
self.MovePrefixes = MovePrefixes
else
self.MovePrefixes = { MovePrefixes }
end
self.MoveCount = 0 -- The internal counter of the amount of Moveing the has happened since MoveStart.
self.MoveMaximum = MoveMaximum -- Contains the Maximum amount of units that are allowed to move...
self.AliveUnits = 0 -- Contains the counter how many units are currently alive
self.MoveUnits = {} -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not.
_EVENTDISPATCHER:OnBirth( self.OnBirth, self )
-- self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth )
--
-- self:EnableEvents()
self:ScheduleStart()
return self
end
--- Call this function to start the MOVEMENT scheduling.
function MOVEMENT:ScheduleStart()
self:F()
--self.MoveFunction = routines.scheduleFunction( self._Scheduler, { self }, timer.getTime() + 1, 120 )
self.MoveFunction = SCHEDULER:New( self, self._Scheduler, {}, 1, 120 )
end
--- Call this function to stop the MOVEMENT scheduling.
-- @todo need to implement it ... Forgot.
function MOVEMENT:ScheduleStop()
self:F()
end
--- Captures the birth events when new Units were spawned.
-- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration.
function MOVEMENT:OnBirth( Event )
self:F( { Event } )
if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line
if Event.IniDCSUnit then
self:T( "Birth object : " .. Event.IniDCSUnitName )
if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then
for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do
if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then
self.AliveUnits = self.AliveUnits + 1
self.MoveUnits[Event.IniDCSUnitName] = Event.IniDCSGroupName
self:T( self.AliveUnits )
end
end
end
end
_EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self )
_EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self )
end
end
--- Captures the Dead or Crash events when Units crash or are destroyed.
-- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration.
function MOVEMENT:OnDeadOrCrash( Event )
self:F( { Event } )
if Event.IniDCSUnit then
self:T( "Dead object : " .. Event.IniDCSUnitName )
for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do
if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then
self.AliveUnits = self.AliveUnits - 1
self.MoveUnits[Event.IniDCSUnitName] = nil
self:T( self.AliveUnits )
end
end
end
end
--- This function is called automatically by the MOVEMENT scheduler. A new function is scheduled when MoveScheduled is true.
function MOVEMENT:_Scheduler()
self:F( { self.MovePrefixes, self.MoveMaximum, self.AliveUnits, self.MovementGroups } )
if self.AliveUnits > 0 then
local MoveProbability = ( self.MoveMaximum * 100 ) / self.AliveUnits
self:T( 'Move Probability = ' .. MoveProbability )
for MovementUnitName, MovementGroupName in pairs( self.MoveUnits ) do
local MovementGroup = Group.getByName( MovementGroupName )
if MovementGroup and MovementGroup:isExist() then
local MoveOrStop = math.random( 1, 100 )
self:T( 'MoveOrStop = ' .. MoveOrStop )
if MoveOrStop <= MoveProbability then
self:T( 'Group continues moving = ' .. MovementGroupName )
trigger.action.groupContinueMoving( MovementGroup )
else
self:T( 'Group stops moving = ' .. MovementGroupName )
trigger.action.groupStopMoving( MovementGroup )
end
else
self.MoveUnits[MovementUnitName] = nil
end
end
end
return true
end

View File

@@ -0,0 +1,821 @@
--- Scoring system for MOOSE.
-- This scoring class calculates the hits and kills that players make within a simulation session.
-- Scoring is calculated using a defined algorithm.
-- With a small change in MissionScripting.lua, the scoring can also be logged in a CSV file, that can then be uploaded
-- to a database or a BI tool to publish the scoring results to the player community.
-- @module Scoring
-- @author FlightControl
--- The Scoring class
-- @type SCORING
-- @field Players A collection of the current players that have joined the game.
-- @extends Base#BASE
SCORING = {
ClassName = "SCORING",
ClassID = 0,
Players = {},
}
local _SCORINGCoalition =
{
[1] = "Red",
[2] = "Blue",
}
local _SCORINGCategory =
{
[Unit.Category.AIRPLANE] = "Plane",
[Unit.Category.HELICOPTER] = "Helicopter",
[Unit.Category.GROUND_UNIT] = "Vehicle",
[Unit.Category.SHIP] = "Ship",
[Unit.Category.STRUCTURE] = "Structure",
}
--- Creates a new SCORING object to administer the scoring achieved by players.
-- @param #SCORING self
-- @param #string GameName The name of the game. This name is also logged in the CSV score file.
-- @return #SCORING self
-- @usage
-- -- Define a new scoring object for the mission Gori Valley.
-- ScoringObject = SCORING:New( "Gori Valley" )
function SCORING:New( GameName )
-- Inherits from BASE
local self = BASE:Inherit( self, BASE:New() )
if GameName then
self.GameName = GameName
else
error( "A game name must be given to register the scoring results" )
end
_EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self )
_EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self )
_EVENTDISPATCHER:OnHit( self._EventOnHit, self )
--self.SchedulerId = routines.scheduleFunction( SCORING._FollowPlayersScheduled, { self }, 0, 5 )
self.SchedulerId = SCHEDULER:New( self, self._FollowPlayersScheduled, {}, 0, 5 )
self:ScoreMenu()
self:OpenCSV( GameName)
return self
end
--- Creates a score radio menu. Can be accessed using Radio -> F10.
-- @param #SCORING self
-- @return #SCORING self
function SCORING:ScoreMenu()
self.Menu = MENU_MISSION:New( 'Scoring' )
self.AllScoresMenu = MENU_MISSION_COMMAND:New( 'Score All Active Players', self.Menu, SCORING.ReportScoreAll, self )
--- = COMMANDMENU:New('Your Current Score', ReportScore, SCORING.ReportScorePlayer, self )
return self
end
--- Follows new players entering Clients within the DCSRTE.
-- TODO: Need to see if i can catch this also with an event. It will eliminate the schedule ...
function SCORING:_FollowPlayersScheduled()
self:F3( "_FollowPlayersScheduled" )
local ClientUnit = 0
local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) }
local unitId
local unitData
local AlivePlayerUnits = {}
for CoalitionId, CoalitionData in pairs( CoalitionsData ) do
self:T3( { "_FollowPlayersScheduled", CoalitionData } )
for UnitId, UnitData in pairs( CoalitionData ) do
self:_AddPlayerFromUnit( UnitData )
end
end
return true
end
--- Track DEAD or CRASH events for the scoring.
-- @param #SCORING self
-- @param Event#EVENTDATA Event
function SCORING:_EventOnDeadOrCrash( Event )
self:F( { Event } )
local TargetUnit = nil
local TargetGroup = nil
local TargetUnitName = ""
local TargetGroupName = ""
local TargetPlayerName = ""
local TargetCoalition = nil
local TargetCategory = nil
local TargetType = nil
local TargetUnitCoalition = nil
local TargetUnitCategory = nil
local TargetUnitType = nil
if Event.IniDCSUnit then
TargetUnit = Event.IniDCSUnit
TargetUnitName = Event.IniDCSUnitName
TargetGroup = Event.IniDCSGroup
TargetGroupName = Event.IniDCSGroupName
TargetPlayerName = TargetUnit:getPlayerName()
TargetCoalition = TargetUnit:getCoalition()
--TargetCategory = TargetUnit:getCategory()
TargetCategory = TargetUnit:getDesc().category -- Workaround
TargetType = TargetUnit:getTypeName()
TargetUnitCoalition = _SCORINGCoalition[TargetCoalition]
TargetUnitCategory = _SCORINGCategory[TargetCategory]
TargetUnitType = TargetType
self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } )
end
for PlayerName, PlayerData in pairs( self.Players ) do
if PlayerData then -- This should normally not happen, but i'll test it anyway.
self:T( "Something got killed" )
-- Some variables
local InitUnitName = PlayerData.UnitName
local InitUnitType = PlayerData.UnitType
local InitCoalition = PlayerData.UnitCoalition
local InitCategory = PlayerData.UnitCategory
local InitUnitCoalition = _SCORINGCoalition[InitCoalition]
local InitUnitCategory = _SCORINGCategory[InitCategory]
self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } )
-- What is he hitting?
if TargetCategory then
if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered???
if not PlayerData.Kill[TargetCategory] then
PlayerData.Kill[TargetCategory] = {}
end
if not PlayerData.Kill[TargetCategory][TargetType] then
PlayerData.Kill[TargetCategory][TargetType] = {}
PlayerData.Kill[TargetCategory][TargetType].Score = 0
PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0
PlayerData.Kill[TargetCategory][TargetType].Penalty = 0
PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0
end
if InitCoalition == TargetCoalition then
PlayerData.Penalty = PlayerData.Penalty + 25
PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25
PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1
MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " ..
PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty ..
". Score Total:" .. PlayerData.Score - PlayerData.Penalty,
5 ):ToAll()
self:ScoreCSV( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
else
PlayerData.Score = PlayerData.Score + 10
PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10
PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1
MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " ..
PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score ..
". Score Total:" .. PlayerData.Score - PlayerData.Penalty,
5 ):ToAll()
self:ScoreCSV( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
end
end
end
end
end
end
--- Add a new player entering a Unit.
function SCORING:_AddPlayerFromUnit( UnitData )
self:F( UnitData )
if UnitData and UnitData:isExist() then
local UnitName = UnitData:getName()
local PlayerName = UnitData:getPlayerName()
local UnitDesc = UnitData:getDesc()
local UnitCategory = UnitDesc.category
local UnitCoalition = UnitData:getCoalition()
local UnitTypeName = UnitData:getTypeName()
self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } )
if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ...
self.Players[PlayerName] = {}
self.Players[PlayerName].Hit = {}
self.Players[PlayerName].Kill = {}
self.Players[PlayerName].Mission = {}
-- for CategoryID, CategoryName in pairs( SCORINGCategory ) do
-- self.Players[PlayerName].Hit[CategoryID] = {}
-- self.Players[PlayerName].Kill[CategoryID] = {}
-- end
self.Players[PlayerName].HitPlayers = {}
self.Players[PlayerName].HitUnits = {}
self.Players[PlayerName].Score = 0
self.Players[PlayerName].Penalty = 0
self.Players[PlayerName].PenaltyCoalition = 0
self.Players[PlayerName].PenaltyWarning = 0
end
if not self.Players[PlayerName].UnitCoalition then
self.Players[PlayerName].UnitCoalition = UnitCoalition
else
if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then
self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50
self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1
MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] ..
"(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.",
2
):ToAll()
self:ScoreCSV( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType,
UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:getTypeName() )
end
end
self.Players[PlayerName].UnitName = UnitName
self.Players[PlayerName].UnitCoalition = UnitCoalition
self.Players[PlayerName].UnitCategory = UnitCategory
self.Players[PlayerName].UnitType = UnitTypeName
if self.Players[PlayerName].Penalty > 100 then
if self.Players[PlayerName].PenaltyWarning < 1 then
MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty,
30
):ToAll()
self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1
end
end
if self.Players[PlayerName].Penalty > 150 then
ClientGroup = GROUP:NewFromDCSUnit( UnitData )
ClientGroup:Destroy()
MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!",
10
):ToAll()
end
end
end
--- Registers Scores the players completing a Mission Task.
-- @param #SCORING self
-- @param Mission#MISSION Mission
-- @param Unit#UNIT PlayerUnit
-- @param #string Text
-- @param #number Score
function SCORING:_AddMissionTaskScore( Mission, PlayerUnit, Text, Score )
local PlayerName = PlayerUnit:GetPlayerName()
local MissionName = Mission:GetName()
self:F( { Mission:GetName(), PlayerUnit.UnitName, PlayerName, Text, Score } )
if not self.Players[PlayerName].Mission[MissionName] then
self.Players[PlayerName].Mission[MissionName] = {}
self.Players[PlayerName].Mission[MissionName].ScoreTask = 0
self.Players[PlayerName].Mission[MissionName].ScoreMission = 0
end
self:T( PlayerName )
self:T( self.Players[PlayerName].Mission[MissionName] )
self.Players[PlayerName].Score = self.Players[PlayerName].Score + Score
self.Players[PlayerName].Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score
MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " ..
Score .. " task score!",
30 ):ToAll()
self:ScoreCSV( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:GetName() )
end
--- Registers Mission Scores for possible multiple players that contributed in the Mission.
-- @param #SCORING self
-- @param Mission#MISSION Mission
-- @param Unit#UNIT PlayerUnit
-- @param #string Text
-- @param #number Score
function SCORING:_AddMissionScore( Mission, Text, Score )
local MissionName = Mission:GetName()
self:F( { Mission, Text, Score } )
for PlayerName, PlayerData in pairs( self.Players ) do
if PlayerData.Mission[MissionName] then
PlayerData.Score = PlayerData.Score + Score
PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score
MESSAGE:New( "Player '" .. PlayerName .. "' has " .. Text .. " in Mission '" .. MissionName .. "'. " ..
Score .. " mission score!",
60 ):ToAll()
self:ScoreCSV( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score )
end
end
end
--- Handles the OnHit event for the scoring.
-- @param #SCORING self
-- @param Event#EVENTDATA Event
function SCORING:_EventOnHit( Event )
self:F( { Event } )
local InitUnit = nil
local InitUnitName = ""
local InitGroup = nil
local InitGroupName = ""
local InitPlayerName = nil
local InitCoalition = nil
local InitCategory = nil
local InitType = nil
local InitUnitCoalition = nil
local InitUnitCategory = nil
local InitUnitType = nil
local TargetUnit = nil
local TargetUnitName = ""
local TargetGroup = nil
local TargetGroupName = ""
local TargetPlayerName = ""
local TargetCoalition = nil
local TargetCategory = nil
local TargetType = nil
local TargetUnitCoalition = nil
local TargetUnitCategory = nil
local TargetUnitType = nil
if Event.IniDCSUnit then
InitUnit = Event.IniDCSUnit
InitUnitName = Event.IniDCSUnitName
InitGroup = Event.IniDCSGroup
InitGroupName = Event.IniDCSGroupName
InitPlayerName = InitUnit:getPlayerName()
InitCoalition = InitUnit:getCoalition()
--TODO: Workaround Client DCS Bug
--InitCategory = InitUnit:getCategory()
InitCategory = InitUnit:getDesc().category
InitType = InitUnit:getTypeName()
InitUnitCoalition = _SCORINGCoalition[InitCoalition]
InitUnitCategory = _SCORINGCategory[InitCategory]
InitUnitType = InitType
self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } )
end
if Event.TgtDCSUnit then
TargetUnit = Event.TgtDCSUnit
TargetUnitName = Event.TgtDCSUnitName
TargetGroup = Event.TgtDCSGroup
TargetGroupName = Event.TgtDCSGroupName
TargetPlayerName = TargetUnit:getPlayerName()
TargetCoalition = TargetUnit:getCoalition()
--TODO: Workaround Client DCS Bug
--TargetCategory = TargetUnit:getCategory()
TargetCategory = TargetUnit:getDesc().category
TargetType = TargetUnit:getTypeName()
TargetUnitCoalition = _SCORINGCoalition[TargetCoalition]
TargetUnitCategory = _SCORINGCategory[TargetCategory]
TargetUnitType = TargetType
self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } )
end
if InitPlayerName ~= nil then -- It is a player that is hitting something
self:_AddPlayerFromUnit( InitUnit )
if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway.
if TargetPlayerName ~= nil then -- It is a player hitting another player ...
self:_AddPlayerFromUnit( TargetUnit )
self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1
end
self:T( "Hitting Something" )
-- What is he hitting?
if TargetCategory then
if not self.Players[InitPlayerName].Hit[TargetCategory] then
self.Players[InitPlayerName].Hit[TargetCategory] = {}
end
if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then
self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {}
self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0
self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0
self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0
self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0
end
local Score = 0
if InitCoalition == TargetCoalition then
self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10
self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10
self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1
MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " ..
self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty ..
". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty,
2
):ToAll()
self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
else
self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 10
self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1
self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1
MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " ..
self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score ..
". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty,
2
):ToAll()
self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
end
end
end
elseif InitPlayerName == nil then -- It is an AI hitting a player???
end
end
function SCORING:ReportScoreAll()
env.info( "Hello World " )
local ScoreMessage = ""
local PlayerMessage = ""
self:T( "Score Report" )
for PlayerName, PlayerData in pairs( self.Players ) do
if PlayerData then -- This should normally not happen, but i'll test it anyway.
self:T( "Score Player: " .. PlayerName )
-- Some variables
local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition]
local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory]
local InitUnitType = PlayerData.UnitType
local InitUnitName = PlayerData.UnitName
local PlayerScore = 0
local PlayerPenalty = 0
ScoreMessage = ":\n"
local ScoreMessageHits = ""
for CategoryID, CategoryName in pairs( _SCORINGCategory ) do
self:T( CategoryName )
if PlayerData.Hit[CategoryID] then
local Score = 0
local ScoreHit = 0
local Penalty = 0
local PenaltyHit = 0
self:T( "Hit scores exist for player " .. PlayerName )
for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do
Score = Score + UnitData.Score
ScoreHit = ScoreHit + UnitData.ScoreHit
Penalty = Penalty + UnitData.Penalty
PenaltyHit = UnitData.PenaltyHit
end
local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty )
self:T( ScoreMessageHit )
ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit
PlayerScore = PlayerScore + Score
PlayerPenalty = PlayerPenalty + Penalty
else
--ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 )
end
end
if ScoreMessageHits ~= "" then
ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n"
end
local ScoreMessageKills = ""
for CategoryID, CategoryName in pairs( _SCORINGCategory ) do
self:T( "Kill scores exist for player " .. PlayerName )
if PlayerData.Kill[CategoryID] then
local Score = 0
local ScoreKill = 0
local Penalty = 0
local PenaltyKill = 0
for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do
Score = Score + UnitData.Score
ScoreKill = ScoreKill + UnitData.ScoreKill
Penalty = Penalty + UnitData.Penalty
PenaltyKill = PenaltyKill + UnitData.PenaltyKill
end
local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty )
self:T( ScoreMessageKill )
ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill
PlayerScore = PlayerScore + Score
PlayerPenalty = PlayerPenalty + Penalty
else
--ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 )
end
end
if ScoreMessageKills ~= "" then
ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n"
end
local ScoreMessageCoalitionChangePenalties = ""
if PlayerData.PenaltyCoalition ~= 0 then
ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition )
PlayerPenalty = PlayerPenalty + PlayerData.Penalty
end
if ScoreMessageCoalitionChangePenalties ~= "" then
ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n"
end
local ScoreMessageMission = ""
local ScoreMission = 0
local ScoreTask = 0
for MissionName, MissionData in pairs( PlayerData.Mission ) do
ScoreMission = ScoreMission + MissionData.ScoreMission
ScoreTask = ScoreTask + MissionData.ScoreTask
ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; "
end
PlayerScore = PlayerScore + ScoreMission + ScoreTask
if ScoreMessageMission ~= "" then
ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n"
end
PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage )
end
end
MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll()
end
function SCORING:ReportScorePlayer()
env.info( "Hello World " )
local ScoreMessage = ""
local PlayerMessage = ""
self:T( "Score Report" )
for PlayerName, PlayerData in pairs( self.Players ) do
if PlayerData then -- This should normally not happen, but i'll test it anyway.
self:T( "Score Player: " .. PlayerName )
-- Some variables
local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition]
local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory]
local InitUnitType = PlayerData.UnitType
local InitUnitName = PlayerData.UnitName
local PlayerScore = 0
local PlayerPenalty = 0
ScoreMessage = ""
local ScoreMessageHits = ""
for CategoryID, CategoryName in pairs( _SCORINGCategory ) do
self:T( CategoryName )
if PlayerData.Hit[CategoryID] then
local Score = 0
local ScoreHit = 0
local Penalty = 0
local PenaltyHit = 0
self:T( "Hit scores exist for player " .. PlayerName )
for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do
Score = Score + UnitData.Score
ScoreHit = ScoreHit + UnitData.ScoreHit
Penalty = Penalty + UnitData.Penalty
PenaltyHit = UnitData.PenaltyHit
end
local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit )
self:T( ScoreMessageHit )
ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit
PlayerScore = PlayerScore + Score
PlayerPenalty = PlayerPenalty + Penalty
else
--ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 )
end
end
if ScoreMessageHits ~= "" then
ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " "
end
local ScoreMessageKills = ""
for CategoryID, CategoryName in pairs( _SCORINGCategory ) do
self:T( "Kill scores exist for player " .. PlayerName )
if PlayerData.Kill[CategoryID] then
local Score = 0
local ScoreKill = 0
local Penalty = 0
local PenaltyKill = 0
for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do
Score = Score + UnitData.Score
ScoreKill = ScoreKill + UnitData.ScoreKill
Penalty = Penalty + UnitData.Penalty
PenaltyKill = PenaltyKill + UnitData.PenaltyKill
end
local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill )
self:T( ScoreMessageKill )
ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill
PlayerScore = PlayerScore + Score
PlayerPenalty = PlayerPenalty + Penalty
else
--ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 )
end
end
if ScoreMessageKills ~= "" then
ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " "
end
local ScoreMessageCoalitionChangePenalties = ""
if PlayerData.PenaltyCoalition ~= 0 then
ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition )
PlayerPenalty = PlayerPenalty + PlayerData.Penalty
end
if ScoreMessageCoalitionChangePenalties ~= "" then
ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " "
end
local ScoreMessageMission = ""
local ScoreMission = 0
local ScoreTask = 0
for MissionName, MissionData in pairs( PlayerData.Mission ) do
ScoreMission = ScoreMission + MissionData.ScoreMission
ScoreTask = ScoreTask + MissionData.ScoreTask
ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; "
end
PlayerScore = PlayerScore + ScoreMission + ScoreTask
if ScoreMessageMission ~= "" then
ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") "
end
PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage )
end
end
MESSAGE:New( PlayerMessage, 30, "Player Scores" ):ToAll()
end
function SCORING:SecondsToClock(sSeconds)
local nSeconds = sSeconds
if nSeconds == 0 then
--return nil;
return "00:00:00";
else
nHours = string.format("%02.f", math.floor(nSeconds/3600));
nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60)));
nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60));
return nHours..":"..nMins..":"..nSecs
end
end
--- Opens a score CSV file to log the scores.
-- @param #SCORING self
-- @param #string ScoringCSV
-- @return #SCORING self
-- @usage
-- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores".
-- ScoringObject = SCORING:New( "Gori Valley" )
-- ScoringObject:OpenCSV( "Player Scores" )
function SCORING:OpenCSV( ScoringCSV )
self:F( ScoringCSV )
if lfs and io and os then
if ScoringCSV then
self.ScoringCSV = ScoringCSV
local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv"
self.CSVFile, self.err = io.open( fdir, "w+" )
if not self.CSVFile then
error( "Error: Cannot open CSV file in " .. lfs.writedir() )
end
self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' )
self.RunTime = os.date("%y-%m-%d_%H-%M-%S")
else
error( "A string containing the CSV file name must be given." )
end
else
self:E( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." )
end
return self
end
--- Registers a score for a player.
-- @param #SCORING self
-- @param #string PlayerName The name of the player.
-- @param #string ScoreType The type of the score.
-- @param #string ScoreTimes The amount of scores achieved.
-- @param #string ScoreAmount The score given.
-- @param #string PlayerUnitName The unit name of the player.
-- @param #string PlayerUnitCoalition The coalition of the player unit.
-- @param #string PlayerUnitCategory The category of the player unit.
-- @param #string PlayerUnitType The type of the player unit.
-- @param #string TargetUnitName The name of the target unit.
-- @param #string TargetUnitCoalition The coalition of the target unit.
-- @param #string TargetUnitCategory The category of the target unit.
-- @param #string TargetUnitType The type of the target unit.
-- @return #SCORING self
function SCORING:ScoreCSV( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
--write statistic information to file
local ScoreTime = self:SecondsToClock( timer.getTime() )
PlayerName = PlayerName:gsub( '"', '_' )
if PlayerUnitName and PlayerUnitName ~= '' then
local PlayerUnit = Unit.getByName( PlayerUnitName )
if PlayerUnit then
if not PlayerUnitCategory then
--PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()]
PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category]
end
if not PlayerUnitCoalition then
PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()]
end
if not PlayerUnitType then
PlayerUnitType = PlayerUnit:getTypeName()
end
else
PlayerUnitName = ''
PlayerUnitCategory = ''
PlayerUnitCoalition = ''
PlayerUnitType = ''
end
else
PlayerUnitName = ''
PlayerUnitCategory = ''
PlayerUnitCoalition = ''
PlayerUnitType = ''
end
if not TargetUnitCoalition then
TargetUnitCoalition = ''
end
if not TargetUnitCategory then
TargetUnitCategory = ''
end
if not TargetUnitType then
TargetUnitType = ''
end
if not TargetUnitName then
TargetUnitName = ''
end
if lfs and io and os then
self.CSVFile:write(
'"' .. self.GameName .. '"' .. ',' ..
'"' .. self.RunTime .. '"' .. ',' ..
'' .. ScoreTime .. '' .. ',' ..
'"' .. PlayerName .. '"' .. ',' ..
'"' .. ScoreType .. '"' .. ',' ..
'"' .. PlayerUnitCoalition .. '"' .. ',' ..
'"' .. PlayerUnitCategory .. '"' .. ',' ..
'"' .. PlayerUnitType .. '"' .. ',' ..
'"' .. PlayerUnitName .. '"' .. ',' ..
'"' .. TargetUnitCoalition .. '"' .. ',' ..
'"' .. TargetUnitCategory .. '"' .. ',' ..
'"' .. TargetUnitType .. '"' .. ',' ..
'"' .. TargetUnitName .. '"' .. ',' ..
'' .. ScoreTimes .. '' .. ',' ..
'' .. ScoreAmount
)
self.CSVFile:write( "\n" )
end
end
function SCORING:CloseCSV()
if lfs and io and os then
self.CSVFile:close()
end
end

View File

@@ -0,0 +1,129 @@
--- Provides defensive behaviour to a set of SAM sites within a running Mission.
-- @module Sead
-- @author to be searched on the forum
-- @author (co) Flightcontrol (Modified and enriched with functionality)
--- The SEAD class
-- @type SEAD
-- @extends Base#BASE
SEAD = {
ClassName = "SEAD",
TargetSkill = {
Average = { Evade = 50, DelayOff = { 10, 25 }, DelayOn = { 10, 30 } } ,
Good = { Evade = 30, DelayOff = { 8, 20 }, DelayOn = { 20, 40 } } ,
High = { Evade = 15, DelayOff = { 5, 17 }, DelayOn = { 30, 50 } } ,
Excellent = { Evade = 10, DelayOff = { 3, 10 }, DelayOn = { 30, 60 } }
},
SEADGroupPrefixes = {}
}
--- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles.
-- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions...
-- Chances are big that the missile will miss.
-- @param table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCSRTE on which evasive actions need to be taken.
-- @return SEAD
-- @usage
-- -- CCCP SEAD Defenses
-- -- Defends the Russian SA installations from SEAD attacks.
-- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } )
function SEAD:New( SEADGroupPrefixes )
local self = BASE:Inherit( self, BASE:New() )
self:F( SEADGroupPrefixes )
if type( SEADGroupPrefixes ) == 'table' then
for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do
self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix
end
else
self.SEADGroupNames[SEADGroupPrefixes] = SEADGroupPrefixes
end
_EVENTDISPATCHER:OnShot( self.EventShot, self )
return self
end
--- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME.
-- @see SEAD
function SEAD:EventShot( Event )
self:F( { Event } )
local SEADUnit = Event.IniDCSUnit
local SEADUnitName = Event.IniDCSUnitName
local SEADWeapon = Event.Weapon -- Identify the weapon fired
local SEADWeaponName = Event.WeaponName -- return weapon type
-- Start of the 2nd loop
self:T( "Missile Launched = " .. SEADWeaponName )
if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD
local _evade = math.random (1,100) -- random number for chance of evading action
local _targetMim = Event.Weapon:getTarget() -- Identify target
local _targetMimname = Unit.getName(_targetMim)
local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon))
local _targetMimgroupName = _targetMimgroup:getName()
local _targetMimcont= _targetMimgroup:getController()
local _targetskill = _DATABASE.Templates.Units[_targetMimname].Template.skill
self:T( self.SEADGroupPrefixes )
self:T( _targetMimgroupName )
local SEADGroupFound = false
for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do
if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then
SEADGroupFound = true
self:T( 'Group Found' )
break
end
end
if SEADGroupFound == true then
if _targetskill == "Random" then -- when skill is random, choose a skill
local Skills = { "Average", "Good", "High", "Excellent" }
_targetskill = Skills[ math.random(1,4) ]
end
self:T( _targetskill )
if self.TargetSkill[_targetskill] then
if (_evade > self.TargetSkill[_targetskill].Evade) then
self:T( string.format("Evading, target skill " ..string.format(_targetskill)) )
local _targetMim = Weapon.getTarget(SEADWeapon)
local _targetMimname = Unit.getName(_targetMim)
local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon))
local _targetMimcont= _targetMimgroup:getController()
routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly
local SuppressedGroups1 = {} -- unit suppressed radar off for a random time
local function SuppressionEnd1(id)
id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN)
SuppressedGroups1[id.groupName] = nil
end
local id = {
groupName = _targetMimgroup,
ctrl = _targetMimcont
}
local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2])
if SuppressedGroups1[id.groupName] == nil then
SuppressedGroups1[id.groupName] = {
SuppressionEndTime1 = timer.getTime() + delay1,
SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function
}
Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN)
timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function
--trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20)
end
local SuppressedGroups = {}
local function SuppressionEnd(id)
id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED)
SuppressedGroups[id.groupName] = nil
end
local id = {
groupName = _targetMimgroup,
ctrl = _targetMimcont
}
local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2])
if SuppressedGroups[id.groupName] == nil then
SuppressedGroups[id.groupName] = {
SuppressionEndTime = timer.getTime() + delay,
SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function
}
timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function
--trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20)
end
end
end
end
end
end

File diff suppressed because it is too large Load Diff