From 5cb1036618a7ed0717dea893c7c36ed3d1757c70 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 19 Jul 2020 01:22:49 +0200 Subject: [PATCH] CHIEF - Added Chief of Staff class. --- Moose Development/Moose/Modules.lua | 1 + Moose Development/Moose/Ops/AirWing.lua | 1 - Moose Development/Moose/Ops/Auftrag.lua | 28 +- Moose Development/Moose/Ops/ChiefOfStaff.lua | 698 ++++++++++++++++++ Moose Development/Moose/Ops/Intelligence.lua | 198 ++++- Moose Development/Moose/Ops/Squadron.lua | 6 +- Moose Development/Moose/Ops/WingCommander.lua | 386 +--------- Moose Setup/Moose.files | 1 + 8 files changed, 899 insertions(+), 420 deletions(-) create mode 100644 Moose Development/Moose/Ops/ChiefOfStaff.lua diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 2a3f84f99..7f49bf4ba 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -78,6 +78,7 @@ __Moose.Include( 'Scripts/Moose/Ops/Squadron.lua' ) __Moose.Include( 'Scripts/Moose/Ops/AirWing.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Intelligence.lua' ) __Moose.Include( 'Scripts/Moose/Ops/WingCommander.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/ChiefOfStaff.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' ) diff --git a/Moose Development/Moose/Ops/AirWing.lua b/Moose Development/Moose/Ops/AirWing.lua index 644d58caa..56bf7ca4b 100644 --- a/Moose Development/Moose/Ops/AirWing.lua +++ b/Moose Development/Moose/Ops/AirWing.lua @@ -1479,7 +1479,6 @@ function AIRWING:onafterNewAsset(From, Event, To, asset, assignment) -- If grouping is larger than units present, copy first unit. if i>nunits then - --unit=UTILS.DeepCopy(template.units[1]) table.insert(template.units, UTILS.DeepCopy(template.units[1])) end diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index a801c5499..53557a8de 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -33,6 +33,8 @@ -- @field #number Tstop Mission stop time in seconds. -- @field #number duration Mission duration in seconds. -- @field Wrapper.Marker#MARKER marker F10 map marker. +-- @field #boolean markerOn If true, display marker on F10 map with the AUFTRAG status. +-- @field #numberr markerCoaliton Coalition to which the marker is dispayed. -- @field #table DCStask DCS task structure. -- @field #number Ntargets Number of mission targets. -- @field #number dTevaluate Time interval in seconds before the mission result is evaluated after mission is over. @@ -259,6 +261,8 @@ AUFTRAG = { missionFraction = 0.5, enrouteTasks = {}, marker = nil, + markerOn = nil, + markerCoalition = nil, conditionStart = {}, conditionSuccess = {}, conditionFailure = {}, @@ -416,7 +420,7 @@ AUFTRAG.TargetType={ --- AUFTRAG class version. -- @field #string version -AUFTRAG.version="0.3.0" +AUFTRAG.version="0.3.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1302,6 +1306,16 @@ function AUFTRAG:SetName(Name) return self end +--- Enable markers, which dispay the mission status on the F10 map. +-- @param #AUFTRAG self +-- @param #number Coalition The coaliton side to which the markers are dispayed. Default is to all. +-- @return #AUFTRAG self +function AUFTRAG:SetEnableMarkers(Coalition) + self.markerOn=true + self.markerCoaliton=Coalition or -1 + return self +end + --- Set weapon type used for the engagement. -- @param #AUFTRAG self -- @param #number WeaponType Weapon type. Default is ENUMS.WeaponFlag.Auto @@ -1821,7 +1835,10 @@ function AUFTRAG:onafterStatus(From, Event, To) end -- Update F10 marker. - self:UpdateMarker() + if self.markerOn then + self:UpdateMarker() + end + end --- Evaluate mission outcome - success or failure. @@ -2863,7 +2880,12 @@ function AUFTRAG:UpdateMarker() -- Get target coordinates. Can be nil! local targetcoord=self:GetTargetCoordinate() - self.marker=MARKER:New(targetcoord, text):ReadOnly():ToAll() + if self.markerCoaliton and self.markerCoaliton>=0 then + self.marker=MARKER:New(targetcoord, text):ReadOnly():ToCoalition(self.markerCoaliton) + else + self.marker=MARKER:New(targetcoord, text):ReadOnly():ToAll() + end + else diff --git a/Moose Development/Moose/Ops/ChiefOfStaff.lua b/Moose Development/Moose/Ops/ChiefOfStaff.lua new file mode 100644 index 000000000..89d98a247 --- /dev/null +++ b/Moose Development/Moose/Ops/ChiefOfStaff.lua @@ -0,0 +1,698 @@ +--- **Ops** - Chief of Staff. +-- +-- **Main Features:** +-- +-- * Stuff +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Chief +-- @image OPS_Chief.png + + +--- CHIEF class. +-- @type CHIEF +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table missionqueue Mission 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. +-- @field #string Defcon Defence condition. +-- @field Ops.WingCommander#WINGCOMMANDER wingcommander Wing commander, commanding airborne forces. +-- @field Ops.Admiral#ADMIRAL admiral Admiral commanding navy forces. +-- @field Ops.General#GENERAL genaral General commanding army forces. +-- @extends Ops.Intelligence#INTEL + +--- Be surprised! +-- +-- === +-- +-- ![Banner Image](..\Presentations\WingCommander\CHIEF_Main.jpg) +-- +-- # 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. +-- +-- +-- @field #CHIEF +CHIEF = { + ClassName = "CHIEF", + Debug = nil, + lid = nil, + wingcommander = nil, + admiral = nil, + general = nil, + missionqueue = {}, + borderzoneset = nil, + yellowzoneset = nil, + engagezoneset = nil, +} + +--- Defence condition. +-- @type CHIEF.DEFCON +-- @field #string GREEN No enemy activities detected. +-- @field #string YELLOW Enemy near our border. +-- @field #string RED Enemy within our border. +CHIEF.DEFCON = { + GREEN="Green", + YELLOW="Yellow", + RED="Red", +} + +--- CHIEF class version. +-- @field #string version +CHIEF.version="0.0.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Define A2A and A2G parameters. +-- DONE: Add/remove spawned flightgroups to detection set. +-- DONE: Borderzones. +-- NOGO: Maybe it's possible to preselect the assets for the mission. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new CHIEF object and start the FSM. +-- @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". +-- @return #CHIEF self +function CHIEF:New(AgentSet, Coalition) + + AgentSet=AgentSet or SET_GROUP:New() + + -- 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 | ") + + self:SetBorderZones() + self:SetYellowZones() + + self:SetThreatLevelRange() + + self.Defcon=CHIEF.DEFCON.GREEN + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "AssignMissionAirforce", "*") -- Assign mission to a WINGCOMMANDER. + self:AddTransition("*", "AssignMissionNavy", "*") -- Assign mission to an ADMIRAL. + self:AddTransition("*", "AssignMissionArmy", "*") -- Assign mission to a GENERAL. + self:AddTransition("*", "CancelMission", "*") -- Cancel mission. + self:AddTransition("*", "Defcon", "*") -- Change defence condition. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the CHIEF. Initializes parameters and starts event handlers. + -- @function [parent=#CHIEF] Start + -- @param #CHIEF self + + --- Triggers the FSM event "Start" after a delay. Starts the CHIEF. Initializes parameters and starts event handlers. + -- @function [parent=#CHIEF] __Start + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the CHIEF and all its event handlers. + -- @param #CHIEF self + + --- Triggers the FSM event "Stop" after a delay. Stops the CHIEF and all its event handlers. + -- @function [parent=#CHIEF] __Stop + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#CHIEF] Status + -- @param #CHIEF self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#CHIEF] __Status + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + self.Debug=true + + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set this to be an air-to-any dispatcher, i.e. engaging air, ground and naval targets. This is the default anyway. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToAny() + + self:SetFilterCategory({}) + + return self +end + +--- Set this to be an air-to-air dispatcher. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToAir() + + self:SetFilterCategory({Unit.Category.AIRPLANE, Unit.Category.HELICOPTER}) + + return self +end + +--- Set this to be an air-to-ground dispatcher, i.e. engage only ground units +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToGround() + + self:SetFilterCategory({Unit.Category.GROUND_UNIT}) + + return self +end + +--- Set this to be an air-to-sea dispatcher, i.e. engage only naval units. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToSea() + + self:SetFilterCategory({Unit.Category.SHIP}) + + return self +end + +--- Set this to be an air-to-surface dispatcher, i.e. engaging ground and naval groups. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToSurface() + + self:SetFilterCategory({Unit.Category.GROUND_UNIT, Unit.Category.SHIP}) + + return self +end + +--- Set a threat level range that will be engaged. Threat level is a number between 0 and 10, where 10 is a very dangerous threat. +-- Targets with threat level 0 are usually harmless. +-- @param #CHIEF self +-- @param #number ThreatLevelMin Min threat level. Default 1. +-- @param #number ThreatLevelMax Max threat level. Default 10. +-- @return #CHIEF self +function CHIEF:SetThreatLevelRange(ThreatLevelMin, ThreatLevelMax) + + self.threatLevelMin=ThreatLevelMin or 1 + self.threatLevelMax=ThreatLevelMax or 10 + + return self +end + +--- Set defence condition. +-- @param #CHIEF self +-- @param #string Defcon Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`. +-- @return #CHIEF self +function CHIEF:SetDefcon(Defcon) + + self.Defcon=Defcon + --self:Defcon(Defcon) + + return self +end + + +--- Set the wing commander for the airforce. +-- @param #CHIEF self +-- @param Ops.WingCommander WingCommander The WINGCOMMANDER object. +-- @return #CHIEF self +function CHIEF:SetWingCommander(WingCommander) + + self.wingcommander=WingCommander + + return self +end + +--- Add mission to mission queue. +-- @param #CHIEF self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be added. +-- @return #CHIEF self +function CHIEF:AddMission(Mission) + + table.insert(self.missionqueue, Mission) + + return self +end + +--- Remove mission from queue. +-- @param #CHIEF self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #CHIEF self +function CHIEF:RemoveMission(Mission) + + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==Mission.auftragsnummer then + self:I(self.lid..string.format("Removing mission %s (%s) status=%s from queue", Mission.name, Mission.type, Mission.status)) + table.remove(self.missionqueue, i) + break + end + + end + + return self +end + +--- Set border zone set. +-- @param #CHIEF self +-- @param Core.Set#SET_ZONE BorderZoneSet Set of zones, defining our borders. +-- @return #CHIEF self +function CHIEF:SetBorderZones(BorderZoneSet) + + -- Border zones. + self.borderzoneset=BorderZoneSet or SET_ZONE:New() + + return self +end + +--- Add a zone defining your territory. +-- @param #CHIEF self +-- @param Core.Zone#ZONE BorderZone The zone defining the border of your territory. +-- @return #CHIEF self +function CHIEF:AddBorderZone(BorderZone) + + -- Add a border zone. + self.borderzoneset:AddZone(BorderZone) + + return self +end + +--- Set yellow zone set. Detected enemy troops in this zone will trigger defence condition YELLOW. +-- @param #CHIEF self +-- @param Core.Set#SET_ZONE YellowZoneSet Set of zones, defining our borders. +-- @return #CHIEF self +function CHIEF:SetYellowZones(YellowZoneSet) + + -- Border zones. + self.yellowzoneset=YellowZoneSet or SET_ZONE:New() + + return self +end + +--- Add a zone defining an area outside your territory that is monitored for enemy activity. +-- @param #CHIEF self +-- @param Core.Zone#ZONE YellowZone The zone defining the border of your territory. +-- @return #CHIEF self +function CHIEF:AddYellowZone(YellowZone) + + -- Add a border zone. + self.yellowzoneset:AddZone(YellowZone) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CHIEF:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting Chief of Staff") + self:I(self.lid..text) + + -- Start parent INTEL. + self:GetParent(self).onafterStart(self, From, Event, To) + + -- Start attached airwings. + for _,_airwing in pairs(self.airwings) do + local airwing=_airwing --Ops.AirWing#AIRWING + if airwing:GetState()=="NotReadyYet" then + airwing:Start() + end + end + +end + +--- On after "Status" event. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CHIEF:onafterStatus(From, Event, To) + + -- Start parent INTEL. + self:GetParent(self).onafterStatus(self, From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + + -- Clean up missions where the contact was lost. + for _,_contact in pairs(self.ContactsLost) do + local contact=_contact --#CHIEF.Contact + + if contact.mission and contact.mission:IsNotOver() then + + local text=string.format("Lost contact to target %s! %s mission %s will be cancelled.", contact.groupname, contact.mission.type:upper(), contact.mission.name) + MESSAGE:New(text, 120, "CHIEF"):ToAll() + self:I(self.lid..text) + + -- Cancel this mission. + contact.mission:Cancel() + + end + + end + + -- Create missions for all new contacts. + local Nred=0 + local Nyellow=0 + local Nengage=0 + for _,_contact in pairs(self.Contacts) do + local contact=_contact --#CHIEF.Contact + local group=contact.group --Wrapper.Group#GROUP + + local inred=self:CheckGroupInBorder(group) + if inred then + Nred=Nred+1 + end + + local inyellow=self:CheckGroupInYellow(group) + if inyellow then + Nyellow=Nyellow+1 + end + + -- Is this a threat? + local threat=contact.threatlevel>=self.threatLevelMin and contact.threatlevel<=self.threatLevelMax + + local redalert=true + if self.borderzoneset:Count()>0 then + redalert=inred + end + + if redalert and threat and not contact.mission then + + -- Create a mission based on group category. + local mission=AUFTRAG:NewAUTO(group) + + -- Add mission to queue. + if mission then + + --TODO: Better amount of necessary assets. Count units in asset and in contact. Might need nassetMin/Max. + mission.nassets=1 + + -- Missons are repeated max 3 times on failure. + mission.missionRepeatMax=3 + + -- Set mission contact. + contact.mission=mission + + -- Add mission to queue. + self:AddMission(mission) + end + + end + + end + + -- Set defcon. + -- TODO: Need to introduce time check to avoid fast oscillation between different defcon states in case groups move in and out of the zones. + if Nred>0 then + self:SetDefcon(CHIEF.DEFCON.RED) + elseif Nyellow>0 then + self:SetDefcon(CHIEF.DEFCON.YELLOW) + else + self:SetDefcon(CHIEF.DEFCON.GREEN) + end + + + -- Check mission queue and assign one PLANNED mission. + self:CheckMissionQueue() + + local text=string.format("Defcon=%s Missions=%d Contacts: Total=%d Yellow=%d Red=%d", self.Defcon, #self.missionqueue, #self.Contacts, Nyellow, Nred) + self:I(self.lid..text) + + -- Infor about contacts. + if #self.Contacts>0 then + local text="Contacts:" + for i,_contact in pairs(self.Contacts) do + local contact=_contact --#CHIEF.Contact + local mtext="N/A" + if contact.mission then + mtext=string.format("Mission %s (%s) %s", contact.mission.name, contact.mission.type, contact.mission.status:upper()) + end + text=text..string.format("\n[%d] %s Type=%s (%s): Threat=%d Mission=%s", i, contact.groupname, contact.categoryname, contact.typename, contact.threatlevel, mtext) + end + self:I(self.lid..text) + end + + -- Mission queue. + if #self.missionqueue>0 then + local text="Mission queue:" + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + local target=mission:GetTargetName() or "unknown" + + text=text..string.format("\n[%d] %s (%s): status=%s, target=%s", i, mission.name, mission.type, mission.status, target) + end + self:I(self.lid..text) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "MissionAssign" event. Mission is added to the AIRWING mission queue. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.AirWing#AIRWING Airwing The AIRWING. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +function CHIEF:onafterMissionAssign(From, Event, To, Airwing, Mission) + + self:I(self.lid..string.format("Assigning mission %s (%s) to airwing %s", Mission.name, Mission.type, Airwing.alias)) + Airwing:AddMission(Mission) + +end + +--- On after "CancelMission" 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:onafterCancelMission(From, Event, To, Mission) + + self:I(self.lid..string.format("Cancelling mission %s (%s) in status %s", Mission.name, Mission.type, Mission.status)) + + if Mission.status==AUFTRAG.Status.PLANNED then + + -- Mission is still in planning stage. Should not have an airbase assigned ==> Just remove it form the queue. + self:RemoveMission(Mission) + + else + + -- Airwing will cancel mission. + if Mission.airwing then + Mission.airwing:MissionCancel(Mission) + end + + end + +end + +--- On before "Defcon" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Defcon New defence condition. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +function CHIEF:onbeforeDefcon(From, Event, To, Defcon) + + local gotit=false + for _,defcon in pairs(CHIEF.DEFCON) do + if defcon==Defcon then + gotit=true + end + end + + if not gotit then + self:E(self.lid..string.format("ERROR: Unknown DEFCON specified! Dont know defcon=%s", tostring(Defcon))) + return false + end + + -- Defcon did not change. + if Defcon==self.Defcon then + self:I(self.lid..string.format("Defcon %s unchanged. No processing transition.", tostring(Defcon))) + return false + end + + return true +end + +--- On after "Defcon" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Defcon New defence condition. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +function CHIEF:onafterDefcon(From, Event, To, Defcon) + self:I(self.lid..string.format("Changing Defcon from %s --> %s", self.Defcon, Defcon)) + + -- Set new defcon. + self.Defcon=Defcon +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Resources +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check mission queue and assign ONE planned mission. +-- @param #CHIEF self +function CHIEF:CheckMissionQueue() + + -- TODO: Sort mission queue. wrt what? Threat level? + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- We look for PLANNED missions. + if mission.status==AUFTRAG.Status.PLANNED then + + --- + -- PLANNNED Mission + --- + + local airwing=self:GetAirwingForMission(mission) + + if airwing then + + -- Add mission to airwing. + self:MissionAssign(airwing, mission) + + return + end + + else + + --- + -- Missions NOT in PLANNED state + --- + + end + + end + +end + +--- Check all airwings if they are able to do a specific mission type at a certain location with a given number of assets. +-- @param #CHIEF self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return Ops.AirWing#AIRWING The airwing best for this mission. +function CHIEF:GetAirwingForMission(Mission) + + if self.wingcommander then + return self.wingcommander:GetAirwingForMission(Mission) + end + + return nil +end + +--- Check if group is inside our border. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @return #boolean If true, group is in any zone. +function CHIEF:CheckGroupInBorder(group) + + local inside=self:CheckGroupInZones(group, self.borderzoneset) + + return inside +end + +--- Check if group is near our border (yellow zone). +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @return #boolean If true, group is in any zone. +function CHIEF:CheckGroupInYellow(group) + + -- Check inside yellow but not inside our border. + local inside=self:CheckGroupInZones(group, self.yellowzoneset) and not self:CheckGroupInZones(group, self.borderzoneset) + + return inside +end + +--- Check if group is inside a zone. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @param Core.Set#SET_ZONE zoneset Set of zones. +-- @return #boolean If true, group is in any zone. +function CHIEF:CheckGroupInZones(group, zoneset) + + for _,_zone in pairs(zoneset.Set or {}) do + local zone=_zone --Core.Zone#ZONE + + if group:IsPartlyOrCompletelyInZone(zone) then + return true + end + end + + return false +end + +--- Check resources. +-- @param #CHIEF self +-- @return #table +function CHIEF:CheckResources() + + local capabilities={} + + for _,MissionType in pairs(AUFTRAG.Type) do + capabilities[MissionType]=0 + + 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 + + end + + return capabilities +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/Moose Development/Moose/Ops/Intelligence.lua b/Moose Development/Moose/Ops/Intelligence.lua index 59eccdb51..a70b7c402 100644 --- a/Moose Development/Moose/Ops/Intelligence.lua +++ b/Moose Development/Moose/Ops/Intelligence.lua @@ -24,6 +24,8 @@ -- @field #table Contacts Table of detected items. -- @field #table ContactsLost Table of lost detected items. -- @field #table ContactsUnknown Table of new detected items. +-- @field #table Clusters Clusters of detected groups. +-- @field #number clustercounter Running number of clusters. -- @field #number dTforget Time interval in seconds before a known contact which is not detected any more is forgotten. -- @extends Core.Fsm#FSM @@ -48,6 +50,8 @@ INTEL = { Contacts = {}, ContactsLost = {}, ContactsUnknown = {}, + Clusters = {}, + clustercounter = 1, } --- Detected item info. @@ -65,6 +69,15 @@ INTEL = { -- @field #number speed Last known speed. -- @field #number markerID F10 map marker ID. +--- Cluster info. +-- @type INTEL.Cluster +-- @field #number size Number of groups in the cluster. +-- @field #table Contacts Table of contacts in the cluster. +-- @field #number threatlevelMax Max threat level of cluster. +-- @field #number threatlevelSum Sum of threat levels. +-- @field Wrapper.Marker#MARKER marker F10 marker. + + --- INTEL class version. -- @field #string version INTEL.version="0.0.3" @@ -404,40 +417,8 @@ function INTEL:UpdateIntel() end ---- Create detected items. --- @param #INTEL self -function INTEL:PaintPicture() - local contacts={} - for _,_contact in pairs(self.Contacts) do - local contact=_contact --#INTEL.Contact - table.insert(contacts, contact.groupname) - end - - local neighbours={} - for _,_cA in pairs(self.Contacts) do - local cA=_cA --#INTEL.Contact - - neighbours[cA.groupname]={} - - for _,_cB in pairs(self.Contacts) do - local cB=_cB --#INTEL.Contact - - if cA.groupname~=cB.groupname then - - local dist=cA.position:Get2DDistance(cB.position) - - if dist<=10*1000 then - neighbours[cA.groupname]={contactname=cB.groupname, distance=dist} - end - - end - end - end - - -end --- Create detected items. -- @param #INTEL self @@ -517,7 +498,7 @@ function INTEL:CreateDetectedItems(detectedunitset) local group=detectedgroupset:FindGroup(item.groupname) -- Check if deltaT>Tforget. We dont want quick oscillations between detected and undetected states. - if self:CheckContactLost(item) then + if self:_CheckContactLost(item) then -- Trigger LostContact event. This also adds the contact to the self.ContactsLost table. self:LostContact(item) @@ -603,7 +584,7 @@ end -- @param #INTEL self -- @param #INTEL.Contact Contact The contact to be removed. -- @return #boolean If true, contact was not detected for at least *dTforget* seconds. -function INTEL:CheckContactLost(Contact) +function INTEL:_CheckContactLost(Contact) -- Group dead? if Contact.group==nil or not Contact.group:IsAlive() then @@ -634,6 +615,155 @@ function INTEL:CheckContactLost(Contact) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Cluster Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create detected items. +-- @param #INTEL self +function INTEL:PaintPicture() + + local contacts={} + for _,_contact in pairs(self.Contacts) do + local contact=_contact --#INTEL.Contact + if not self:CheckContactInClusters(contact) then + table.insert(contacts, contact.groupname) + end + end + + local contacts={} + for i,_cA in pairs(self.Contacts) do + local cA=_cA --#INTEL.Contact + + if not self:CheckContactInClusters(cA) then + + local n=0 + local neighbours={} + for _,_cB in pairs(self.Contacts) do + local cB=_cB --#INTEL.Contact + + if cA.groupname~=cB.groupname and not self:CheckContactInClusters(cB) then + + local dist=cA.position:Get2DDistance(cB.position) + + if dist<=10*1000 then + n=n+1 + table.insert(neighbours, {name=cB.groupname, distance=dist}) + end + + end + end + + table.insert(contacts, {name=cA.groupname, n=n, neighbours=neighbours}) + + end + end + + -- Sort contacts with respect to number of neighbours. + local function sort(a,b) + return a.n>b.n + end + table.sort(contacts, sort) + + + --- Remove a contact. + local function removecontact(contactname) + for i,contact in pairs(contacts) do + if contact.name==contactname then + table.remove(contacts, i) + end + end + end + + --[[ + env.info("FF before removing neighbours") + for _,contact in pairs(contacts) do + env.info(string.format("Group %s has %d neighbours", contact.name, contact.n)) + end + ]] + + + local function checkcontact(contact) + + for _,c in pairs(contacts) do + if c.name==contact.name then + return true + end + end + return false + end + + for _,contact in pairs(UTILS.DeepCopy(contacts)) do + + if checkcontact(contact) then + + local cluster={} --#INTEL.Cluster + cluster.index=self.clustercounter + + cluster.groups={} + table.insert(cluster.groups, contact.name) + + cluster.Contacts={} + table.insert(cluster.groups, self:GetContactByName(contact.name)) + + local Contact=self:GetContactByName(contact.name) + cluster.coordinate=Contact.position + + cluster.size=1 + for _,neighbour in pairs(contact.neighbours) do + + removecontact(neighbour.name) + + table.insert(cluster.groups, neighbour.name) + cluster.size=cluster.size+1 + + table.insert(cluster.groups, self:GetContactByName(neighbour.name)) + end + + local text=string.format("Cluster #%d. Size %d", cluster.index, cluster.size) + cluster.maker=MARKER:New(cluster.coordinate, text):ToAll() + + -- Add cluster. + table.insert(self.Clusters, cluster) + + -- Increase couter. + self.clustercounter=self.clustercounter+1 + + end + end + + --[[ + table.sort(contacts, sort) + + env.info("FF after removing neighbours") + for i,cluster in pairs(clusters) do + env.info(string.format("Cluster %d has %d groups", i, cluster.n)) + end + ]] + +end + +--- Check if contact is in any known cluster. +-- @param #INTEL self +-- @param #INTEL.Contact contact The contact. +-- @return #boolean If true, contact is in clusters +function INTEL:CheckContactInClusters(contact) + + for _,_cluster in pairs(self.Clusters) do + local cluster=_cluster --#INTEL.Cluster + + for _,_contact in pairs(cluster.Contacts) do + local Contact=_contact --#INTEL.Contact + + if Contact.groupname==contact.groupname then + return true + end + end + end + + return false +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Squadron.lua b/Moose Development/Moose/Ops/Squadron.lua index a118a878b..99ac9c9b1 100644 --- a/Moose Development/Moose/Ops/Squadron.lua +++ b/Moose Development/Moose/Ops/Squadron.lua @@ -250,9 +250,9 @@ end -- @param #nunits Number of units. Must be >=1 and <=4. Default 2. -- @return #SQUADRON self function SQUADRON:SetGrouping(nunits) - self.grouping=nunits or 2 - if self.grouping<1 then self.grouping=1 end - if self.grouping>4 then self.grouping=4 end + self.ngrouping=nunits or 2 + if self.ngrouping<1 then self.ngrouping=1 end + if self.ngrouping>4 then self.ngrouping=4 end return self end diff --git a/Moose Development/Moose/Ops/WingCommander.lua b/Moose Development/Moose/Ops/WingCommander.lua index 55e13e0a5..b6ada11ae 100644 --- a/Moose Development/Moose/Ops/WingCommander.lua +++ b/Moose Development/Moose/Ops/WingCommander.lua @@ -16,13 +16,9 @@ -- @field #string ClassName Name of the class. -- @field #boolean Debug Debug mode. Messages to all about status. -- @field #string lid Class id string for output to DCS log file. --- @field #table airwings Table of airwings. +-- @field #table airwings Table of airwings which are commanded. -- @field #table missionqueue Mission 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. --- @field #string Defcon Defence condition. --- @extends Ops.Intelligence#INTEL +-- @extends Core.Fsm#FSM --- Be surprised! -- @@ -31,7 +27,8 @@ -- ![Banner Image](..\Presentations\WingCommander\WINGCOMMANDER_Main.jpg) -- -- # The WINGCOMMANDER Concept --- +-- +-- A wing commander is the head of airwings. He will find the best AIRWING to perform an assigned AUFTRAG (mission). -- -- -- @field #WINGCOMMANDER @@ -41,25 +38,6 @@ WINGCOMMANDER = { lid = nil, airwings = {}, missionqueue = {}, - borderzoneset = nil, - yellowzoneset = nil, - engagezoneset = nil, -} - ---- Contact details. --- @type WINGCOMMANDER.Contact --- @field Ops.Auftrag#AUFTRAG mission The assigned mission. --- @extends Ops.Intelligence#INTEL.DetectedItem - ---- Defence condition. --- @type WINGCOMMANDER.DEFCON --- @field #string GREEN No enemy activities detected. --- @field #string YELLOW Enemy near our border. --- @field #string RED Enemy within our border. -WINGCOMMANDER.DEFCON = { - GREEN="Green", - YELLOW="Yellow", - RED="Red", } --- WINGCOMMANDER class version. @@ -82,25 +60,11 @@ WINGCOMMANDER.version="0.1.0" --- Create a new WINGCOMMANDER object and start the FSM. -- @param #WINGCOMMANDER 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". -- @return #WINGCOMMANDER self -function WINGCOMMANDER:New(AgentSet, Coalition) - - AgentSet=AgentSet or SET_GROUP:New() +function WINGCOMMANDER:New() -- Inherit everything from INTEL class. - local self=BASE:Inherit(self, INTEL:New(AgentSet, Coalition)) --#WINGCOMMANDER - - -- Set some string id for output to DCS.log file. - --self.lid=string.format("WINGCOMMANDER | ") - - self:SetBorderZones() - self:SetYellowZones() - - self:SetThreatLevelRange() - - self.Defcon=WINGCOMMANDER.DEFCON.GREEN + local self=BASE:Inherit(self, FSM:New()) --#WINGCOMMANDER -- Add FSM transitions. -- From State --> Event --> To State @@ -133,7 +97,7 @@ function WINGCOMMANDER:New(AgentSet, Coalition) -- @function [parent=#WINGCOMMANDER] Status -- @param #WINGCOMMANDER self - --- Triggers the FSM event "SkipperStatus" after a delay. + --- Triggers the FSM event "Status" after a delay. -- @function [parent=#WINGCOMMANDER] __Status -- @param #WINGCOMMANDER self -- @param #number delay Delay in seconds. @@ -156,83 +120,6 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Set this to be an air-to-any dispatcher, i.e. engaging air, ground and naval targets. This is the default anyway. --- @param #WINGCOMMANDER self --- @return #WINGCOMMANDER self -function WINGCOMMANDER:SetAirToAny() - - self:SetFilterCategory({}) - - return self -end - ---- Set this to be an air-to-air dispatcher. --- @param #WINGCOMMANDER self --- @return #WINGCOMMANDER self -function WINGCOMMANDER:SetAirToAir() - - self:SetFilterCategory({Unit.Category.AIRPLANE, Unit.Category.HELICOPTER}) - - return self -end - ---- Set this to be an air-to-ground dispatcher, i.e. engage only ground units --- @param #WINGCOMMANDER self --- @return #WINGCOMMANDER self -function WINGCOMMANDER:SetAirToGround() - - self:SetFilterCategory({Unit.Category.GROUND_UNIT}) - - return self -end - ---- Set this to be an air-to-sea dispatcher, i.e. engage only naval units. --- @param #WINGCOMMANDER self --- @return #WINGCOMMANDER self -function WINGCOMMANDER:SetAirToSea() - - self:SetFilterCategory({Unit.Category.SHIP}) - - return self -end - ---- Set this to be an air-to-surface dispatcher, i.e. engaging ground and naval groups. --- @param #WINGCOMMANDER self --- @return #WINGCOMMANDER self -function WINGCOMMANDER:SetAirToSurface() - - self:SetFilterCategory({Unit.Category.GROUND_UNIT, Unit.Category.SHIP}) - - return self -end - ---- Set a threat level range that will be engaged. Threat level is a number between 0 and 10, where 10 is a very dangerous threat. --- Targets with threat level 0 are usually harmless. --- @param #WINGCOMMANDER self --- @param #number ThreatLevelMin Min threat level. Default 1. --- @param #number ThreatLevelMax Max threat level. Default 10. --- @return #WINGCOMMANDER self -function WINGCOMMANDER:SetThreatLevelRange(ThreatLevelMin, ThreatLevelMax) - - self.threatLevelMin=ThreatLevelMin or 1 - self.threatLevelMax=ThreatLevelMax or 10 - - return self -end - ---- Set defence condition. --- @param #WINGCOMMANDER self --- @param #string Defcon Defence condition. See @{#WINGCOMMANDER.DEFCON}, e.g. `WINGCOMMANDER.DEFCON.RED`. --- @return #WINGCOMMANDER self -function WINGCOMMANDER:SetDefcon(Defcon) - - self.Defcon=Defcon - --self:Defcon(Defcon) - - return self -end - - --- Add an airwing to the wingcommander. -- @param #WINGCOMMANDER self -- @param Ops.AirWing#AIRWING Airwing The airwing to add. @@ -280,57 +167,6 @@ function WINGCOMMANDER:RemoveMission(Mission) return self end ---- Set border zone set. --- @param #WINGCOMMANDER self --- @param Core.Set#SET_ZONE BorderZoneSet Set of zones, defining our borders. --- @return #WINGCOMMANDER self -function WINGCOMMANDER:SetBorderZones(BorderZoneSet) - - -- Border zones. - self.borderzoneset=BorderZoneSet or SET_ZONE:New() - - return self -end - ---- Add a zone defining your territory. --- @param #WINGCOMMANDER self --- @param Core.Zone#ZONE BorderZone The zone defining the border of your territory. --- @return #WINGCOMMANDER self -function WINGCOMMANDER:AddBorderZone(BorderZone) - - -- Add a border zone. - self.borderzoneset:AddZone(BorderZone) - - -- Set accept zone. - --self:AddAcceptZone(BorderZone) - - return self -end - ---- Set yellow zone set. Detected enemy troops in this zone will trigger defence condition YELLOW. --- @param #WINGCOMMANDER self --- @param Core.Set#SET_ZONE YellowZoneSet Set of zones, defining our borders. --- @return #WINGCOMMANDER self -function WINGCOMMANDER:SetYellowZones(YellowZoneSet) - - -- Border zones. - self.yellowzoneset=YellowZoneSet or SET_ZONE:New() - - return self -end - ---- Add a zone defining an area outside your territory that is monitored for enemy activity. --- @param #WINGCOMMANDER self --- @param Core.Zone#ZONE YellowZone The zone defining the border of your territory. --- @return #WINGCOMMANDER self -function WINGCOMMANDER:AddYellowZone(YellowZone) - - -- Add a border zone. - self.yellowzoneset:AddZone(YellowZone) - - return self -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -374,107 +210,9 @@ function WINGCOMMANDER:onafterStatus(From, Event, To) -- FSM state. local fsmstate=self:GetState() - - -- Clean up missions where the contact was lost. - for _,_contact in pairs(self.ContactsLost) do - local contact=_contact --#WINGCOMMANDER.Contact - - if contact.mission and contact.mission:IsNotOver() then - - local text=string.format("Lost contact to target %s! %s mission %s will be cancelled.", contact.groupname, contact.mission.type:upper(), contact.mission.name) - MESSAGE:New(text, 120, "WINGCOMMANDER"):ToAll() - self:I(self.lid..text) - - -- Cancel this mission. - contact.mission:Cancel() - - end - - end - - -- Create missions for all new contacts. - local Nred=0 - local Nyellow=0 - local Nengage=0 - for _,_contact in pairs(self.Contacts) do - local contact=_contact --#WINGCOMMANDER.Contact - local group=contact.group --Wrapper.Group#GROUP - - local inred=self:CheckGroupInBorder(group) - if inred then - Nred=Nred+1 - end - - local inyellow=self:CheckGroupInYellow(group) - if inyellow then - Nyellow=Nyellow+1 - end - - -- Is this a threat? - local threat=contact.threatlevel>=self.threatLevelMin and contact.threatlevel<=self.threatLevelMax - - local redalert=true - if self.borderzoneset:Count()>0 then - redalert=inred - end - - if redalert and threat and not contact.mission then - - -- Create a mission based on group category. - local mission=AUFTRAG:NewAUTO(group) - - - -- Add mission to queue. - if mission then - - --TODO: Better amount of necessary assets. Count units in asset and in contact. Might need nassetMin/Max. - mission.nassets=1 - - -- Missons are repeated max 3 times on failure. - mission.missionRepeatMax=3 - - -- Set mission contact. - contact.mission=mission - - -- Add mission to queue. - self:AddMission(mission) - end - - end - - end - - -- Set defcon. - -- TODO: Need to introduce time check to avoid fast oscillation between different defcon states in case groups move in and out of the zones. - if Nred>0 then - self:SetDefcon(WINGCOMMANDER.DEFCON.RED) - elseif Nyellow>0 then - self:SetDefcon(WINGCOMMANDER.DEFCON.YELLOW) - else - self:SetDefcon(WINGCOMMANDER.DEFCON.GREEN) - end - - -- Check mission queue and assign one PLANNED mission. self:CheckMissionQueue() - local text=string.format("Defcon=%s Missions=%d Contacts: Total=%d Yellow=%d Red=%d", self.Defcon, #self.missionqueue, #self.Contacts, Nyellow, Nred) - self:I(self.lid..text) - - -- Infor about contacts. - if #self.Contacts>0 then - local text="Contacts:" - for i,_contact in pairs(self.Contacts) do - local contact=_contact --#WINGCOMMANDER.Contact - local mtext="N/A" - if contact.mission then - mtext=string.format("Mission %s (%s) %s", contact.mission.name, contact.mission.type, contact.mission.status:upper()) - end - text=text..string.format("\n[%d] %s Type=%s (%s): Threat=%d Mission=%s", i, contact.groupname, contact.categoryname, contact.typename, contact.threatlevel, mtext) - end - self:I(self.lid..text) - end - -- Mission queue. if #self.missionqueue>0 then local text="Mission queue:" @@ -534,50 +272,6 @@ function WINGCOMMANDER:onafterCancelMission(From, Event, To, Mission) end ---- On before "Defcon" event. --- @param #WINGCOMMANDER self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param #string Defcon New defence condition. --- @param Ops.Auftrag#AUFTRAG Mission The mission. -function WINGCOMMANDER:onbeforeDefcon(From, Event, To, Defcon) - - local gotit=false - for _,defcon in pairs(WINGCOMMANDER.DEFCON) do - if defcon==Defcon then - gotit=true - end - end - - if not gotit then - self:E(self.lid..string.format("ERROR: Unknown DEFCON specified! Dont know defcon=%s", tostring(Defcon))) - return false - end - - -- Defcon did not change. - if Defcon==self.Defcon then - self:I(self.lid..string.format("Defcon %s unchanged. No processing transition.", tostring(Defcon))) - return false - end - - return true -end - ---- On after "Defcon" event. --- @param #WINGCOMMANDER self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param #string Defcon New defence condition. --- @param Ops.Auftrag#AUFTRAG Mission The mission. -function WINGCOMMANDER:onafterDefcon(From, Event, To, Defcon) - self:I(self.lid..string.format("Changing Defcon from %s --> %s", self.Defcon, Defcon)) - - -- Set new defcon. - self.Defcon=Defcon -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Resources ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -684,72 +378,6 @@ function WINGCOMMANDER:GetAirwingForMission(Mission) return nil end ---- Check if group is inside our border. --- @param #WINGCOMMANDER self --- @param Wrapper.Group#GROUP group The group. --- @return #boolean If true, group is in any zone. -function WINGCOMMANDER:CheckGroupInBorder(group) - - local inside=self:CheckGroupInZones(group, self.borderzoneset) - - return inside -end - ---- Check if group is near our border (yellow zone). --- @param #WINGCOMMANDER self --- @param Wrapper.Group#GROUP group The group. --- @return #boolean If true, group is in any zone. -function WINGCOMMANDER:CheckGroupInYellow(group) - - -- Check inside yellow but not inside our border. - local inside=self:CheckGroupInZones(group, self.yellowzoneset) and not self:CheckGroupInZones(group, self.borderzoneset) - - return inside -end - ---- Check if group is inside a zone. --- @param #WINGCOMMANDER self --- @param Wrapper.Group#GROUP group The group. --- @param Core.Set#SET_ZONE zoneset Set of zones. --- @return #boolean If true, group is in any zone. -function WINGCOMMANDER:CheckGroupInZones(group, zoneset) - - for _,_zone in pairs(zoneset.Set or {}) do - local zone=_zone --Core.Zone#ZONE - - if group:IsPartlyOrCompletelyInZone(zone) then - return true - end - end - - return false -end - ---- Check resources. --- @param #WINGCOMMANDER self --- @return #table -function WINGCOMMANDER:CheckResources() - - local capabilities={} - - for _,MissionType in pairs(AUFTRAG.Type) do - capabilities[MissionType]=0 - - 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 - - end - - return capabilities -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 4992f7f97..36f5f604a 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -73,6 +73,7 @@ Ops/Squadron.lua Ops/AirWing.lua Ops/Intelligence.lua Ops/WingCommander.lua +Ops/ChiefOfStaff.lua AI/AI_Balancer.lua AI/AI_Air.lua