diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 84cfec085..c270afb98 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -1390,24 +1390,37 @@ function AUFTRAG:NewALERT5(MissionType) end ---- Create a mission to attack a group. Mission type is automatically chosen from the group category. +--- Create a mission to attack a TARGET object. -- @param #AUFTRAG self -- @param Ops.Target#TARGET Target The target. +-- @param #string MissionType The mission type. -- @return #AUFTRAG self -function AUFTRAG:NewTargetAir(Target) +function AUFTRAG:NewFromTarget(Target, MissionType) local mission=nil --#AUFTRAG - self.engageTarget=Target - - local target=self.engageTarget:GetObject() - - local mission=self:NewAUTO(target) - - if mission then - mission:SetPriority(10, true) + if MissionType==AUFTRAG.Type.ANTISHIP then + mission=self:NewANTISHIP(Target, Altitude) + elseif MissionType==AUFTRAG.Type.ARTY then + mission=self:NewARTY(Target, Nshots, Radius) + elseif MissionType==AUFTRAG.Type.BAI then + mission=self:NewBAI(Target, Altitude) + elseif MissionType==AUFTRAG.Type.BOMBCARPET then + mission=self:NewBOMBCARPET(Target, Altitude, CarpetLength) + elseif MissionType==AUFTRAG.Type.BOMBING then + mission=self:NewBOMBING(Target, Altitude) + elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then + mission=self:NewBOMBRUNWAY(Target, Altitude) + elseif MissionType==AUFTRAG.Type.INTERCEPT then + mission=self:NewINTERCEPT(Target) + elseif MissionType==AUFTRAG.Type.SEAD then + mission=self:NewSEAD(Target, Altitude) + elseif MissionType==AUFTRAG.Type.STRIKE then + mission=self:NewSTRIKE(Target, Altitude) + else + return nil end - + return mission end diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua index 008e71b85..bbe3941bc 100644 --- a/Moose Development/Moose/Ops/Chief.lua +++ b/Moose Development/Moose/Ops/Chief.lua @@ -17,6 +17,7 @@ -- @field #number verbose Verbosity level. -- @field #string lid Class id string for output to DCS log file. -- @field #table targetqueue Target queue. +-- @field #table zonequeue Strategic zone queue. -- @field Core.Set#SET_ZONE borderzoneset Set of zones defining the border of our territory. -- @field Core.Set#SET_ZONE yellowzoneset Set of zones defining the extended border. Defcon is set to YELLOW if enemy activity is detected. -- @field Core.Set#SET_ZONE engagezoneset Set of zones where enemies are actively engaged. @@ -30,9 +31,7 @@ -- -- # The CHIEF Concept -- --- The Chief of staff gathers intel and assigns missions (AUFTRAG) the airforce (WINGCOMMANDER), army (GENERAL) or navy (ADMIRAL). --- --- **Note** that currently only assignments to airborne forces (WINGCOMMANDER) are implemented. +-- The Chief of staff gathers INTEL and assigns missions (AUFTRAG) the airforce, army and/or navy. -- -- -- @field #CHIEF @@ -41,6 +40,7 @@ CHIEF = { verbose = 0, lid = nil, targetqueue = {}, + zonequeue = {}, borderzoneset = nil, yellowzoneset = nil, engagezoneset = nil, @@ -70,6 +70,18 @@ CHIEF.Strategy = { TOTALWAR="Total War" } +--- Strategy. +-- @type CHIEF.MissionTypePerformance +-- @field #string MissionType Mission Type. +-- @field #number Performance Performance: a number between 0 and 100, where 100 is best performance. + + +--- Strategy. +-- @type CHIEF.MissionTypeMapping +-- @field #string Attribute Generalized attibute +-- @field #table MissionTypes + + --- CHIEF class version. -- @field #string version CHIEF.version="0.0.1" @@ -97,15 +109,17 @@ CHIEF.version="0.0.1" -- @param #CHIEF self -- @param Core.Set#SET_GROUP AgentSet Set of agents (groups) providing intel. Default is an empty set. -- @param #number Coalition Coalition side, e.g. `coaliton.side.BLUE`. Can also be passed as a string "red", "blue" or "neutral". +-- @param #string Alias An *optional* alias how this object is called in the logs etc. -- @return #CHIEF self -function CHIEF:New(AgentSet, Coalition) +function CHIEF:New(AgentSet, Coalition, Alias) + + -- Set alias. + Alias=Alias or "CHIEF" -- Inherit everything from INTEL class. - local self=BASE:Inherit(self, INTEL:New(AgentSet, Coalition)) --#CHIEF - - -- Set some string id for output to DCS.log file. - --self.lid=string.format("CHIEF | ") + local self=BASE:Inherit(self, INTEL:New(AgentSet, Coalition, Alias)) --#CHIEF + -- Define zones. self:SetBorderZones() self:SetYellowZones() @@ -114,21 +128,25 @@ function CHIEF:New(AgentSet, Coalition) -- Create a new COMMANDER. self.commander=COMMANDER:New() + -- Init DEFCON. self.Defcon=CHIEF.DEFCON.GREEN -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("*", "AssignMissionAirforce", "*") -- Assign mission to a COMMANDER but request only AIR assets. - self:AddTransition("*", "AssignMissionNavy", "*") -- Assign mission to a COMMANDER but request only NAVAL assets. - self:AddTransition("*", "AssignMissionArmy", "*") -- Assign mission to a COMMANDER but request only GROUND assets. + -- From State --> Event --> To State + self:AddTransition("*", "MissionAssignToAny", "*") -- Assign mission to a COMMANDER. - self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. + self:AddTransition("*", "MissionAssignToAirfore", "*") -- Assign mission to a COMMANDER but request only AIR assets. + self:AddTransition("*", "MissionAssignToNavy", "*") -- Assign mission to a COMMANDER but request only NAVAL assets. + self:AddTransition("*", "MissionAssignToArmy", "*") -- Assign mission to a COMMANDER but request only GROUND assets. - self:AddTransition("*", "Defcon", "*") -- Change defence condition. + self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. + self:AddTransition("*", "TransportCancel", "*") -- Cancel transport. - self:AddTransition("*", "Stategy", "*") -- Change strategy condition. + self:AddTransition("*", "Defcon", "*") -- Change defence condition. - self:AddTransition("*", "DeclareWar", "*") -- Declare War. + self:AddTransition("*", "Stategy", "*") -- Change strategy condition. + + self:AddTransition("*", "DeclareWar", "*") -- Declare War. ------------------------ --- Pseudo Functions --- @@ -163,13 +181,33 @@ function CHIEF:New(AgentSet, Coalition) -- @param #number delay Delay in seconds. + --- Triggers the FSM event "MissionAssignToAny". + -- @function [parent=#CHIEF] MissionAssignToAny + -- @param #CHIEF self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionAssignToAny" after a delay. + -- @function [parent=#CHIEF] __MissionAssignToAny + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionAssignToAny" event. + -- @function [parent=#CHIEF] OnAfterMissionAssignToAny + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionCancel". -- @function [parent=#CHIEF] MissionCancel -- @param #CHIEF self -- @param Ops.Auftrag#AUFTRAG Mission The mission. --- Triggers the FSM event "MissionCancel" after a delay. - -- @function [parent=#CHIEF] MissionCancel + -- @function [parent=#CHIEF] __MissionCancel -- @param #CHIEF self -- @param #number delay Delay in seconds. -- @param Ops.Auftrag#AUFTRAG Mission The mission. @@ -189,7 +227,7 @@ function CHIEF:New(AgentSet, Coalition) -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. --- Triggers the FSM event "TransportCancel" after a delay. - -- @function [parent=#CHIEF] TransportCancel + -- @function [parent=#CHIEF] __TransportCancel -- @param #CHIEF self -- @param #number delay Delay in seconds. -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. @@ -359,6 +397,19 @@ function CHIEF:AddTarget(Target) return self end +--- Add strategically important zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE_RADIUS Zone Strategic zone. +-- @return #CHIEF self +function CHIEF:AddStrateticZone(Zone) + + local opszone=OPSZONE:New(Zone, CoalitionOwner) + + table.insert(self.zonequeue, opszone) + + return self +end + --- Set border zone set. -- @param #CHIEF self @@ -538,7 +589,7 @@ function CHIEF:onafterStatus(From, Event, To) if self.verbose>=1 then local Nassets=self.commander:CountAssets() - local Ncontacts=#self.contacts + local Ncontacts=#self.Contacts local Nmissions=#self.commander.missionqueue local Ntargets=#self.targetqueue @@ -589,7 +640,7 @@ function CHIEF:onafterStatus(From, Event, To) -- Mission queue. if self.verbose>=4 and #self.commander.missionqueue>0 then local text="Mission queue:" - for i,_mission in pairs(self.missionqueue) do + for i,_mission in pairs(self.commander.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG local target=mission:GetTargetName() or "unknown" @@ -629,13 +680,13 @@ end -- FSM Events ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- On after "AssignMissionAssignAirforce" event. +--- On after "MissionAssignToAny" event. -- @param #CHIEF self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param Ops.Auftrag#AUFTRAG Mission The mission. -function CHIEF:onafterAssignMissionAirforce(From, Event, To, Mission) +function CHIEF:onafterMissionAssignToAny(From, Event, To, Mission) if self.commander then self:I(self.lid..string.format("Assigning mission %s (%s) to COMMANDER", Mission.name, Mission.type)) @@ -647,13 +698,31 @@ function CHIEF:onafterAssignMissionAirforce(From, Event, To, Mission) end ---- On after "AssignMissionAssignArmy" event. +--- On after "MissionAssignToAirforce" event. -- @param #CHIEF self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param Ops.Auftrag#AUFTRAG Mission The mission. -function CHIEF:onafterAssignMissionArmy(From, Event, To, Mission) +function CHIEF:onafterMissionAssignToAirforce(From, Event, To, Mission) + + if self.commander then + self:I(self.lid..string.format("Assigning mission %s (%s) to COMMANDER", Mission.name, Mission.type)) + --TODO: Request only air assets. + self.commander:AddMission(Mission) + else + self:E(self.lid..string.format("Mission cannot be assigned as no COMMANDER is defined!")) + end + +end + +--- On after "MissionAssignToArmy" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +function CHIEF:onafterMissionAssignToArmy(From, Event, To, Mission) if self.commander then self:I(self.lid..string.format("Assigning mission %s (%s) to COMMANDER", Mission.name, Mission.type)) @@ -819,10 +888,33 @@ function CHIEF:CheckTargetQueue() -- Valid target? if valid then - --TODO: Create a good mission, which can be passed on to the COMMANDER. - - -- Create mission. - local mission=AUFTRAG:NewTargetAir(target) + env.info("FF got valid target "..target:GetName()) + + -- Get mission performances for the given target. + local MissionPerformances=self:_GetMissionPerformanceFromTarget(target) + + -- Mission. + local mission=nil --Ops.Auftrag#AUFTRAG + + if #MissionPerformances>0 then + + env.info(string.format("FF found mission performance N=%d", #MissionPerformances)) + + -- Mission Type. + local MissionType=nil + + for _,_mp in pairs(MissionPerformances) do + local mp=_mp --#CHIEF.MissionTypePerformance + local n=self.commander:CountAssets(true, {mp.MissionType}) + env.info(string.format("FF Found N=%d assets in stock for mission type %s [Performance=%d]", n, mp.MissionType, mp.Performance)) + if n>0 then + mission=AUFTRAG:NewFromTarget(target, mp.MissionType) + break + end + end + + + end if mission then @@ -847,6 +939,7 @@ function CHIEF:CheckTargetQueue() end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Resources ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -916,29 +1009,209 @@ end --- Check resources. -- @param #CHIEF self +-- @param Ops.Target#TARGET Target Target. -- @return #table -function CHIEF:CheckResources() +function CHIEF:CheckResources(Target) - -- TODO: look at lower classes to do this! it's all there... - - local capabilities={} - - for _,MissionType in pairs(AUFTRAG.Type) do - capabilities[MissionType]=0 + local objective=Target:GetObjective() - for _,_airwing in pairs(self.airwings) do - local airwing=_airwing --Ops.AirWing#AIRWING - - -- Get Number of assets that can do this type of missions. - local _,assets=airwing:CanMission(MissionType) - - -- Add up airwing resources. - capabilities[MissionType]=capabilities[MissionType]+#assets - end + local missionperformances={} + + for _,missionperformance in pairs(missionperformances) do + local mp=missionperformance --#CHIEF.MissionTypePerformance + end - return capabilities + +end + +--- Create a mission performance table. +-- @param #CHIEF self +-- @param #string MissionType Mission type. +-- @param #number Performance Performance. +-- @return #CHIEF.MissionPerformance Mission performance. +function CHIEF:_CreateMissionPerformance(MissionType, Performance) + local mp={} --#CHIEF.MissionTypePerformance + mp.MissionType=MissionType + mp.Performance=Performance + return mp +end + +--- Create a mission to attack a group. Mission type is automatically chosen from the group category. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target +-- @return #table Mission performances of type #CHIEF.MissionPerformance +function CHIEF:_GetMissionPerformanceFromTarget(Target) + + local group=nil --Wrapper.Group#GROUP + local airbase=nil --Wrapper.Airbase#AIRBASE + local scenery=nil --Wrapper.Scenery#SCENERY + local coordinate=nil --Core.Point#COORDINATE + + -- Get target objective. + local target=Target:GetObject() + + if target:IsInstanceOf("GROUP") then + group=target --Target is already a group. + elseif target:IsInstanceOf("UNIT") then + group=target:GetGroup() + elseif target:IsInstanceOf("AIRBASE") then + airbase=target + elseif target:IsInstanceOf("SCENERY") then + scenery=target + end + + local TargetCategory=Target:GetCategory() + + local missionperf={} --#CHIEF.MissionTypePerformance + + if group then + + local category=group:GetCategory() + local attribute=group:GetAttribute() + + if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then + + --- + -- A2A: Intercept + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.INTERCEPT, 100)) + + elseif category==Group.Category.GROUND or category==Group.Category.TRAIN then + + --- + -- GROUND + --- + + if attribute==GROUP.Attribute.GROUND_SAM then + + -- SEAD/DEAD + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD, 100)) + + elseif attribute==GROUP.Attribute.GROUND_AAA then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + + elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 70)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif attribute==GROUP.Attribute.GROUND_INFANTRY then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + + else + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + + end + + + elseif category==Group.Category.SHIP then + + --- + -- NAVAL + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ANTISHIP, 100)) + + else + self:E(self.lid.."ERROR: Unknown Group category!") + end + + elseif airbase then + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBRUNWAY, 100)) + elseif scenery then + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.STRIKE, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 70)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + elseif coordinate then + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + end + + return missionperf +end + +--- Check if group is inside our border. +-- @param #CHIEF self +-- @param #string Attribute Group attibute. +-- @return #table Mission types +function CHIEF:_GetMissionTypeForGroupAttribute(Attribute) + + local missiontypes={} + + if Attribute==GROUP.Attribute.AIR_ATTACKHELO then + + local mt={} --#CHIEF.MissionTypePerformance + mt.MissionType=AUFTRAG.Type.INTERCEPT + mt.Performance=100 + table.insert(missiontypes, mt) + + elseif Attribute==GROUP.Attribute.GROUND_AAA then + + local mt={} --#CHIEF.MissionTypePerformance + mt.MissionType=AUFTRAG.Type.BAI + mt.Performance=100 + table.insert(missiontypes, mt) + + local mt={} --#CHIEF.MissionTypePerformance + mt.MissionType=AUFTRAG.Type.BOMBING + mt.Performance=70 + table.insert(missiontypes, mt) + + local mt={} --#CHIEF.MissionTypePerformance + mt.MissionType=AUFTRAG.Type.BOMBCARPET + mt.Performance=70 + table.insert(missiontypes, mt) + + local mt={} --#CHIEF.MissionTypePerformance + mt.MissionType=AUFTRAG.Type.ARTY + mt.Performance=30 + table.insert(missiontypes, mt) + + elseif Attribute==GROUP.Attribute.GROUND_SAM then + + local mt={} --#CHIEF.MissionTypePerformance + mt.MissionType=AUFTRAG.Type.SEAD + mt.Performance=100 + table.insert(missiontypes, mt) + + local mt={} --#CHIEF.MissionTypePerformance + mt.MissionType=AUFTRAG.Type.BAI + mt.Performance=100 + table.insert(missiontypes, mt) + + local mt={} --#CHIEF.MissionTypePerformance + mt.MissionType=AUFTRAG.Type.ARTY + mt.Performance=50 + table.insert(missiontypes, mt) + + elseif Attribute==GROUP.Attribute.GROUND_EWR then + + local mt={} --#CHIEF.MissionTypePerformance + mt.MissionType=AUFTRAG.Type.SEAD + mt.Performance=100 + table.insert(missiontypes, mt) + + local mt={} --#CHIEF.MissionTypePerformance + mt.MissionType=AUFTRAG.Type.BAI + mt.Performance=100 + table.insert(missiontypes, mt) + + local mt={} --#CHIEF.MissionTypePerformance + mt.MissionType=AUFTRAG.Type.ARTY + mt.Performance=50 + table.insert(missiontypes, mt) + + + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua index 659736d62..62d543db7 100644 --- a/Moose Development/Moose/Ops/Cohort.lua +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -294,7 +294,7 @@ function COHORT:AddMissionCapability(MissionTypes, Performance) capability.MissionType=missiontype capability.Performance=Performance or 50 table.insert(self.missiontypes, capability) - + env.info("FF adding mission capability "..tostring(capability.MissionType)) end end @@ -783,11 +783,11 @@ function COHORT:CountAssets(InStock, MissionTypes, Attributes) if MissionTypes==nil or self:CheckMissionCapability(MissionTypes, self.missiontypes) then if Attributes==nil or self:CheckAttribute(Attributes) then if asset.spawned then - if InStock==true or InStock==nil then + if InStock==false or InStock==nil then N=N+1 --Spawned but we also count the spawned ones. end else - if InStock==false or InStock==nil then + if InStock==true or InStock==nil then N=N+1 --This is in stock. end end diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua index a739f045c..7433f3413 100644 --- a/Moose Development/Moose/Ops/Commander.lua +++ b/Moose Development/Moose/Ops/Commander.lua @@ -549,7 +549,7 @@ function COMMANDER:onafterMissionCancel(From, Event, To, Mission) end ---- On after "MissionAssign" event. Mission is added to a LEGION mission queue. +--- On after "TransportAssign" event. Transport is added to a LEGION mission queue. -- @param #COMMANDER self -- @param #string From From state. -- @param #string Event Event. diff --git a/Moose Development/Moose/Ops/OpsZone.lua b/Moose Development/Moose/Ops/OpsZone.lua index 585a4da7b..d4ca460b1 100644 --- a/Moose Development/Moose/Ops/OpsZone.lua +++ b/Moose Development/Moose/Ops/OpsZone.lua @@ -18,9 +18,18 @@ -- @field #number verbose Verbosity of output. -- @field Core.Zone#ZONE zone The zone. -- @field #string zoneName Name of the zone. +-- @field #number zoneRadius Radius of the zone in meters. -- @field #number ownerCurrent Coalition of the current owner of the zone. -- @field #number ownerPrevious Coalition of the previous owner of the zone. -- @field Core.Timer#TIMER timerStatus Timer for calling the status update. +-- @field #number Nred Number of red units in the zone. +-- @field #number Nblu Number of blue units in the zone. +-- @field #number Nnut Number of neutral units in the zone. +-- @field #table ObjectCategories Object categories for the scan. +-- @field #table UnitCategories Unit categories for the scan. +-- @field #number Tattacked Abs. mission time stamp when an attack was started. +-- @field #number dTCapture Time interval in seconds until a zone is captured. +-- @field #boolean neutralCanCapture Neutral units can capture. Default `false`. -- @extends Core.Fsm#FSM --- Be surprised! @@ -31,24 +40,32 @@ -- -- An OPSZONE is a strategically important area. -- +-- **Restrictions** +-- +-- * Since we are using a DCS routine that scans a zone for units or other objects present in the zone and this DCS routine is limited to cicular zones, only those can be used. -- -- @field #OPSZONE OPSZONE = { ClassName = "OPSZONE", - verbose = 3, + verbose = 0, + Nred = 0, + Nblu = 0, + Nnut = 0, } --- OPSZONE class version. -- @field #string version -OPSZONE.version="0.0.1" +OPSZONE.version="0.1.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Can neutrals capture? --- TODO: Can statics capture or hold a zone? +-- TODO: Pause/unpause evaluations. +-- TODO: Capture time, i.e. time how long a single coalition has to be inside the zone to capture it. +-- TODO: Can neutrals capture? No, since they are _neutral_! +-- TODO: Can statics capture or hold a zone? No, unless explicitly requested by mission designer. -- TODO: Differentiate between ground attack and boming by air or arty. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -78,34 +95,139 @@ function OPSZONE:New(Zone, CoalitionOwner) self:E("ERROR: OPSZONE must be a SPHERICAL zone due to DCS restrictions!") return nil end - - self.zone=Zone - self.zoneName=Zone:GetName() - self.zoneRadius=Zone:GetRadius() - - self.ownerCurrent=CoalitionOwner or coalition.side.NEUTRAL - self.ownerPrevious=CoalitionOwner or coalition.side.NEUTRAL -- Set some string id for output to DCS.log file. self.lid=string.format("OPSZONE %s | ", Zone:GetName()) - -- FMS start state is PLANNED. + -- Set some values. + self.zone=Zone + self.zoneName=Zone:GetName() + self.zoneRadius=Zone:GetRadius() + + -- Current and previous owners. + self.ownerCurrent=CoalitionOwner or coalition.side.NEUTRAL + self.ownerPrevious=CoalitionOwner or coalition.side.NEUTRAL + + -- Set object categories. + self:SetObjectCategories() + self:SetUnitCategories() + + -- Status timer. + self.timerStatus=TIMER:New(OPSZONE.Status, self) + + + -- FMS start state is EMPTY. self:SetStartState("Empty") - -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("*", "Start", "*") -- Start FSM. - self:AddTransition("*", "Stop", "*") -- Start FSM. + self:AddTransition("*", "Stop", "*") -- Stop FSM. - self:AddTransition("*", "Captured", "Guarded") -- Start FSM. - self:AddTransition("*", "Empty", "Empty") -- Start FSM. + self:AddTransition("*", "Captured", "Guarded") -- Zone was captured. + self:AddTransition("*", "Empty", "Empty") -- No red or blue units inside the zone. self:AddTransition("*", "Attacked", "Attacked") -- A guarded zone is under attack. self:AddTransition("*", "Defeated", "Guarded") -- The owning coalition defeated an attack. + ------------------------ + --- Pseudo Functions --- + ------------------------ - self.timerStatus=TIMER:New(OPSZONE.Status, self) + --- Triggers the FSM event "Start". + -- @function [parent=#OPSZONE] Start + -- @param #OPSZONE self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#OPSZONE] __Start + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". + -- @param #OPSZONE self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#OPSZONE] __Stop + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Captured". + -- @function [parent=#OPSZONE] Captured + -- @param #OPSZONE self + -- @param #number Coalition Coalition side that captured the zone. + + --- Triggers the FSM event "Captured" after a delay. + -- @function [parent=#OPSZONE] __Captured + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + -- @param #number Coalition Coalition side that captured the zone. + + --- On after "Captured" event. + -- @function [parent=#OPSZONE] OnAfterCaptured + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Coalition Coalition side that captured the zone. + + + --- Triggers the FSM event "Empty". + -- @function [parent=#OPSZONE] Empty + -- @param #OPSZONE self + + --- Triggers the FSM event "Empty" after a delay. + -- @function [parent=#OPSZONE] __Empty + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + --- On after "Empty" event. + -- @function [parent=#OPSZONE] OnAfterEmpty + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Attacked". + -- @function [parent=#OPSZONE] Attacked + -- @param #OPSZONE self + -- @param #number AttackerCoalition Coalition side that is attacking the zone. + + --- Triggers the FSM event "Attacked" after a delay. + -- @function [parent=#OPSZONE] __Attacked + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + -- @param #number AttackerCoalition Coalition side that is attacking the zone. + + --- On after "Attacked" event. + -- @function [parent=#OPSZONE] OnAfterAttacked + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number AttackerCoalition Coalition side that is attacking the zone. + + + --- Triggers the FSM event "Defeated". + -- @function [parent=#OPSZONE] Defeated + -- @param #OPSZONE self + -- @param #number DefeatedCoalition Coalition side that was defeated. + + --- Triggers the FSM event "Defeated" after a delay. + -- @function [parent=#OPSZONE] __Defeated + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + -- @param #number DefeatedCoalition Coalition side that was defeated. + + --- On after "Defeated" event. + -- @function [parent=#OPSZONE] OnAfterDefeated + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number DefeatedCoalition Coalition side that was defeated. return self end @@ -114,6 +236,58 @@ end -- User Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Set verbosity level. +-- @param #OPSZONE self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #OPSZONE self +function OPSZONE:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set categories of objects that can capture or hold the zone. +-- @param #OPSZONE self +-- @param #table Categories Object categories. Default is `{Object.Category.UNIT, Object.Category.STATIC}`, i.e. UNITs and STATICs. +-- @return #OPSZONE self +function OPSZONE:SetObjectCategories(Categories) + + -- Ensure table if something was passed. + if Categories and type(Categories)~="table" then + Categories={Categories} + end + + -- Set categories. + self.ObjectCategories=Categories or {Object.Category.UNIT, Object.Category.STATIC} + + return self +end + +--- Set categories of units that can capture or hold the zone. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). +-- @param #OPSZONE self +-- @param #table Categories Table of unit categories. Default `{Unit.Category.GROUND_UNIT}`. +-- @return #OPSZONE +function OPSZONE:SetUnitCategories(Categories) + + -- Ensure table. + if Categories and type(Categories)~="table" then + Categories={Categories} + end + + -- Set categories. + self.UnitCategories=Categories or {Unit.Category.GROUND_UNIT} + + return self +end + +--- Set whether *neutral* units can capture the zone. +-- @param #OPSZONE self +-- @param #boolean CanCapture If `true`, neutral units can. +-- @return #OPSZONE self +function OPSZONE:SetNeutralCanCapture(CanCapture) + self.neutralCanCapture=CanCapture + return self +end + --- Get current owner of the zone. -- @param #OPSZONE self -- @return #number Owner coalition. @@ -128,6 +302,19 @@ function OPSZONE:GetPreviousOwner() return self.ownerPrevious end +--- Get duration of the current attack. +-- @param #OPSZONE self +-- @return #number Duration in seconds since when the last attack began. Is `nil` if the zone is not under attack currently. +function OPSZONE:GetAttackDuration() + if self:IsAttacked() and self.Tattacked then + + local dT=timer.getAbsTime()-self.Tattacked + return dT + end + + return nil +end + --- Check if the red coalition is currently owning the zone. -- @param #OPSZONE self @@ -156,7 +343,7 @@ end --- Check if zone is guarded. -- @param #OPSZONE self -- @return #boolean If `true`, zone is guarded. -function OPSZONE:IsEmpty() +function OPSZONE:IsGuarded() local is=self:is("Guarded") return is end @@ -186,7 +373,7 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- FSM Functions +-- Start/Stop and Status Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Start OPSZONE FSM. @@ -199,6 +386,9 @@ function OPSZONE:onafterStart(From, Event, To) -- Info. self:I(self.lid..string.format("Starting OPSZONE v%s", OPSZONE.version)) + -- Reinit the timer. + self.timerStatus=self.timerStatus or TIMER:New(OPSZONE.Status, self) + -- Status update. self.timerStatus:Start(1, 60) @@ -210,10 +400,15 @@ function OPSZONE:Status() -- Current FSM state. local fsmstate=self:GetState() + + -- Get contested. + local contested=tostring(self:IsContested()) -- Info message. - local text=string.format("State %s: Owner %d (previous %d), contested=%s, Nunits: red=%d, blue=%d, neutral=%d", fsmstate, self.ownerCurrent, self.ownerPrevious, tostring(self:IsContested()), 0, 0, 0) - self:I(self.lid..text) + if self.verbose>=1 then + local text=string.format("State %s: Owner %d (previous %d), contested=%s, Nunits: red=%d, blue=%d, neutral=%d", fsmstate, self.ownerCurrent, self.ownerPrevious, contested, self.Nred, self.Nblu, self.Nnut) + self:I(self.lid..text) + end -- Scanning zone. self:Scan() @@ -233,8 +428,9 @@ end function OPSZONE:onafterCaptured(From, Event, To, NewOwnerCoalition) -- Debug info. - self:I(self.lid..string.format("Zone captured by %d coalition", NewOwnerCoalition)) + self:T(self.lid..string.format("Zone captured by coalition=%d", NewOwnerCoalition)) + -- Set owners. self.ownerPrevious=self.ownerCurrent self.ownerCurrent=NewOwnerCoalition @@ -248,7 +444,7 @@ end function OPSZONE:onafterEmpty(From, Event, To) -- Debug info. - self:I(self.lid..string.format("Zone is empty now")) + self:T(self.lid..string.format("Zone is empty now")) end @@ -261,10 +457,7 @@ end function OPSZONE:onafterAttacked(From, Event, To, AttackerCoalition) -- Debug info. - self:I(self.lid..string.format("Zone is being attacked by coalition %s!", tostring(AttackerCoalition))) - - -- Time stam when the attack started. - self.Tattacked=timer.getAbsTime() + self:T(self.lid..string.format("Zone is being attacked by coalition=%s!", tostring(AttackerCoalition))) end @@ -277,7 +470,7 @@ end function OPSZONE:onafterEmpty(From, Event, To) -- Debug info. - self:I(self.lid..string.format("Zone is empty now")) + self:T(self.lid..string.format("Zone is empty now")) end @@ -286,10 +479,11 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function OPSZONE:onafterDefeated(From, Event, To) +-- @param #number DefeatedCoalition Coalition side that was defeated. +function OPSZONE:onafterDefeated(From, Event, To, DefeatedCoalition) -- Debug info. - self:I(self.lid..string.format("Attack on zone has been defeated")) + self:T(self.lid..string.format("Defeated attack on zone by coalition=%d", DefeatedCoalition)) -- Not attacked any more. self.Tattacked=nil @@ -304,8 +498,9 @@ end function OPSZONE:onenterGuarded(From, Event, To) -- Debug info. - self:I(self.lid..string.format("Zone is guarded")) + self:T(self.lid..string.format("Zone is guarded")) + -- Not attacked any more. self.Tattacked=nil end @@ -318,9 +513,10 @@ end function OPSZONE:onenterAttacked(From, Event, To) -- Debug info. - self:I(self.lid..string.format("Zone is Attacked")) + self:T(self.lid..string.format("Zone is Attacked")) - self.Tattacked=nil + -- Time stamp when the attack started. + self.Tattacked=timer.getAbsTime() end @@ -334,14 +530,15 @@ end function OPSZONE:Scan() -- Debug info. - local text=string.format("Scanning zone %s R=%.1f m", self.zone:GetName(), self.zone:GetRadius()) - self:I(self.lid..text) + if self.verbose>=3 then + local text=string.format("Scanning zone %s R=%.1f m", self.zoneName, self.zoneRadius) + self:I(self.lid..text) + end -- Search. - local SphereSearch={id=world.VolumeType.SPHERE, params={point=self.zone:GetVec3(), radius=self.zone:GetRadius(),}} + local SphereSearch={id=world.VolumeType.SPHERE, params={point=self.zone:GetVec3(), radius=self.zoneRadius}} - local ObjectCategories={Object.Category.UNIT, Object.Category.STATIC} - + -- Init number of red, blue and neutral units. local Nred=0 local Nblu=0 local Nnut=0 @@ -362,12 +559,67 @@ function OPSZONE:Scan() -- UNIT --- + -- This is a DCS unit object. local DCSUnit=ZoneObject --DCS#Unit - --TODO: only ground units! + --- Function to check if unit category is included. + local function Included() + + if not self.UnitCategories then + -- Any unit is included. + return true + else + -- Check if found object is in specified categories. + local CategoryDCSUnit = ZoneObject:getDesc().category + + for _,UnitCategory in pairs(self.UnitCategories) do + if UnitCategory==CategoryDCSUnit then + return true + end + end + + end + + return false + end - local Coalition=DCSUnit:getCoalition() + + if Included() then + -- Get Coalition. + local Coalition=DCSUnit:getCoalition() + + -- Increase counter. + if Coalition==coalition.side.RED then + Nred=Nred+1 + elseif Coalition==coalition.side.BLUE then + Nblu=Nblu+1 + elseif Coalition==coalition.side.NEUTRAL then + Nnut=Nnut+1 + end + + -- Debug info. + if self.verbose>=4 then + self:I(self.lid..string.format("Found unit %s (coalition=%d)", DCSUnit:getName(), Coalition)) + end + end + + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then + + --- + -- STATIC + --- + + -- This is a DCS static object. + local DCSStatic=ZoneObject --DCS#Static + + -- Get coalition. + local Coalition=DCSStatic:getCoalition() + + -- CAREFUL! Downed pilots break routine here without any error thrown. + --local unit=STATIC:Find(DCSStatic) + + -- Increase counter. if Coalition==coalition.side.RED then Nred=Nred+1 elseif Coalition==coalition.side.BLUE then @@ -376,23 +628,10 @@ function OPSZONE:Scan() Nnut=Nnut+1 end - local unit=UNIT:Find(DCSUnit) - - env.info(string.format("FF found unit %s", unit:GetName())) - - - elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then - - --- - -- STATIC - --- - - local DCSStatic=ZoneObject --DCS#Static - - -- CAREFUL! Downed pilots break routine here without any error thrown. - local unit=STATIC:Find(DCSStatic) - - --env.info(string.format("FF found static %s", unit:GetName())) + -- Debug info + if self.verbose>=4 then + self:I(self.lid..string.format("Found static %s (coalition=%d)", DCSStatic:getName(), Coalition)) + end elseif ObjectCategory==Object.Category.SCENERY then @@ -403,9 +642,8 @@ function OPSZONE:Scan() local SceneryType = ZoneObject:getTypeName() local SceneryName = ZoneObject:getName() - local Scenery=SCENERY:Register(SceneryName, ZoneObject) - - env.info(string.format("FF found scenery type=%s, name=%s", SceneryType, SceneryName)) + -- Debug info. + self:T2(self.lid..string.format("Found scenery type=%s, name=%s", SceneryType, SceneryName)) end end @@ -414,11 +652,18 @@ function OPSZONE:Scan() end -- Search objects. - world.searchObjects(ObjectCategories, SphereSearch, EvaluateZone) + world.searchObjects(self.ObjectCategories, SphereSearch, EvaluateZone) -- Debug info. - local text=string.format("Scan result Nred=%d, Nblue=%d, Nneutrl=%d", Nred, Nblu, Nnut) - self:I(self.lid..text) + if self.verbose>=3 then + local text=string.format("Scan result Nred=%d, Nblue=%d, Nneutral=%d", Nred, Nblu, Nnut) + self:I(self.lid..text) + end + + -- Set values. + self.Nred=Nred + self.Nblu=Nblu + self.Nnut=Nnut if self:IsRed() then @@ -438,7 +683,9 @@ function OPSZONE:Scan() self:Captured(coalition.side.NEUTRAL) else -- Red zone is now empty (but will remain red). - self:Empty() + if not self:IsEmpty() then + self:Empty() + end end else @@ -486,7 +733,9 @@ function OPSZONE:Scan() self:Captured(coalition.side.NEUTRAL) else -- Blue zone is empty now. - self:Empty() + if not self:IsEmpty() then + self:Empty() + end end else @@ -530,7 +779,7 @@ function OPSZONE:Scan() -- No neutral units in neutral zone any more. if Nred>0 and Nblu>0 then - env.info("FF neutrals left neutral zone and red and blue are present! What to do?") + env.info(self.lid.."FF neutrals left neutral zone and red and blue are present! What to do?") -- TODO Contested! self:Attacked() self.isContested=true @@ -550,7 +799,7 @@ function OPSZONE:Scan() --end else - env.info("FF error") + self:E(self.lid.."ERROR!") end return self diff --git a/Moose Development/Moose/Ops/Target.lua b/Moose Development/Moose/Ops/Target.lua index 57993a85c..35aa98cda 100644 --- a/Moose Development/Moose/Ops/Target.lua +++ b/Moose Development/Moose/Ops/Target.lua @@ -31,15 +31,14 @@ -- @field #table elements Table of target elements/units. -- @field #table casualties Table of dead element names. -- @field #number prio Priority. --- @field #number importance Importance +-- @field #number importance Importance. +-- @field #boolean isDestroyed If true, target objects were destroyed. -- @extends Core.Fsm#FSM --- **It is far more important to be able to hit the target than it is to haggle over who makes a weapon or who pulls a trigger** -- Dwight D. Eisenhower -- -- === -- --- ![Banner Image](..\Presentations\OPS\Target\_Main.pngs) --- -- # The TARGET Concept -- -- Define a target of your mission and monitor its status. Events are triggered when the target is damaged or destroyed. @@ -130,13 +129,13 @@ _TARGETID=0 --- TARGET class version. -- @field #string version -TARGET.version="0.5.0" +TARGET.version="0.5.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: A lot. +-- TODO: Add pseudo functions. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -189,7 +188,7 @@ function TARGET:New(TargetObject) self:AddTransition("*", "Damaged", "*") -- Target was damaged. self:AddTransition("*", "Destroyed", "Dead") -- Target was completely destroyed. - self:AddTransition("*", "Dead", "Dead") -- Target was completely destroyed. + self:AddTransition("*", "Dead", "Dead") -- Target is dead. Could be destroyed or despawned. ------------------------ --- Pseudo Functions --- @@ -222,7 +221,6 @@ function TARGET:New(TargetObject) -- @param #number delay Delay in seconds. - -- Start. self:__Start(-1) @@ -300,7 +298,7 @@ end --- Set importance of the target. -- @param #TARGET self --- @param #number Priority Priority of the target. Default `nil`. +-- @param #number Importance Importance of the target. Default `nil`. -- @return #TARGET self function TARGET:SetImportance(Importance) self.importance=Importance @@ -311,14 +309,24 @@ end -- @param #TARGET self -- @return #boolean If true, target is alive. function TARGET:IsAlive() - return self:Is("Alive") + local is=self:Is("Alive") + return is end +--- Check if TARGET is destroyed. +-- @param #TARGET self +-- @return #boolean If true, target is destroyed. +function TARGET:IsDestroyed() + return self.isDestroyed +end + + --- Check if TARGET is dead. -- @param #TARGET self -- @return #boolean If true, target is dead. function TARGET:IsDead() - return self:Is("Dead") + local is=self:Is("Dead") + return is end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -469,6 +477,8 @@ function TARGET:onafterObjectDead(From, Event, To, Target) if self.Ndestroyed==self.Ntargets0 then + self.isDestroyed=true + self:Destroyed() else @@ -1041,6 +1051,12 @@ function TARGET:GetCoordinate() return nil end +--- Get category. +-- @param #TARGET self +-- @return #string Target category. See `TARGET.Category.X`, where `X=AIRCRAFT, GROUND`. +function TARGET:GetCategory() + return self.category +end --- Get target category. -- @param #TARGET self