From ef65f5a353ad85ede6850d6a42ecf5ba724c3bc8 Mon Sep 17 00:00:00 2001 From: dutchie031 Date: Thu, 12 Jun 2025 22:40:55 +0200 Subject: [PATCH] Sweep cap (#54) * Added initial implementation of Sweep groups * Added initial implementation of Sweep groups * Added initial implementation of Sweep groups * Added initial implementation of Sweep groups * fixed issues when testing * Intercept cap (#55) * First intial WIP for intercept units * First intial WIP for intercept units * major updates --------- Co-authored-by: ex61wi Co-authored-by: dutchie032 --------- Co-authored-by: ex61wi Co-authored-by: dutchie032 --- .vscode/settings.json | 3 +- DcsTypes.lua | 16 + _docs/index.html | 36 +- _docs/pages/getstarted.html | 7 - _docs/pages/persistence.html | 108 ++--- _docs/pages/reference.html | 74 +--- _docs/pages/spearheadapi.html | 40 +- _docs/pages/tutorials.html | 53 +-- _docs/style/style.css | 136 ++++-- _miz/SPEARHEAD_CAP_DEV.miz | Bin 30888 -> 32147 bytes classes/capClasses/CapAirbase.lua | 181 +++++++- classes/capClasses/GlobalCapManager.lua | 5 +- classes/capClasses/airGroups/AirGroup.lua | 15 + classes/capClasses/airGroups/CapGroup.lua | 89 ++-- .../capClasses/airGroups/InterceptGroup.lua | 358 ++++++++++++++++ classes/capClasses/airGroups/SweepGroup.lua | 120 ++++++ .../capClasses/detection/DetectionManager.lua | 168 ++++++++ classes/capClasses/taskings/INTERCEPT.lua | 399 ++++++++++++++++++ classes/capClasses/taskings/SWEEP.lua | 287 +++++++++++++ classes/spearhead_db.lua | 77 +++- dev/dev_classes.lua | 5 + main.lua | 10 +- 22 files changed, 1834 insertions(+), 353 deletions(-) delete mode 100644 _docs/pages/getstarted.html create mode 100644 classes/capClasses/airGroups/InterceptGroup.lua create mode 100644 classes/capClasses/airGroups/SweepGroup.lua create mode 100644 classes/capClasses/detection/DetectionManager.lua create mode 100644 classes/capClasses/taskings/INTERCEPT.lua create mode 100644 classes/capClasses/taskings/SWEEP.lua diff --git a/.vscode/settings.json b/.vscode/settings.json index 75d8e18..91fbe2b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,5 +15,6 @@ "foldStartRegex": "[\\s]*--region[\\s]*(.*)", "disableFolding": false } - } + }, + "livePreview.serverRoot": "/_docs" } \ No newline at end of file diff --git a/DcsTypes.lua b/DcsTypes.lua index 1bf8544..e9912dc 100644 --- a/DcsTypes.lua +++ b/DcsTypes.lua @@ -544,8 +544,24 @@ do -- Controller ---@class Controller ---@field setTask fun(self:Controller, task: Task) Sets the task of the controller to the passed task ---@field setCommand function + ---@field getDetectedTargets fun(self:Controller, type1: DetectionType?, type2: DetectionType?, type3: DetectionType?) : Array Returns a list of units that are detected by the controller.
Only works for unit controllers, not group controllers. Controller = Controller + ---@enum DetectionType + Controller.Detection = { + VISUAL = 1, + OPTIC = 2, + RADAR = 4, + IRST = 8, + RWR = 16, + DLINK = 32 + } + + ---@class DetectedObject + ---@field object Object + ---@field visible boolean + ---@field type boolean + ---@field distance boolean ---@alias TaskType ---| "NoTask" diff --git a/_docs/index.html b/_docs/index.html index 9ec9fab..1c4e5da 100644 --- a/_docs/index.html +++ b/_docs/index.html @@ -5,46 +5,26 @@ Spearhead + + +
-
- -
- - -
-
+
diff --git a/_docs/pages/getstarted.html b/_docs/pages/getstarted.html deleted file mode 100644 index 4028ac7..0000000 --- a/_docs/pages/getstarted.html +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/_docs/pages/persistence.html b/_docs/pages/persistence.html index 5b5f5f9..164bd69 100644 --- a/_docs/pages/persistence.html +++ b/_docs/pages/persistence.html @@ -4,69 +4,36 @@ - Spearhead Persistence - - + Spearhead Persistence + + + +
-
- -
- - -
-
-
-
+ +
- +

Persistence

- Spearhead comes with a custom Persistence option.
- It will even save burned out vehicles to give players a consistent battlefield even after the restart.
+ Spearhead comes with a custom Persistence option.
+ It will even save burned out vehicles to give players a consistent battlefield even after the + restart.

- Most is pretty straightforward, however, some zones are somewhat special.
+ Most is pretty straightforward, however, some zones are somewhat special.
Underneath you'll see all special zones listed.

@@ -77,9 +44,10 @@

Feedback

- Since this feature is still very much in development, please let any issues be known as soon as possible and as concise as possible so a fix can be made quickly.
- Currently implemented is local file storage.
- If enough interest is expressed, cloud-based persistence would be possible.
+ Since this feature is still very much in development, please let any issues be known as soon as + possible and as concise as possible so a fix can be made quickly.
+ Currently implemented is local file storage.
+ If enough interest is expressed, cloud-based persistence would be possible.

@@ -102,40 +70,46 @@
 
                 

Basic Behavior

- While playing the mission, Spearhead is keeping track of all units killed.
- These units are stored in memory internally and written to file.
- This happens every 2 minutes AND during the "onMissionStop" event to make sure the mission is as up-to-date as possible without having to call IO methods on each event.
+ While playing the mission, Spearhead is keeping track of all units killed.
+ These units are stored in memory internally and written to file.
+ This happens every 2 minutes AND during the "onMissionStop" event to make sure the mission is as + up-to-date as possible without having to call IO methods on each event.

Misc Units

- Miscellaneous units will follow basic behavior.
- These are units that are part of a stage but are not in a mission or airbase.
- These units will be replaced by a static "DEAD" unit after a mission restart at the location it was killed.
- Due to blue units spawning afterwards, it's generally best to not have these units move through or over areas where BLUESAMS and Airbase units will spawn after a stage completion. + Miscellaneous units will follow basic behavior.
+ These are units that are part of a stage but are not in a mission or airbase.
+ These units will be replaced by a static "DEAD" unit after a mission restart at the location it was + killed.
+ Due to blue units spawning afterwards, it's generally best to not have these units move through or + over areas where BLUESAMS and Airbase units will spawn after a stage completion.

Missions

- Missions follow the same logic as Misc Units.
+ Missions follow the same logic as Misc Units.

Blue SAMs

- For Blue SAMs, due to placements easily overlapping between red and blue units within a BLUESAM trigger zone, red units that overlap with blue units will be deleted.
- This will ensure that the blue units are placed as needed.
+ For Blue SAMs, due to placements easily overlapping between red and blue units within a BLUESAM + trigger zone, red units that overlap with blue units will be deleted.
+ This will ensure that the blue units are placed as needed.

Airbases

- Airbase units will also be checked for overlap. As the blue units will be spawned after the RED unit.
- Units that were alive when the stage was completed will be removed. Units that died will have corpses spawned.
+ Airbase units will also be checked for overlap. As the blue units will be spawned after the RED + unit.
+ Units that were alive when the stage was completed will be removed. Units that died will have + corpses spawned.

Warehouses

- Currently, warehouses are not implemented and therefore warehouses are not persisted.
- When supply missions and logistics get implemented, warehouses will be persisted as well.
+ Currently, warehouses are not implemented and therefore warehouses are not persisted.
+ When supply missions and logistics get implemented, warehouses will be persisted as well.

@@ -146,4 +120,4 @@ -``` \ No newline at end of file +``` \ No newline at end of file diff --git a/_docs/pages/reference.html b/_docs/pages/reference.html index 15b9d1c..827821d 100644 --- a/_docs/pages/reference.html +++ b/_docs/pages/reference.html @@ -4,7 +4,8 @@ - Spearhead Reference + Spearhead Reference + + +
-
- -
- - -
-
-
+ +
- +

Reference

diff --git a/_docs/pages/spearheadapi.html b/_docs/pages/spearheadapi.html index eff1e33..2bded0c 100644 --- a/_docs/pages/spearheadapi.html +++ b/_docs/pages/spearheadapi.html @@ -5,46 +5,18 @@ Spearhead API + - + +
-
- -
- - -
-
-
-
+ +
-
-

- -
+

Spearhead API

diff --git a/_docs/pages/tutorials.html b/_docs/pages/tutorials.html index b3a5080..b3df258 100644 --- a/_docs/pages/tutorials.html +++ b/_docs/pages/tutorials.html @@ -5,65 +5,24 @@ Spearhead Tutorials + +
-
- -
- - -
-
-
-
+ +
- +

Tutorials

diff --git a/_docs/style/style.css b/_docs/style/style.css index 7d12d4a..5a6858e 100644 --- a/_docs/style/style.css +++ b/_docs/style/style.css @@ -1,16 +1,17 @@ + :root { /* Dark Theme (Default) - Slightly lighter */ - --color-bg-primary: #0a0b10; /* Much darker primary background */ - --color-bg-secondary: #13141a; /* Darker secondary background */ - --color-bg-tertiary: #181a22; /* Darker tertiary */ - --color-bg-header: #090a0f; /* Darker header */ + --color-bg-primary: #1b1d27; /* Much darker primary background */ + --color-bg-secondary: #2d303f; /* Darker secondary background */ + --color-bg-tertiary: #2c3041; /* Darker tertiary */ + --color-bg-header: #10121b; /* Darker header */ --color-bg-code: #10121a; /* Almost black code block background */ --color-bg-note: rgba(30, 32, 40, 0.85); /* Slightly darker note background */ --color-bg-hover: rgba(120, 140, 255, 0.12); /* More visible hover */ --color-bg-inline-lua: #181a22; /* Match code block background */ --color-bg-note-dark: rgba(40, 35, 30, 0.85); /* Slightly darker note box */ - --color-text-primary: #f6f8fa; /* Brighter text */ + --color-text-primary: #a5a5a5; /* Brighter text */ --color-text-secondary: #bfc8d8; /* Lighter secondary text */ --color-text-accent: #7ecbff; /* Brighter accent */ --color-text-light: #ffffff; @@ -32,10 +33,11 @@ /* Shadow and transparency values - unchanged */ --shadow-small: 0 2px 5px rgba(0, 0, 0, 0.3); --shadow-medium: 0 3px 8px rgba(0, 0, 0, 0.5); - --shadow-large: 0 4px 12px rgba(0, 0, 0, 0.6); - - /* Common opacity values */ - --opacity-sidenav: 0.85; /* Increased opacity for better contrast */ + --shadow-large: 0 4px 12px rgba(0, 0, 0, 0.6); /* Common opacity values */ + --opacity-sidenav: 0.85; /* Increased opacity for better contrast */ /* Font families with improved Inter setup */ + --font-family-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-family-code: 'JetBrains Mono', 'Monaco', 'Cascadia Code', monospace; + --font-family-heading: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; /* Responsive margins - unchanged */ --margin-left: 20%; @@ -58,17 +60,23 @@ --color-bg-header-accent: #2a2620; --color-logo-gradient: linear-gradient(120deg, #5caaff, #a0e8ff, #ffffff); --color-nav-underline: #b0a890; - --color-nav-underline-glow: rgba(176, 168, 144, 0.6); - --color-nav-hover-bg: var(--color-bg-hover); + --color-nav-underline-glow: rgba(176, 168, 144, 0.6); --color-nav-hover-bg: var(--color-bg-hover); --color-nav-hover-text: #ffffff; --color-nav-hover-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + + /* Heading colors */ + --color-heading-h1: #8fc6ff; + --color-heading-h2: #b3e0ff; + --color-heading-h3: #c9e6ff; + --color-heading-h1-border: #8fc6ff; + --color-heading-h2-border: #b3e0ff; } [data-theme="light"] { - --color-bg-primary: #f5f7ff; - --color-bg-secondary: #e8ecf7; - --color-bg-tertiary: #dce2f0; - --color-bg-header: #f0f2fa; + --color-bg-primary: #e0e0e0; + --color-bg-secondary: #b6b6b6; + --color-bg-tertiary: #b6b6b6; + --color-bg-header: #d3d3d3; --color-bg-code: #e3e6eb; /* Medium-light gray for code block background */ --color-bg-note: rgba(215, 228, 245, 0.75); --color-bg-hover: rgba(100, 120, 200, 0.1); @@ -80,8 +88,8 @@ --color-text-accent: #3050b0; --color-text-light: #2a2e40; - --color-border-primary: #a0a8b8; - --color-border-accent: #5c75d9; + --color-border-primary: #496cb3; + --color-border-accent: #1d46ee; --color-link: #3050b0; --color-link-hover: #4060c0; @@ -117,10 +125,16 @@ --color-bg-header-accent: #f5e9d2; --color-logo-gradient: linear-gradient(120deg, #225a9e, #3578c7, #70aaff); --color-nav-underline: #e0cfa0; - --color-nav-underline-glow: rgba(224, 207, 160, 0.4); - --color-nav-hover-bg: var(--color-bg-hover); + --color-nav-underline-glow: rgba(224, 207, 160, 0.4); --color-nav-hover-bg: var(--color-bg-hover); --color-nav-hover-text: var(--color-text-primary); --color-nav-hover-shadow: 0 4px 8px rgba(48, 80, 176, 0.15); + + /* Heading colors for light theme */ + --color-heading-h1: #225a9e; + --color-heading-h2: #3578c7; + --color-heading-h3: #4a90e2; + --color-heading-h1-border: #225a9e; + --color-heading-h2-border: #3578c7; } html { @@ -131,14 +145,17 @@ html { body { color: var(--color-text-primary); - font-family: Arial, sans-serif; + font-family: var(--font-family-primary); margin: 0; padding: 0; height: 100%; display: flex; flex-direction: column; font-size: 16px; - line-height: 1.6; + line-height: 1.65; /* Increased for better Inter readability */ + font-weight: 500; /* Medium weight for substantial feel */ + letter-spacing: 0.015em; /* Slightly more spacing for better readability */ + font-feature-settings: "cv02", "cv03", "cv04", "cv11"; /* Better Inter alternates */ transition: background-color 0.3s ease, color 0.3s ease; } @@ -243,6 +260,35 @@ header nav a:hover::after { width: 80%; } +.dropdown { + position: relative; + display: flex; + flex-wrap: wrap; + flex-direction: column; +} + +.dropdown-content { + display: flex; + flex-direction: column; + position: absolute; + overflow: hidden; + top: 100%; /* Position below the dropdown */ + left: 0; /* Align left edge */ + min-width: 160px; + + background-color: var(--color-bg-secondary); + box-shadow: var(--shadow-medium); + z-index: 1; + border-radius: 8px; + + max-height: 0px; + transition: all 1.5s ease; +} + +.dropdown:hover .dropdown-content { + max-height: 200px; +} + header.collapsible { cursor: pointer; position: relative; @@ -292,13 +338,13 @@ main { /* Side navigation styles */ .reference-container { - display: flex; - align-items: flex-start; + display: block; /* Change from flex since sidebar is now fixed */ margin-top: 2em; margin-left: var(--margin-left); - flex-direction: row; - gap: 2em; position: relative; + /* Ensure the container doesn't interfere with sticky positioning */ + overflow: visible; + height: auto; } .side-nav { @@ -307,9 +353,14 @@ main { padding-left: 0; overflow-y: auto; border-right: none; - position: sticky; - top: 2em; - align-self: flex-start; + position: fixed; + top: 7em; /* Account for header height (5em) + spacing (2em) */ + left: var(--margin-left); + max-height: calc(100vh - 9em); /* Account for header + top spacing */ + z-index: 10; + /* Ensure proper stacking context */ + transform: translateZ(0); + will-change: transform; } .side-nav h4 { @@ -366,7 +417,7 @@ main { .content-wrapper { flex: 1; max-width: 70%; - margin-left: 0; + margin-left: 220px; /* Account for fixed sidebar width + gap */ border-left: 2px solid var(--color-border-accent); padding-left: 1.5em; padding-bottom: 20vh; /* Allow last section to scroll to top */ @@ -377,33 +428,34 @@ h1, h2, h3, h4, h5, h6 { margin-top: 1.5em; margin-bottom: 0.8em; color: var(--color-text-secondary); + font-family: var(--font-family-heading); font-weight: 600; line-height: 1.3; } h1 { font-size: 2.2em; - color: #8fc6ff; + color: var(--color-heading-h1); font-weight: 700; text-shadow: 0 1px 4px rgba(143,198,255,0.08); letter-spacing: 0.01em; - border-bottom: 1.5px solid #8fc6ff; + border-bottom: 1.5px solid var(--color-heading-h1-border); padding-bottom: 0.3em; } h2 { font-size: 1.8em; - color: #b3e0ff; + color: var(--color-heading-h2); font-weight: 600; text-shadow: 0 1px 2px rgba(179,224,255,0.07); letter-spacing: 0.005em; - border-bottom: 1px solid #b3e0ff; + border-bottom: 1px solid var(--color-heading-h2-border); padding-bottom: 0.2em; } h3 { font-size: 1.4em; - color: #c9e6ff; + color: var(--color-heading-h3); font-weight: 600; text-shadow: 0 1px 1px rgba(201,230,255,0.06); letter-spacing: 0.005em; @@ -455,7 +507,7 @@ th { .inline-lua { background-color: var(--color-bg-inline-lua); border-radius: 3px; - font-family: 'Courier New', monospace; + font-family: var(--font-family-code); white-space: nowrap; padding: 0.1em 0.3em; color: #23272e; @@ -469,7 +521,7 @@ pre { padding: 1.2em; margin: 1.5em 0; overflow-x: auto; - font-family: 'Courier New', monospace; + font-family: var(--font-family-code); line-height: 1.4; box-shadow: inset 0 1px 8px rgba(0, 0, 0, 0.08); color: #23272e; @@ -590,6 +642,10 @@ img { } .content-wrapper { margin-left: 0; + max-width: 100%; + } + .reference-container { + margin-left: var(--margin-small); } } @@ -605,6 +661,12 @@ img { .content-wrapper { width: 100%; /* Make content take full width */ + margin-left: 0; + max-width: 100%; + } + + .reference-container { + margin-left: 1em; } main { @@ -667,4 +729,4 @@ img { .theme-toggle:hover svg { fill: var(--color-text-accent); -} \ No newline at end of file +} diff --git a/_miz/SPEARHEAD_CAP_DEV.miz b/_miz/SPEARHEAD_CAP_DEV.miz index 0fcebb3b3df10155beffd4ba2a4ce3245d34cac6..6341a69ef03802e85eba3859b63276ec59884411 100644 GIT binary patch delta 8035 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 bSa^BZs4JnoM))@v|8@ik0b%-|g@Ev1$jJXg diff --git a/classes/capClasses/CapAirbase.lua b/classes/capClasses/CapAirbase.lua index f8a82d5..68bae5b 100644 --- a/classes/capClasses/CapAirbase.lua +++ b/classes/capClasses/CapAirbase.lua @@ -2,16 +2,25 @@ ---@field private airbaseName string ---@field private logger table ---@field private database Database +---@field private detectionManager DetectionManager ---@field private activeStage number ---@field private capConfig table ---@field private capGroupsByName table +---@field private sweepGroupsByName table +---@field private interceptGroupsByName table ---@field private runwayBombingTracker RunwayBombingTracker ---@field private runwayStrikeMissions table local CapBase = {} +---comment +---@param self CapBase +---@param time number +---@return number? local CheckStateContinuous = function(self, time) self:CheckAndScheduleCAP() + self:CheckAndScheduleSweep() + self:CheckAndScheduleIntercept() return time + 15 end @@ -22,8 +31,9 @@ end ---@param capConfig table ---@param stageConfig table ---@param runwayBombingTracker RunwayBombingTracker +---@param detectionManager DetectionManager ---@return CapBase -function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runwayBombingTracker) +function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runwayBombingTracker, detectionManager) CapBase.__index = CapBase local self = setmetatable({}, { __index = CapBase }) --[[@as CapBase]] @@ -36,6 +46,9 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runw self.capConfig = capConfig self.database = database self.capGroupsByName = {} + self.sweepGroupsByName = {} + self.interceptGroupsByName = {} + self.detectionManager = detectionManager local baseData = database:getAirbaseDataForZone(airbaseName) if baseData and baseData.CapGroups then @@ -47,14 +60,36 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runw end end - logger:info("Airbase with name '" .. - airbaseName .. "' has a total of " .. Spearhead.Util.tableLength(self.capGroupsByName) .. " cap flights registered") + if baseData and baseData.SweepGroups then + for key, name in pairs(baseData.SweepGroups) do + local sweepGroup = Spearhead.classes.capClasses.airGroups.SweepGroup.New(name, capConfig, logger) + if sweepGroup then + self.sweepGroupsByName[name] = sweepGroup + end + end + end + + if baseData and baseData.InterceptGroups then + for key, name in pairs(baseData.InterceptGroups) do + local interceptGroup = Spearhead.classes.capClasses.airGroups.InterceptGroup.New(name, capConfig, logger, detectionManager) + if interceptGroup then + self.interceptGroupsByName[name] = interceptGroup + end + end + end + + local capFlights = Spearhead.Util.tableLength(self.capGroupsByName) + local sweepFlights = Spearhead.Util.tableLength(self.sweepGroupsByName) + local interceptFlights = Spearhead.Util.tableLength(self.interceptGroupsByName) + + logger:info(airbaseName .. " : " .. capFlights .." CAP | " .. sweepFlights .. " SWEEP | " .. interceptFlights .. " INTERCEPT") self:CreateRunwayStrikeMission(database) Spearhead.Events.AddStageNumberChangedListener(self) timer.scheduleFunction(CheckStateContinuous, self, timer.getTime() + 15) + return self end @@ -91,6 +126,22 @@ function CapBase:SpawnIfApplicable() capGroup:Spawn() end end + + for groupName, sweepGroup in pairs(self.sweepGroupsByName) do + local targetStage = sweepGroup:GetZoneIDWhenStageID(tostring(self.activeStage)) + + if targetStage ~= nil and sweepGroup:GetState() == "UnSpawned" then + sweepGroup:Spawn() + end + end + + for groupName, interceptGroup in pairs(self.interceptGroupsByName) do + local targetStage = interceptGroup:GetZoneIDWhenStageID(tostring(self.activeStage)) + + if targetStage ~= nil and interceptGroup:GetState() == "UnSpawned" then + interceptGroup:Spawn() + end + end end function CapBase:CheckAndScheduleCAP() @@ -223,6 +274,116 @@ function CapBase:CheckAndScheduleCAP() end end +function CapBase:CheckAndScheduleSweep() + self.logger:debug("Check sweep taskings for airbase " .. self.airbaseName) + + local airbase = Airbase.getByName(self.airbaseName) + if not airbase then + return nil + end + + local activeStageID = tostring(self.activeStage) + + for _, group in pairs(self.sweepGroupsByName) do + local targetZoneID = group:GetZoneIDWhenStageID(activeStageID) + if targetZoneID then + local triggerZone = self.database:GetCapZoneForZoneID(targetZoneID) + if triggerZone then + if group:GetState() == "ReadyOnTheRamp" then + group:SendToZone(triggerZone, targetZoneID, airbase) + end + else + self.logger:debug("SweepGroup " .. group:GetName() .. " has no trigger zone for stage " .. activeStageID) + group:SendRTB(airbase) + end + end + end +end + +function CapBase:CheckAndScheduleIntercept() + + self.logger:debug("Check intercept taskings for airbase " .. self.airbaseName) + + local interceptZoneIDs = {} + + local airbase = Airbase.getByName(self.airbaseName) + if not airbase then + return nil + end + + for name, group in pairs(self.interceptGroupsByName) do + local targetZoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage)) + if targetZoneID then + interceptZoneIDs[targetZoneID] = true + end + end + + ---@type table> + local unitsToInterceptPerZone = {} + + local detectedUnits = self.detectionManager:GetDetectedUnitsBy(coalition.side.RED) + for targetZoneID, _ in pairs(interceptZoneIDs) do + local zones = self.database:GetInterceptZonesForZoneID(targetZoneID) + if zones then + for _, zone in pairs(zones) do + + self.logger:debug("Check intercept zone " .. zone.name .. " for airbase " .. self.airbaseName) + + for _, unitName in pairs(detectedUnits) do + self.logger:debug("Check unit " .. unitName .. " for intercept in zone " .. zone.name) + + local unit = Unit.getByName(unitName) + if unit and unit:isExist() then + local unitPos = unit:getPoint() + if Spearhead.Util.is3dPointInZone(unitPos, zone) == true then + if not unitsToInterceptPerZone[zone.name] then + unitsToInterceptPerZone[zone.name] = {} + end + + table.insert(unitsToInterceptPerZone[zone.name], unitName) + end + end + end + end + end + end + + ---RATIO. Amount of enemy fighters required per zone before another group gets added. + local ratio = 4 -- Ratio of units to intercept per zone, can be adjusted + + for name, targets in pairs(unitsToInterceptPerZone) do + local required = 0 + local nrTargets = Spearhead.Util.tableLength(targets) + if targets and nrTargets > 0 then + required = math.ceil(nrTargets / ratio) + end + + local total = 0 + for _, group in pairs(self.interceptGroupsByName) do + if group:GetState() == "OnStation" and group:GetCurrentTargetZone() == name then + group:SetTargetUnits(targets) + total = total + 1 + end + end + + if total < required then + for _, group in pairs(self.interceptGroupsByName) do + if total < required then + local zoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage)) + if group:GetState() == "ReadyOnTheRamp" then + group:SendToInterceptUnits(targets, name, airbase) + total = total + 1 + + self.logger:debug("Intercept group " .. group:GetName() .. " sent to intercept zone " .. name ) + elseif group:GetState() == "InTransit" or group:GetState() == "OnStation" then + group:SetTargetUnits(targets) + end + end + end + end + end + +end function CapBase:OnStageNumberChanged(number) self.activeStage = number @@ -243,6 +404,20 @@ function CapBase:IsBaseActiveWhenStageIsActive(stageNumber) return true end end + + for _, group in pairs(self.sweepGroupsByName) do + local target = group:GetZoneIDWhenStageID(tostring(stageNumber)) + if target ~= nil then + return true + end + end + + for _, group in pairs(self.interceptGroupsByName) do + local target = group:GetZoneIDWhenStageID(tostring(stageNumber)) + if target ~= nil then + return true + end + end return false end diff --git a/classes/capClasses/GlobalCapManager.lua b/classes/capClasses/GlobalCapManager.lua index da61669..44ae1dc 100644 --- a/classes/capClasses/GlobalCapManager.lua +++ b/classes/capClasses/GlobalCapManager.lua @@ -11,8 +11,9 @@ do ---@param database Database ---@param capConfig table ---@param stageConfig StageConfig + ---@param detectionManager DetectionManager ---@param logLevel LogLevel - function GlobalCapManager.start(database, capConfig, stageConfig, logLevel) + function GlobalCapManager.start(database, capConfig, detectionManager, stageConfig, logLevel) if initiated == true then return end local logger = Spearhead.LoggerTemplate.new("AirbaseManager", logLevel) @@ -31,7 +32,7 @@ do for _, airbaseName in pairs(airbaseNames) do if airbaseName then local airbaseSpecificLogger = Spearhead.LoggerTemplate.new("CAP_" .. airbaseName, logLevel) - local airbase = Spearhead.classes.capClasses.CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, stageConfig, runwayBombingTracker) + local airbase = Spearhead.classes.capClasses.CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, stageConfig, runwayBombingTracker, detectionManager) if airbase then table.insert(airbasesPerStage[stageName], airbase) allAirbasesByName[airbaseName] = airbase diff --git a/classes/capClasses/airGroups/AirGroup.lua b/classes/capClasses/airGroups/AirGroup.lua index a7c9cec..8d8328b 100644 --- a/classes/capClasses/airGroups/AirGroup.lua +++ b/classes/capClasses/airGroups/AirGroup.lua @@ -113,6 +113,19 @@ function AirGroup:Spawn() self:SpawnInternal(false) end +function AirGroup:IsInAir() + local group = Group.getByName(self._groupName) + if group then + local units = group:getUnits() + for _, unit in pairs(units) do + if unit:inAir() == true then + return true + end + end + end + return false +end + ---@param force boolean ---@param withoutLoadout boolean? ---@protected @@ -423,6 +436,8 @@ Spearhead.classes.capClasses.airGroups.AirGroup = AirGroup ---@alias AirGroupType ---| "CAP" +---| "SWEEP" +---| "INTERCEPT" ---| "CAS" ---| "SEAD" diff --git a/classes/capClasses/airGroups/CapGroup.lua b/classes/capClasses/airGroups/CapGroup.lua index cc22a04..ececb7a 100644 --- a/classes/capClasses/airGroups/CapGroup.lua +++ b/classes/capClasses/airGroups/CapGroup.lua @@ -80,63 +80,62 @@ end ---@private function CapGroup:InitWithName(groupName) local split_string = Spearhead.Util.split_string(groupName, "_") - local partCount = Spearhead.Util.tableLength(split_string) - if partCount >= 3 then + local partCount = Spearhead.Util.tableLength(split_string) + if partCount >= 3 then - local configPart = split_string[2] - local first = configPart:sub(1, 1) - if first == "A" then - self._isBackup = false - configPart = string.sub(configPart, 2, #configPart) - elseif first == "B" then - configPart = string.sub(configPart, 2, #configPart) - self._isBackup = true - elseif first == "[" then - self._isBackup = false - else - Spearhead.AddMissionEditorWarning("Could not parse the CAP config for group: " .. groupName) - return - end + local configPart = split_string[2] + local first = configPart:sub(1, 1) + if first == "A" then + self._isBackup = false + configPart = string.sub(configPart, 2, #configPart) + elseif first == "B" then + configPart = string.sub(configPart, 2, #configPart) + self._isBackup = true + elseif first == "[" then + self._isBackup = false + else + Spearhead.AddMissionEditorWarning("Could not parse the CAP config for group: " .. groupName) + return + end - + - local subsplit = Spearhead.Util.split_string(configPart, "|") - if subsplit then - for key, value in pairs(subsplit) do - local keySplit = Spearhead.Util.split_string(value, "]") - local targetZone = keySplit[2] - local allActives = string.sub(keySplit[1], 2, #keySplit[1]) - local commaSeperated = Spearhead.Util.split_string(allActives, ",") - for _, value in pairs(commaSeperated) do - local dashSeperated = Spearhead.Util.split_string(value, "-") - if Spearhead.Util.tableLength(dashSeperated) > 1 then - local from = tonumber(dashSeperated[1]) - local till = tonumber(dashSeperated[2]) + local subsplit = Spearhead.Util.split_string(configPart, "|") + if subsplit then + for key, value in pairs(subsplit) do + local keySplit = Spearhead.Util.split_string(value, "]") + local targetZone = keySplit[2] + local allActives = string.sub(keySplit[1], 2, #keySplit[1]) + local commaSeperated = Spearhead.Util.split_string(allActives, ",") + for _, value in pairs(commaSeperated) do + local dashSeperated = Spearhead.Util.split_string(value, "-") + if Spearhead.Util.tableLength(dashSeperated) > 1 then + local from = tonumber(dashSeperated[1]) + local till = tonumber(dashSeperated[2]) - for i = from, till do - if Spearhead.Util.strContains(targetZone, "A") == true then - self._targetZoneIdPerStage[tostring(i)] = string.gsub(targetZone, "A", tostring(i)) - else - self._targetZoneIdPerStage[tostring(i)] = targetZone - end - end - else + for i = from, till do if Spearhead.Util.strContains(targetZone, "A") == true then - self._targetZoneIdPerStage[tostring(dashSeperated[1])] = string.gsub(targetZone, "A", tostring(dashSeperated[1])) + self._targetZoneIdPerStage[tostring(i)] = string.gsub(targetZone, "A", tostring(i)) else - self._targetZoneIdPerStage[tostring(dashSeperated[1])] = targetZone + self._targetZoneIdPerStage[tostring(i)] = targetZone end end + else + if Spearhead.Util.strContains(targetZone, "A") == true then + self._targetZoneIdPerStage[tostring(dashSeperated[1])] = string.gsub(targetZone, "A", tostring(dashSeperated[1])) + else + self._targetZoneIdPerStage[tostring(dashSeperated[1])] = targetZone + end end end end - - env.info("Capgroup parsed with table: " .. Spearhead.Util.toString(self._targetZoneIdPerStage)) - - else - Spearhead.AddMissionEditorWarning("CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount) - return nil end + + env.info("Capgroup parsed with table: " .. Spearhead.Util.toString(self._targetZoneIdPerStage)) + + else + Spearhead.AddMissionEditorWarning("CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount) + end end if not Spearhead then Spearhead = {} end diff --git a/classes/capClasses/airGroups/InterceptGroup.lua b/classes/capClasses/airGroups/InterceptGroup.lua new file mode 100644 index 0000000..288c3ec --- /dev/null +++ b/classes/capClasses/airGroups/InterceptGroup.lua @@ -0,0 +1,358 @@ +---@class InterceptGroup : AirGroup +---@field private _targetNames Array +---@field private _targetZoneName string +---@field private _detectionManager DetectionManager +---@field private _coalitionSide CoalitionSide +---@field private _lastKnownTargetAt number +---@field private _currentTargetName string? +---@field private _targetZoneIdPerStage table +---@field private _config CapConfig +---@field private _airbase Airbase +---@field private _maxSpeed number? +---@field private _updateTaskID number|nil +local InterceptGroup = {} +InterceptGroup.__index = InterceptGroup + + +---@param groupName string +---@param config CapConfig +---@param logger Logger +---@param detectionManager DetectionManager +---@return InterceptGroup? +function InterceptGroup.New(groupName, config, logger, detectionManager) + + setmetatable(InterceptGroup, Spearhead.classes.capClasses.airGroups.AirGroup) + local self = setmetatable({}, InterceptGroup) + Spearhead.classes.capClasses.airGroups.AirGroup.New(self, groupName, "INTERCEPT", config, logger) + + local group = Group.getByName(groupName) + if not group then + logger:error("InterceptGroup: Group " .. groupName .. " does not exist") + return nil + end + + local unit = group:getUnit(1) + if unit then + local desc = unit:getDesc() + if desc["speedMax10K"] then + self._maxSpeed = desc["speedMax10K"] * 0.75 + self._logger:debug("InterceptGroup: Max speed for group " .. groupName .. " is set to " .. self._maxSpeed .. " m/s") + end + end + + self._coalitionSide = group:getCoalition() + self._detectionManager = detectionManager + self._config = config + self._targetNames = {} + self._targetZoneIdPerStage = {} + + self:InitWithName(groupName) + + return self +end + +---comment +---@param units Array +---@param homeAirbase Airbase +function InterceptGroup:SendToInterceptUnits(units, zoneName, homeAirbase) + + self._airbase = homeAirbase + self._targetZoneName = zoneName + self:SetTargetUnits(units) +end + +---@return string? +function InterceptGroup:GetZoneIDWhenStageID(stageID) + return self._targetZoneIdPerStage[stageID] +end + +---@return string? +function InterceptGroup:GetCurrentTargetZone() + return self._targetZoneName +end + +---comment +---@param unitNames Array +function InterceptGroup:SetTargetUnits(unitNames) + + self._targetNames = unitNames + self:UpdateTask() + + if self._updateTaskID then + timer.removeFunction(self._updateTaskID) + end + + local updateContinous = function(selfA, time) + local next = selfA:UpdateTask() + if next then + return time + next + else + return nil + end + end + self._updateTaskID = timer.scheduleFunction(updateContinous, self, timer.getTime() + 30) +end + +function InterceptGroup:RemoveTargetUnit(unit) + + if not unit then return end + + local name = unit:getName() + for key, value in pairs(self._targetNames) do + if value == "name" then + table.remove(self._targetNames, key) + self._logger:debug("InterceptGroup: Removed target unit " .. name .. " from group " .. self._groupName) + break + end + end +end + +---@private +---@return Unit? +---@return Vec3? groupPoint +function InterceptGroup:GetClosestTarget() + + local group = Group.getByName(self._groupName) + if not group then return nil end + + local groupPoint = nil + for _, unit in pairs(group:getUnits()) do + if unit and unit:isExist() then + groupPoint = unit:getPoint() + break + end + end + + if not groupPoint then return nil end + + local closestUnit = nil + local closestDistance = math.huge + + for _, targetName in pairs(self._targetNames) do + local targetUnit = Unit.getByName(targetName) + if targetUnit and targetUnit:isExist() then + local pos = targetUnit:getPoint() + local distance = Spearhead.Util.VectorDistance3d(groupPoint, pos) + if distance < closestDistance then + closestDistance = distance + closestUnit = targetUnit + end + end + end + + return closestUnit, groupPoint +end + +---@return number? @interval or null if no retry required +function InterceptGroup:UpdateTask() + local group = Group.getByName(self._groupName) + if not group then return end + + + local closestUnit, groupPoint = self:GetClosestTarget() + if not closestUnit then return 15 end + if not groupPoint then return 15 end + + local closesUnitVec = closestUnit:getPoint() + local alt = closesUnitVec.y + if alt < 1000 then + alt = 1000 -- Ensure minimum altitude for intercept + end + + local selfDetected = false + for _, unit in pairs(group:getUnits()) do + if unit and unit:isExist() then + local controller = unit:getController() + if controller then + for _, detected in pairs(controller:getDetectedTargets(Controller.Detection.VISUAL, Controller.Detection.OPTIC, Controller.Detection.RADAR)) do + if detected and detected.object and detected.distance == true then + if detected.object:getName() == closestUnit:getName() then + selfDetected = true + break + end + end + end + end + end + end + + if Spearhead.DcsUtil.IsBingoFuel(self._groupName) then + self._logger:debug("InterceptGroup: " .. self._groupName .. " is at bingo fuel, returning to base") + self:SendRTB(self._airbase) + return nil -- Return to base if bingo fuel + end + + if selfDetected and self:IsInAir() == true then + if self._currentTargetName and self._currentTargetName == closestUnit:getName() then + local distance = Spearhead.Util.VectorDistance3d(closestUnit:getPoint(), groupPoint) + if distance < 30 * 1852 then + -- If the target is within 10 nautical miles, continue attacking + self._logger:debug("InterceptGroup: " .. self._groupName .. " continues attacking target " .. closestUnit:getName()) + return 15 -- Continue attacking the detected target + end + end + + self._logger:debug("InterceptGroup: " .. self._groupName .. " has detected target " .. closestUnit:getName() .. ", creating intercept mission") + local vec3 = closestUnit:getPoint() + local vec2 = { x = vec3.x, y = vec3.z } + local groupPointVec2 = { x = groupPoint.x, y = groupPoint.z } + local mission = Spearhead.classes.capClasses.taskings.INTERCEPT.getUnitInterceptMissionFromAir(self._groupName, groupPointVec2, vec2, closestUnit, self._airbase, self._config, self._maxSpeed, alt) + self:SetMission(mission) + self._currentTargetName = closestUnit:getName() + return 15 -- Just continue attacking the detected target + else + self._currentTargetName = nil + end + + + + local speed = self._config:getMaxSpeed() + local interceptPoint = self:GetInterceptPoint(groupPoint, speed, closestUnit) + + if interceptPoint == nil then + return 30 -- Return to base if no intercept point could be calculated + end + + local mission = nil + if self:IsInAir() == true then + -- If the group is in the air, create an intercept mission + mission = Spearhead.classes.capClasses.taskings.INTERCEPT.getMissionFromInAir( + self._groupName, + { x = groupPoint.x, y = groupPoint.z }, + interceptPoint, + self._airbase, + self._config, + self._maxSpeed, + alt + ) + else + mission = Spearhead.classes.capClasses.taskings.INTERCEPT.getMissionFromAirbase( + self._groupName, + interceptPoint, + self._airbase, + self._config, + self._maxSpeed, + alt + ) + end + + if mission then + self:SetMission(mission) + else + self._logger:warn("InterceptGroup: Could not create mission for group " .. self._groupName) + return 30 -- Return to base if mission could not be created + end + + return 15 +end + + +---@param originatingUnit Vec3 +---@param speed number +---@param targetUnit Unit +---@return Vec2? @Returns the intercept point as a Vec2 or nil if no intercept point could be calculated +function InterceptGroup:GetInterceptPoint(originatingUnit, speed, targetUnit) + + -- Calculate intercept point for a moving target + -- originatingUnit: Vec3 (our position) + -- speed: our speed (scalar, m/s) + -- targetUnit: Unit (target) + + if not targetUnit or not targetUnit:isExist() then return nil end + + local targetPos = targetUnit:getPoint() + local targetVel = targetUnit:getVelocity() -- Vec3 + local relPos = { + x = targetPos.x - originatingUnit.x, + y = targetPos.y - originatingUnit.y, + z = targetPos.z - originatingUnit.z + } + local relVel = { + x = targetVel.x, + y = targetVel.y, + z = targetVel.z + } + local relPos2 = relPos.x^2 + relPos.y^2 + relPos.z^2 + local relVel2 = relVel.x^2 + relVel.y^2 + relVel.z^2 + local speed2 = speed^2 + local dot = relPos.x * relVel.x + relPos.y * relVel.y + relPos.z * relVel.z + + -- Quadratic formula: a*t^2 + b*t + c = 0 + local a = relVel2 - speed2 + local b = 2 * dot + local c = relPos2 + local discriminant = b^2 - 4*a*c + if discriminant < 0 or a == 0 then + -- No solution, just head to current position + return { x = targetPos.x, y = targetPos.z } + end + local sqrtDisc = math.sqrt(discriminant) + local t1 = (-b + sqrtDisc) / (2*a) + local t2 = (-b - sqrtDisc) / (2*a) + local t = math.min(t1, t2) + if t < 0 then t = math.max(t1, t2) end + if t < 0 then + -- No valid intercept time, just head to current position + return { x = targetPos.x, y = targetPos.z } + end + -- Intercept point + local intercept = { + x = targetPos.x + targetVel.x * t, + y = targetPos.z + targetVel.z * t + } + return intercept +end + + + +---@private +function InterceptGroup:InitWithName(groupName) + local split_string = Spearhead.Util.split_string(groupName, "_") + local partCount = Spearhead.Util.tableLength(split_string) + if partCount >= 3 then + local configPart = split_string[2] + configPart = string.sub(configPart, 2, #configPart) + local subsplit = Spearhead.Util.split_string(configPart, "|") + if subsplit then + for key, value in pairs(subsplit) do + local keySplit = Spearhead.Util.split_string(value, "]") + local targetZone = keySplit[2] + local allActives = string.sub(keySplit[1], 2, #keySplit[1]) + local commaSeperated = Spearhead.Util.split_string(allActives, ",") + for _, value in pairs(commaSeperated) do + local dashSeperated = Spearhead.Util.split_string(value, "-") + if Spearhead.Util.tableLength(dashSeperated) > 1 then + local from = tonumber(dashSeperated[1]) + local till = tonumber(dashSeperated[2]) + + for i = from, till do + if Spearhead.Util.strContains(targetZone, "A") == true then + self._targetZoneIdPerStage[tostring(i)] = string.gsub(targetZone, "A", tostring(i)) + else + self._targetZoneIdPerStage[tostring(i)] = targetZone + end + end + else + if Spearhead.Util.strContains(targetZone, "A") == true then + self._targetZoneIdPerStage[tostring(dashSeperated[1])] = string.gsub(targetZone, "A", tostring(dashSeperated[1])) + else + self._targetZoneIdPerStage[tostring(dashSeperated[1])] = targetZone + end + end + end + end + end + + env.info("interceptGroup parsed with table: " .. Spearhead.Util.toString(self._targetZoneIdPerStage)) + + else + Spearhead.AddMissionEditorWarning("CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount) + end +end + + +if not Spearhead then Spearhead = {} end +if not Spearhead.classes then Spearhead.classes = {} end +if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end +if not Spearhead.classes.capClasses.airGroups then Spearhead.classes.capClasses.airGroups = {} end +Spearhead.classes.capClasses.airGroups.InterceptGroup = InterceptGroup \ No newline at end of file diff --git a/classes/capClasses/airGroups/SweepGroup.lua b/classes/capClasses/airGroups/SweepGroup.lua new file mode 100644 index 0000000..3c7ca6a --- /dev/null +++ b/classes/capClasses/airGroups/SweepGroup.lua @@ -0,0 +1,120 @@ +---@class SweepGroup : AirGroup +---@field _targetZoneIdPerStage table +---@field _currentTargetZoneID string? +local SweepGroup = {} +SweepGroup.__index = SweepGroup + + +---@param groupName string +---@param config CapConfig +---@param logger Logger +---@return SweepGroup +function SweepGroup.New(groupName, config, logger) + setmetatable(SweepGroup, Spearhead.classes.capClasses.airGroups.AirGroup) + local self = setmetatable({}, SweepGroup) --[[@as SweepGroup]] + Spearhead.classes.capClasses.airGroups.AirGroup.New(self, groupName, "SWEEP", config, logger) + + self._targetZoneIdPerStage = {} + self:InitWithName(groupName) + + return self +end + +---@return string? +function SweepGroup:GetZoneIDWhenStageID(stageID) + return self._targetZoneIdPerStage[stageID] +end + +---@return string? +function SweepGroup:GetCurrentTargetZoneID() + return self._currentTargetZoneID +end + +---@class SetTaskParams +---@field task table +---@field self SweepGroup + +---@param params SetTaskParams +local setMissionDelayedTask = function(params, time) + params.self:SetMissionPrivate(params.task) +end + +function SweepGroup:SendToZone(zone, targetZoneID, airbase) + self._logger:debug("Airgroup " .. self._groupName .. " called to zone: " .. zone.name) + + self._currentTargetZoneID = targetZoneID + + local group = Group.getByName(self._groupName) + + local mission = Spearhead.classes.capClasses.taskings.SWEEP.getAsMissionFromAirbase(self._groupName, airbase, zone, self._config) + if mission then + ---@type SetTaskParams + local params = { + task = mission, + self = self + } + local delay = math.random(120, 600) + timer.scheduleFunction(setMissionDelayedTask, params, timer.getTime() + delay) + else + self._logger:error("SweepGroup:SendToZone - Mission could not be created for group: " .. self._groupName) + end +end + +function SweepGroup:SendToZoneInternal() + + +end + +---@private +function SweepGroup:InitWithName(groupName) + local split_string = Spearhead.Util.split_string(groupName, "_") + local partCount = Spearhead.Util.tableLength(split_string) + if partCount >= 3 then + + local configPart = split_string[2] + configPart = string.sub(configPart, 2, #configPart) + + local subsplit = Spearhead.Util.split_string(configPart, "|") + if subsplit then + for key, value in pairs(subsplit) do + local keySplit = Spearhead.Util.split_string(value, "]") + local targetZone = keySplit[2] + local allActives = string.sub(keySplit[1], 2, #keySplit[1]) + local commaSeperated = Spearhead.Util.split_string(allActives, ",") + for _, value in pairs(commaSeperated) do + local dashSeperated = Spearhead.Util.split_string(value, "-") + if Spearhead.Util.tableLength(dashSeperated) > 1 then + local from = tonumber(dashSeperated[1]) + local till = tonumber(dashSeperated[2]) + + for i = from, till do + if Spearhead.Util.strContains(targetZone, "A") == true then + self._targetZoneIdPerStage[tostring(i)] = string.gsub(targetZone, "A", tostring(i)) + else + self._targetZoneIdPerStage[tostring(i)] = targetZone + end + end + else + if Spearhead.Util.strContains(targetZone, "A") == true then + self._targetZoneIdPerStage[tostring(dashSeperated[1])] = string.gsub(targetZone, "A", + tostring(dashSeperated[1])) + else + self._targetZoneIdPerStage[tostring(dashSeperated[1])] = targetZone + end + end + end + end + end + + env.info("SweepGroup parsed with table: " .. Spearhead.Util.toString(self._targetZoneIdPerStage)) + else + Spearhead.AddMissionEditorWarning("SWEEP Group with name: " .. + groupName .. "should have at least 3 parts, but has " .. partCount) + end +end + +if not Spearhead then Spearhead = {} end +if not Spearhead.classes then Spearhead.classes = {} end +if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end +if not Spearhead.classes.capClasses.airGroups then Spearhead.classes.capClasses.airGroups = {} end +Spearhead.classes.capClasses.airGroups.SweepGroup = SweepGroup \ No newline at end of file diff --git a/classes/capClasses/detection/DetectionManager.lua b/classes/capClasses/detection/DetectionManager.lua new file mode 100644 index 0000000..e0025cb --- /dev/null +++ b/classes/capClasses/detection/DetectionManager.lua @@ -0,0 +1,168 @@ + +---@class DetectionManager +---@field private _logger Logger +---@field private _detectedUnits table> +---@field private _detectingUnits table> +local DetectionManager = {} +DetectionManager.__index = DetectionManager + + +---@param logger Logger +function DetectionManager.New(logger) + local self = setmetatable({}, DetectionManager) + + self._logger = logger + self._detectedUnits = { + [tostring(coalition.side.RED)] = {}, + [tostring(coalition.side.BLUE)] = {} + } + + self._detectingUnits = { + [tostring(coalition.side.RED)] = {}, + [tostring(coalition.side.BLUE)] = {} + } + + ---@param selfA DetectionManager + ---@param time number + local updateDetectingUnitsTask = function(selfA, time) + selfA:UpdateDetectingUnits() + return time + 120 + end + timer.scheduleFunction(updateDetectingUnitsTask, self, timer.getTime() + 120) + + ---@param selfA DetectionManager + ---@param time number + local updateDetected = function(selfA, time) + selfA:UpdateDetectedUnits() + return time + 10 + end + timer.scheduleFunction(updateDetected, self, timer.getTime() + 130) + + + return self +end + +---@param unitName string +---@param coalitionSide CoalitionSide +function DetectionManager:IsUnitDetectedBy(unitName, coalitionSide) + local coalitionString = tostring(coalitionSide) + if not self._detectedUnits[coalitionString] then + return false + end + + if not self._detectedUnits[coalitionString][unitName] then + return false + end + + return timer.getTime() - self._detectedUnits[coalitionString][unitName] < 20 +end + +---@return Array +function DetectionManager:GetDetectedUnitsBy(coalitionSide) + local coalitionString = tostring(coalitionSide) + if not self._detectedUnits[coalitionString] then + return {} + end + + local detectedUnits = {} + for unitName, _ in pairs(self._detectedUnits[coalitionString]) do + if self:IsUnitDetectedBy(unitName, coalitionSide) then + table.insert(detectedUnits, unitName) + end + end + + return detectedUnits +end + +function DetectionManager:UpdateDetectingUnits() + self:UpdateDetectingInCoalition(coalition.side.RED) + self:UpdateDetectingInCoalition(coalition.side.BLUE) + self._logger:debug("Updated detecting units") +end + +function DetectionManager:UpdateDetectedUnits() + self:UpdateDetectedUnitsByCoalition(coalition.side.RED) + self:UpdateDetectedUnitsByCoalition(coalition.side.BLUE) + self._logger:debug("Updated detected units") +end + +---@private +---@param unit Unit +---@return boolean +function DetectionManager:IsDetectingType(unit) + return unit:hasAttribute("EWR") or unit:hasAttribute("AWACS") or unit:hasAttribute("SAM SR") +end + +---@private +---@param coalitionSide CoalitionSide +function DetectionManager:UpdateDetectingInCoalition(coalitionSide) + + local coalitionString = tostring(coalitionSide) + + local airGroups = coalition.getGroups(coalitionSide, Group.Category.AIRPLANE) + for _, group in ipairs(airGroups) do + if group and group:isExist() then + for _, unit in ipairs(group:getUnits()) do + if unit and self:IsDetectingType(unit) == true then + self._detectingUnits[coalitionString][unit:getName()] = unit + end + end + end + end + + local groundGroups = coalition.getGroups(coalitionSide, Group.Category.GROUND) + for _, group in ipairs(groundGroups) do + if group and group:isExist() then + for _, unit in ipairs(group:getUnits()) do + if unit and self:IsDetectingType(unit) == true then + self._detectingUnits[coalitionString][unit:getName()] = unit + end + end + end + end + + local ships = coalition.getGroups(coalitionSide, Group.Category.SHIP) + for _, group in ipairs(ships) do + if group and group:isExist() then + for _, unit in ipairs(group:getUnits()) do + if unit and self:IsDetectingType(unit) == true then + self._detectingUnits[coalitionString][unit:getName()] = unit + end + end + end + end + +end + +---@private +---@param coalitionSide CoalitionSide +function DetectionManager:UpdateDetectedUnitsByCoalition(coalitionSide) + + local coalitionString = tostring(coalitionSide) + if not self._detectingUnits[coalitionString] then return end + if not self._detectedUnits[coalitionString] then + self._detectedUnits[coalitionString] = {} + end + + for _, detectingUnit in pairs(self._detectingUnits[coalitionString]) do + if detectingUnit and detectingUnit:isExist() then + local controller = detectingUnit:getController() + if controller then + local targets = controller:getDetectedTargets(Controller.Detection.RADAR) + for _, target in pairs(targets) do + if target and target.object ~= nil and target.distance == true then + local targetUnit = target.object + local name = targetUnit:getName() + self._detectedUnits[coalitionString][name] = timer.getTime() + end + end + end + end + end +end + +if not Spearhead then Spearhead = {} end +if not Spearhead.classes then Spearhead.classes = {} end +if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end +if not Spearhead.classes.capClasses.detection then Spearhead.classes.capClasses.detection = {} end +Spearhead.classes.capClasses.detection.DetectionManager = DetectionManager diff --git a/classes/capClasses/taskings/INTERCEPT.lua b/classes/capClasses/taskings/INTERCEPT.lua new file mode 100644 index 0000000..a649be2 --- /dev/null +++ b/classes/capClasses/taskings/INTERCEPT.lua @@ -0,0 +1,399 @@ + + +---@class InterceptTasking +local INTERCEPT = {} + + +---@param groupName string +---@param interceptPoint Vec2 +---@param airbase Airbase +---@param speed number? +---@param alt number? +---@param config CapConfig +function INTERCEPT.getMissionFromAirbase(groupName, interceptPoint, airbase, config, speed, alt) + + local airbaseVec3 = airbase:getPoint() + local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z } + local heading = Spearhead.Util.vectorHeadingFromTo(airbaseVec2, interceptPoint) + + env.info("BLAAT: heading from runway to first point: " .. heading) + + local point = Spearhead.Util.vectorMove(airbaseVec2, heading, 3*1852) + + local pointA, pointB, pointC, pointD = INTERCEPT.getInterceptTaskPoint(groupName, point, interceptPoint, airbaseVec2, config, speed, alt) + + local points = { + [1] = pointA, + [2] = pointA, + [3] = pointB, + [4] = pointC, + [5] = pointD, + [6] = Spearhead.classes.capClasses.taskings.RTB.getApproachPoint(airbase, interceptPoint, config), + [7] = Spearhead.classes.capClasses.taskings.RTB.getInitialPoint(airbase), + [8] = Spearhead.classes.capClasses.taskings.RTB.getLandingPoint(airbase) + } + + local mission = { + id = 'Mission', + params = { + airborne = true, + route = { + points = points + } + } + } + return mission +end + +---comment +---@param groupName string +---@param currentPosition Vec2 +---@param interceptPoint Vec2 +---@param airbase Airbase +---@param speed number? +---@param alt number? +---@param config CapConfig +---@return table +function INTERCEPT.getMissionFromInAir(groupName, currentPosition, interceptPoint, airbase, config, speed, alt) + + local pointAirbasev3 = airbase:getPoint() + local airbase2 = { x = pointAirbasev3.x, y = pointAirbasev3.z } + + local pointA, pointB, pointC, pointD = INTERCEPT.getInterceptTaskPoint(groupName, currentPosition, interceptPoint, airbase2, config, speed, alt) + + local points = { + [1] = pointA, + [2] = pointA, + [3] = pointB, + [4] = pointC, + [5] = pointD, + [6] = Spearhead.classes.capClasses.taskings.RTB.getApproachPoint(airbase, interceptPoint, config), + [7] = Spearhead.classes.capClasses.taskings.RTB.getInitialPoint(airbase), + [8] = Spearhead.classes.capClasses.taskings.RTB.getLandingPoint(airbase) + } + + local mission = { + id = 'Mission', + params = { + airborne = true, + route = { + points = points + } + } + } + + return mission + +end + +---comment +---@param groupName string +---@param currentPoint Vec2 +---@param targetPosition Vec2 +---@param targetUnit Unit +---@param airbase any +---@param config any +---@param speed number? +---@param alt number? +---@return table +function INTERCEPT.getUnitInterceptMissionFromAir(groupName, currentPoint, targetPosition, targetUnit, airbase, config, speed, alt) + + local pointAirbasev3 = airbase:getPoint() + local airbase2 = { x = pointAirbasev3.x, y = pointAirbasev3.z } + + local pointA, pointB, pointC, pointD = INTERCEPT.getUnitInterceptTaskPoint(groupName, currentPoint, targetPosition, targetUnit, airbase2, config, speed, alt) + + local points = { + [1] = pointA, + [2] = pointA, + [3] = pointB, + [4] = pointC, + [5] = pointD, + [6] = Spearhead.classes.capClasses.taskings.RTB.getApproachPoint(airbase, currentPoint, config), + [7] = Spearhead.classes.capClasses.taskings.RTB.getInitialPoint(airbase), + [8] = Spearhead.classes.capClasses.taskings.RTB.getLandingPoint(airbase) + } + + local mission = { + id = 'Mission', + params = { + airborne = true, + route = { + points = points + } + } + } + + return mission + +end + +---@private +---@param groupName string +---@param currentPoint Vec2 +---@param targetPoint Vec2 +---@param airbasePoint Vec2 +---@param config CapConfig +---@param speed number? +---@param alt number? +---@return table pointA @Starts the task right away +---@return table pointB @The actual target point to fly to and search, but searching started at pointA +---@return table pointC A fly over point halfway between the target and the airbase, if nothing it found then the unit will fly it's route till here +---@return table pointD THe point after Point C where the unit will execute the RTB command +function INTERCEPT.getInterceptTaskPoint(groupName, currentPoint, targetPoint, airbasePoint, config, speed, alt) + + local pointA = { + alt = alt or config:getMinAlt(), + action = "Turning Point", + alt_type = "BARO", + speed = speed or config:getMaxSpeed(), + ETA = 0, + ETA_locked = false, + x = currentPoint.x, + y = currentPoint.y, + speed_locked = true, + formation_template = "", + task = { + id = "ComboTask", + params = { + tasks = { + id = 'EngageTargetsInZone', + params = { + point = targetPoint, + zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone. + targetTypes = { + [1] = "Planes", + }, + priority = 0 + } + } + } + } + } + + local pointB = { + alt = alt or config:getMinAlt(), + action = "Turning Point", + alt_type = "BARO", + speed = speed or config:getMaxSpeed(), + ETA = 0, + ETA_locked = false, + x = targetPoint.x, + y = targetPoint.y, + speed_locked = true, + formation_template = "", + task = { + id = "ComboTask", + params = { + tasks = { + id = 'EngageTargetsInZone', + params = { + point = targetPoint, + zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone. + targetTypes = { + [1] = "Planes", + }, + priority = 0 + } + } + } + } + } + + local midPoint = { + x = (targetPoint.x + airbasePoint.x) / 2, + y = (targetPoint.y + airbasePoint.y) / 2 + } + + local pointC = { + alt = config:getMinAlt(), + action = "Fly Over Point", + alt_type = "BARO", + speed = config:getMaxSpeed(), + ETA = 0, + ETA_locked = false, + x = midPoint.x, + y = midPoint.y, + speed_locked = true, + formation_template = "", + task = { + id = "ComboTask", + params = {} + } + } + + local pointD = { + alt = config:getMinAlt(), + action = "Turning Point", + alt_type = "BARO", + speed = config:getMaxSpeed(), + ETA = 0, + ETA_locked = false, + x = midPoint.x, + y = midPoint.y, + speed_locked = true, + formation_template = "", + task = { + id = "ComboTask", + params = { + tasks = { + [1] = { + number = 1, + auto = false, + id = "WrappedAction", + enabled = "true", + params = { + action = { + id = "Script", + params = { + command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" + } + } + } + } + } + } + } + } + + return pointA, pointB, pointC, pointD +end + + +---@private +---@param groupName string +---@param currentPoint Vec2 +---@param targetPosition Vec2 +---@param targetUnit Unit +---@param airbasePoint Vec2 +---@param config CapConfig +---@param speed number? +---@param alt number? +---@return table pointA @Starts the task right away +---@return table pointB @The actual target point to fly to and search, but searching started at pointA +---@return table pointC A fly over point halfway between the target and the airbase, if nothing it found then the unit will fly it's route till here +---@return table pointD THe point after Point C where the unit will execute the RTB command +function INTERCEPT.getUnitInterceptTaskPoint(groupName, currentPoint, targetPosition, targetUnit, airbasePoint, config, speed, alt) + + local pointA = { + alt = alt or config:getMinAlt(), + action = "Turning Point", + alt_type = "BARO", + speed = speed or config:getMaxSpeed(), + ETA = 0, + ETA_locked = false, + x = currentPoint.x, + y = currentPoint.y, + speed_locked = true, + formation_template = "", + task = { + id = "ComboTask", + params = { + tasks = { + [1] = { + id = 'EngageUnit', + params = { + unitId = targetUnit:getID(), + groupAttack = true, + priority = 0 + } + } + } + } + } + } + + local pointB = { + alt = alt or config:getMinAlt(), + action = "Turning Point", + alt_type = "BARO", + speed = speed or config:getMaxSpeed(), + ETA = 0, + ETA_locked = false, + x = targetPosition.x, + y = targetPosition.y, + speed_locked = true, + formation_template = "", + task = { + id = "ComboTask", + params = { + tasks = { + [1] = { + id = 'EngageUnit', + params = { + unitId = targetUnit:getID(), + groupAttack = true, + priority = 0 + } + } + } + } + } + } + + + local midPoint = { + x = (targetPosition.x + airbasePoint.x) / 2, + y = (targetPosition.y + airbasePoint.y) / 2 + } + + local pointC = { + alt = config:getMinAlt(), + action = "Fly Over Point", + alt_type = "BARO", + speed = config:getMaxSpeed(), + ETA = 0, + ETA_locked = false, + x = midPoint.x, + y = midPoint.y, + speed_locked = true, + formation_template = "", + task = { + id = "ComboTask", + params = {} + } + } + + local pointD = { + alt = config:getMinAlt(), + action = "Turning Point", + alt_type = "BARO", + speed = config:getMaxSpeed(), + ETA = 0, + ETA_locked = false, + x = midPoint.x, + y = midPoint.y, + speed_locked = true, + formation_template = "", + task = { + id = "ComboTask", + params = { + tasks = { + [1] = { + number = 1, + auto = false, + id = "WrappedAction", + enabled = "true", + params = { + action = { + id = "Script", + params = { + command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" + } + } + } + } + } + } + } + } + + return pointA, pointB, pointC, pointD +end + + +if not Spearhead then Spearhead = {} end +if not Spearhead.classes then Spearhead.classes = {} end +if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end +if not Spearhead.classes.capClasses.taskings then Spearhead.classes.capClasses.taskings = {} end +Spearhead.classes.capClasses.taskings.INTERCEPT = INTERCEPT \ No newline at end of file diff --git a/classes/capClasses/taskings/SWEEP.lua b/classes/capClasses/taskings/SWEEP.lua new file mode 100644 index 0000000..63bb99f --- /dev/null +++ b/classes/capClasses/taskings/SWEEP.lua @@ -0,0 +1,287 @@ +---@class SWEEPTasking +local SWEEP = {} + +---@param attackHelos boolean +---@return table +local function GetCAPTargetTypes(attackHelos) + local targetTypes = { + [1] = "Planes", + } + + if attackHelos then + targetTypes[2] = "Helicopters" + end + + return targetTypes +end + +---@class SWEEPTaskingOptions +---@field furthest Vec2 +---@field closest Vec2 + +---@param capZone SpearheadTriggerZone +---@param airBase Airbase +---@return SWEEPTaskingOptions +local function GetCAPPointFromTriggerZone(airBase, capZone) + local furthestA = nil + local furthestB = nil + + local furthestDistance = 0 + + for indexA, pointA in ipairs(capZone.verts) do + for indexB, pointB in ipairs(capZone.verts) do + if pointA ~= pointB then + local distance = Spearhead.Util.VectorDistance2d(pointA, pointB) + if distance > furthestDistance then + furthestDistance = distance + furthestA = indexA + furthestB = indexB + end + end + end + end + + local baseVec3 = airBase:getPoint() + + ---@type Vec2 + local baseVec2 = { x = baseVec3.x, y = baseVec3.z } + + local pointA = capZone.verts[furthestA] + local pointB = capZone.verts[furthestB] + local furthest = pointA + local closest = pointB + + local heading = Spearhead.Util.vectorHeadingFromTo(pointA, pointB) + + if Spearhead.Util.VectorDistance2d(baseVec2, pointB) > Spearhead.Util.VectorDistance2d(baseVec2, pointA) then + furthest = pointB + closest = pointA + heading = Spearhead.Util.vectorHeadingFromTo(pointB, pointA) + end + + local distance = furthestDistance + if distance > 15000 then + distance = distance - 10000 + end + + return { + width = 10000, + furthest = furthest, + closest = closest, + legLength = distance, + hotLegDir = math.rad(heading), + orbitOriginPoint = closest + } +end + +---@param airbase Airbase +---@param capZone SpearheadTriggerZone +---@param capConfig CapConfig +local GetOutboundTask = function(airbase, capZone, capConfig) + local airbaseVec3 = airbase:getPoint() + local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z } + local heading = Spearhead.Util.vectorHeadingFromTo(airbaseVec2, capZone.location) + local point = Spearhead.Util.vectorMove(airbaseVec2, heading, 18520) + + return { + alt = 2000, + action = "Fly Over Point", + type = "Turning Point", + alt_type = "BARO", + speed = capConfig:getMinSpeed(), + ETA = 0, + ETA_locked = false, + x = point.x, + y = point.y, + speed_locked = false, + formation_template = "", + task = { + id = "ComboTask", + params = { + tasks = { + [1] = { + id = 'EngageTargets', + params = { + maxDist = capConfig:getMaxDeviationRange(), + maxDistEnabled = capConfig:getMaxDeviationRange() > 0, -- required to check maxDist + targetTypes = GetCAPTargetTypes(false), + priority = 0 + } + }, + } + } + } + } +end + +---@param groupName string +---@param airbase Airbase +---@param capZone SpearheadTriggerZone +---@param capConfig CapConfig +function SWEEP.getAsMissionFromAirbase(groupName, airbase, capZone, capConfig) + + local pointA, pointB, pointC = SWEEP.getAsTasking(groupName, airbase, capZone, capConfig) + + local points = { + [1] = GetOutboundTask(airbase, capZone, capConfig), + [2] = GetOutboundTask(airbase, capZone, capConfig), + [3] = pointA, + [4] = pointB, + [5] = pointC, + [6] = Spearhead.classes.capClasses.taskings.RTB.getApproachPoint(airbase, capZone.location, capConfig), + [7] = Spearhead.classes.capClasses.taskings.RTB.getInitialPoint(airbase), + [8] = Spearhead.classes.capClasses.taskings.RTB.getLandingPoint(airbase) + } + + local mission = { + id = 'Mission', + params = { + airborne = true, + route = { + points = points + } + } + } + + return mission +end + +---@private +---@param groupName string +---@param airbase Airbase +---@param capZone SpearheadTriggerZone +---@param capConfig CapConfig +---@return table TurningPointA +---@return table TurningPointB +---@return table TurningPointC +function SWEEP.getAsTasking(groupName, airbase, capZone, capConfig) + + local capTaskingOptions = GetCAPPointFromTriggerZone(airbase, capZone) + + + local alt = math.random(capConfig:getMinAlt(), capConfig:getMaxAlt()) + local speed = capConfig:getMaxSpeed() + + local pointA = { + alt = alt, + action = "Turning Point", + alt_type = "BARO", + speed = speed, + ETA = 0, + ETA_locked = false, + x = capTaskingOptions.closest.x, + y = capTaskingOptions.closest.y, + speed_locked = true, + formation_template = "", + task = { + id = "ComboTask", + params = { + tasks = { + [1] = { + number = 1, + auto = false, + id = "WrappedAction", + enabled = "true", + params = { + action = { + id = "Script", + params = { + command = "pcall(Spearhead.Events.PublishOnStation, \"" .. groupName .. "\")" + } + } + } + }, + [2] = { + id = 'EngageTargets', + params = { + maxDist = capConfig:getMaxDeviationRange(), + maxDistEnabled = capConfig:getMaxDeviationRange() >= 0, -- required to check maxDist + targetTypes = GetCAPTargetTypes(false), + priority = 0 + } + } + } + } + } + } + + local pointB = { + alt = alt, + action = "Turning Point", + alt_type = "BARO", + speed = speed, + ETA = 0, + ETA_locked = false, + x = capTaskingOptions.furthest.x, + y = capTaskingOptions.furthest.y, + speed_locked = true, + formation_template = "", + task = { + id = "ComboTask", + params = { + tasks = { + [1] = { + id = 'EngageTargets', + params = { + maxDist = capConfig:getMaxDeviationRange(), + maxDistEnabled = capConfig:getMaxDeviationRange() >= 0, -- required to check maxDist + targetTypes = GetCAPTargetTypes(false), + priority = 0 + } + } + } + } + } + } + + local pointC = { + alt = alt, + action = "Turning Point", + alt_type = "BARO", + speed = speed, + ETA = 0, + ETA_locked = false, + x = capTaskingOptions.closest.x, + y = capTaskingOptions.closest.y, + speed_locked = true, + formation_template = "", + task = { + id = "ComboTask", + params = { + tasks = { + [1] = { + id = 'EngageTargets', + params = { + maxDist = capConfig:getMaxDeviationRange(), + maxDistEnabled = capConfig:getMaxDeviationRange() >= 0, -- required to check maxDist + targetTypes = GetCAPTargetTypes(false), + priority = 0 + } + }, + [2] = { + number = 2, + auto = false, + id = "WrappedAction", + enabled = "true", + params = { + action = { + id = "Script", + params = { + command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" + } + } + } + } + } + } + } + } + + return pointA, pointB, pointC +end + +if not Spearhead then Spearhead = {} end +if not Spearhead.classes then Spearhead.classes = {} end +if not Spearhead.classes.capClasses then Spearhead.classes.capClasses = {} end +if not Spearhead.classes.capClasses.taskings then Spearhead.classes.capClasses.taskings = {} end +Spearhead.classes.capClasses.taskings.SWEEP = SWEEP diff --git a/classes/spearhead_db.lua b/classes/spearhead_db.lua index dc71dcf..f0aa811 100644 --- a/classes/spearhead_db.lua +++ b/classes/spearhead_db.lua @@ -7,8 +7,10 @@ ---@field MissionZonesLocations table All Mission Zone Locations ---@field StageZonesByNumber table> Stage zones grouped by index number ---@field AllFarpZones Array ----@field CapRoutes Array All Cap route zone names ----@field capZonesByCapZoneID table table +---@field AllCapRoutes Array All Cap route zone names +---@field AllInterceptZones Array All intercept zones +---@field capZonesByCapZoneID table +---@field interceptZonesByZoneID table> ---@field CarrierRouteZones Array All Carrier routes zones ---@field BlueSams Array All blue sam zones ---@field SupplyHubZones Array All supply hub zones @@ -43,6 +45,8 @@ ---@class AirbaseData ---@field CapGroups Array +---@field SweepGroups Array +---@field InterceptGroups Array ---@field RedGroups Array ---@field BlueGroups Array ---@field supplyHubNames Array @@ -78,8 +82,10 @@ function Database.New(Logger) local tables = { AllZoneNames = {}, BlueSams = {}, - CapRoutes = {}, + AllCapRoutes = {}, capZonesByCapZoneID = {}, + AllInterceptZones = {}, + interceptZonesByZoneID = {}, CarrierRouteZones = {}, MissionZones = {}, MissionZonesLocations = {}, @@ -141,37 +147,43 @@ function Database.New(Logger) end end - if string.lower(split_string[1]) == "waitingstage" then + local lowered = string.lower(split_string[1]) + + if lowered == "waitingstage" then table.insert(self._tables.StageZoneNames, zone_name) end - if string.lower(split_string[1]) == "mission" then + if lowered == "mission" then table.insert(self._tables.MissionZones, zone_name) self._tables.MissionZonesLocations[zone_name] = zoneLocation end - if string.lower(split_string[1]) == "randommission" then + if lowered == "randommission" then table.insert(self._tables.RandomMissionZones, zone_name) self._tables.MissionZonesLocations[zone_name] = zoneLocation end - if string.lower(split_string[1]) == "farp" then + if lowered == "farp" then table.insert(self._tables.AllFarpZones, zone_name) end - if string.lower(split_string[1]) == "caproute" then - table.insert(self._tables.CapRoutes, zone_name) + if lowered == "caproute" then + table.insert(self._tables.AllCapRoutes, zone_name) end - if string.lower(split_string[1]) == "carrierroute" then + if lowered == "interceptzone" then + table.insert(self._tables.AllInterceptZones, zone_name) + end + + if lowered == "carrierroute" then table.insert(self._tables.CarrierRouteZones, zone_name) end - if string.lower(split_string[1]) == "bluesam" then + if lowered == "bluesam" then table.insert(self._tables.BlueSams, zone_name) end - if string.lower(split_string[1]) == "supplyhub" then + if lowered == "supplyhub" then table.insert(self._tables.SupplyHubZones, zone_name) end end @@ -364,7 +376,7 @@ function Database.New(Logger) self:loadMiscGroupsInStages() - for _, cap_route_zone in pairs(self._tables.CapRoutes) do + for _, cap_route_zone in pairs(self._tables.AllCapRoutes) do local split = Spearhead.Util.split_string(cap_route_zone, "_") local zoneID = split[2] @@ -383,6 +395,22 @@ function Database.New(Logger) end end + for _, interceptZone in pairs(self._tables.AllInterceptZones) do + local split = Spearhead.Util.split_string(interceptZone, "_") + local zoneID = split[2] + + if zoneID then + if tables.interceptZonesByZoneID[zoneID] == nil then + tables.interceptZonesByZoneID[zoneID] = {} + end + + local zone = Spearhead.DcsUtil.getZoneByName(interceptZone) + if zone then + table.insert(tables.interceptZonesByZoneID[zoneID], zone) + end + end + end + local totalUnits = 0 local missions = 0 for _, data in pairs(self._tables.MissionZoneData) do @@ -525,6 +553,8 @@ function Database:getOrCreateAirbaseData(baseName) if baseData == nil then baseData = { CapGroups = {}, + InterceptGroups = {}, + SweepGroups = {}, RedGroups = {}, BlueGroups = {}, supplyHubNames = {} @@ -591,7 +621,14 @@ function Database:loadCapUnits() local groups = Spearhead.DcsUtil.areGroupsInCustomZone(all_groups, zone) for _, groupName in pairs(groups) do is_group_taken[groupName] = true - table.insert(baseData.CapGroups, groupName) + + if Spearhead.Util.startswith(groupName, "CAP_A", true) or Spearhead.Util.startswith(groupName, "CAP_B", true) then + table.insert(baseData.CapGroups, groupName) + elseif Spearhead.Util.startswith(groupName, "CAP_I", true) then + table.insert(baseData.InterceptGroups, groupName) + elseif Spearhead.Util.startswith(groupName, "CAP_S", true) then + table.insert(baseData.SweepGroups, groupName) + end end self._tables.AirbaseDataPerAirfield[airbase:getName()] = baseData @@ -832,6 +869,18 @@ function Database:GetCapZoneForZoneID(zoneID) return nil end +function Database:GetInterceptZonesForZoneID(zoneID) + zoneID = tostring(zoneID) or "nothing" + + local interceptZonesForID = self._tables.interceptZonesByZoneID[zoneID] + + if not interceptZonesForID then + return {} + end + + return interceptZonesForID +end + ---@return Array result a list of stage zone names function Database:getStagezoneNames() return self._tables.StageZoneNames diff --git a/dev/dev_classes.lua b/dev/dev_classes.lua index fc7d9c5..a2b0c34 100644 --- a/dev/dev_classes.lua +++ b/dev/dev_classes.lua @@ -46,8 +46,13 @@ assert(loadfile(classPath .. "stageClasses\\SpecialZones\\SupplyHub.lua"))() assert(loadfile(classPath .. "capClasses\\taskings\\RTB.lua"))() assert(loadfile(classPath .. "capClasses\\taskings\\CAP.lua"))() +assert(loadfile(classPath .. "capClasses\\taskings\\SWEEP.lua"))() +assert(loadfile(classPath .. "capClasses\\taskings\\INTERCEPT.lua"))() assert(loadfile(classPath .. "capClasses\\airGroups\\AirGroup.lua"))() assert(loadfile(classPath .. "capClasses\\airGroups\\CapGroup.lua"))() +assert(loadfile(classPath .. "capClasses\\airGroups\\SweepGroup.lua"))() +assert(loadfile(classPath .. "capClasses\\airGroups\\InterceptGroup.lua"))() +assert(loadfile(classPath .. "capClasses\\detection\\DetectionManager.lua"))() assert(loadfile(classPath .. "capClasses\\GlobalCapManager.lua"))() assert(loadfile(classPath .. "capClasses\\CapAirbase.lua"))() assert(loadfile(classPath .. "capClasses\\runwayBombing\\RunwayBombingTracker.lua"))() diff --git a/main.lua b/main.lua index 4a72dda..4d173f1 100644 --- a/main.lua +++ b/main.lua @@ -33,7 +33,10 @@ else standardLogger:info("Persistence disabled") end -Spearhead.classes.capClasses.GlobalCapManager.start(databaseManager, capConfig, stageConfig, defaultLogLevel) +local detectionLogger = Spearhead.LoggerTemplate.new("DetectionManager", defaultLogLevel) +local detectionManager = Spearhead.classes.capClasses.detection.DetectionManager.New(detectionLogger) + +Spearhead.classes.capClasses.GlobalCapManager.start(databaseManager, capConfig, detectionManager, stageConfig, defaultLogLevel) Spearhead.internal.GlobalStageManager:NewAndStart(databaseManager, stageConfig, defaultLogLevel) Spearhead.internal.GlobalFleetManager.start(databaseManager) @@ -74,3 +77,8 @@ Spearhead.internal.GlobalStageManager:printFullOverview() -- trigger.action.lineToAll(-1 , 56+i , { x= a.x, y = 0, z = a.z } , { x = b.x, y = 0, z = b.z } , color , 1, true) -- end + + +local unit = Unit.getByName("CAP_B[1-2]10A_sochi-2-1") +local desc = unit:getDesc() +env.info("Blaat desc: " .. Spearhead.Util.toString(desc)) \ No newline at end of file