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 <tim.rorije@ing.com>
Co-authored-by: dutchie032 <dutchie032>

---------

Co-authored-by: ex61wi <tim.rorije@ing.com>
Co-authored-by: dutchie032 <dutchie032>
This commit is contained in:
2025-06-12 22:40:55 +02:00
committed by GitHub
co-authored by ex61wi dutchie032 <dutchie032>
parent bdf7154333
commit ef65f5a353
22 changed files with 1834 additions and 353 deletions
+2 -1
View File
@@ -15,5 +15,6 @@
"foldStartRegex": "[\\s]*--region[\\s]*(.*)",
"disableFolding": false
}
}
},
"livePreview.serverRoot": "/_docs"
}
+16
View File
@@ -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<DetectedObject> Returns a list of units that are detected by the controller. <br>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"
+8 -28
View File
@@ -5,46 +5,26 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead</title>
<link rel="stylesheet" href="style/style.css">
<script src="js/site.js"></script>
<script type="module" src="js/components.js"></script>
</head>
<body>
<header>
<div class="header-box">
<a class="logo" href="index.html">Spearhead</a>
<div class="header-right">
<nav>
<a href="index.html">Home</a>
<a href="pages/tutorials.html">Tutorials</a>
<a href="pages/persistence.html">Persistence</a>
<a href="pages/reference.html">Reference</a>
<a href="pages/spearheadapi.html">API</a>
</nav>
<button id="theme-toggle" class="theme-toggle" title="Toggle light/dark theme">
<svg id="moon-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 11.807A9.002 9.002 0 0 1 10.049 2a9.942 9.942 0 0 0-5.12 2.735c-3.905 3.905-3.905 10.237 0 14.142 3.906 3.906 10.237 3.905 14.143 0a9.946 9.946 0 0 0 2.735-5.119A9.003 9.003 0 0 1 12 11.807z"/>
</svg>
<svg id="sun-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="display: none;">
<path d="M6.995 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007-2.246-5.007-5.007-5.007S6.995 9.239 6.995 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19H12.998V22H10.998zM10.998 2H12.998V5H10.998zM1.998 11H4.998V13H1.998zM18.998 11H21.998V13H18.998z"/>
<path transform="rotate(-45.017 5.986 18.01)" d="M4.986 17.01H6.986V19.01H4.986z"/>
<path transform="rotate(-45.001 18.008 5.99)" d="M17.008 4.99H19.008V6.99H17.008z"/>
<path transform="rotate(-134.983 5.988 5.99)" d="M4.988 4.99H6.988V6.99H4.988z"/>
<path transform="rotate(134.999 18.008 18.01)" d="M17.008 17.01H19.008V19.01H17.008z"/>
</svg>
</button>
</div>
</div>
<app-header></app-header>
</header>
<main>
<div class="reference-container">
<div class="side-nav">
<h4>Quick Links</h4>
<ul>
<li><a href="pages/tutorials.html" class="side-nav-h2">Tutorials</a></li>
<li><a href="pages/persistence.html" class="side-nav-h2">Persistence</a></li>
<li><a href="pages/reference.html" class="side-nav-h2">Reference</a></li>
<li><a href="pages/spearheadapi.html" class="side-nav-h2">API</a></li>
<li><a href="/pages/tutorials.html" class="side-nav-h2">Tutorials</a></li>
<li><a href="/pages/persistence.html" class="side-nav-h2">Persistence</a></li>
<li><a href="/pages/reference.html" class="side-nav-h2">Reference</a></li>
<li><a href="/pages/spearheadapi.html" class="side-nav-h2">API</a></li>
</ul>
</div>
<div class="content-wrapper">
-7
View File
@@ -1,7 +0,0 @@
<style>
.side-nav a.active {
font-weight: bold;
color: #4fc3f7;
}
</style>
<!-- Removed inline script: sidenav highlight now handled by site.js -->
+41 -67
View File
@@ -4,69 +4,36 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead Persistence</title>
<link rel="stylesheet" href="../style/style.css">
<script src="../js/site.js"></script>
<title>Spearhead Persistence</title> <!-- Google Fonts -->
<link rel="stylesheet" href="/style/style.css">
<script src="/js/site.js"></script>
<style>
.side-nav a.active {
font-weight: bold;
color: #4fc3f7;
}
.side-nav a.active {
font-weight: bold;
color: #4fc3f7;
}
</style>
<script type="module" src="/js/components.js"></script>
</head>
<body>
<header>
<div class="header-box">
<a class="logo" href="../index.html">Spearhead</a>
<div class="header-right">
<nav>
<a href="../index.html">Home</a>
<a href="../pages/tutorials.html">Tutorials</a>
<a href="../pages/persistence.html">Persistence</a>
<a href="../pages/reference.html">Reference</a>
<a href="../pages/spearheadapi.html">API</a>
</nav>
<button id="theme-toggle" class="theme-toggle" title="Toggle light/dark theme">
<svg id="moon-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 11.807A9.002 9.002 0 0 1 10.049 2a9.942 9.942 0 0 0-5.12 2.735c-3.905 3.905-3.905 10.237 0 14.142 3.906 3.906 10.237 3.905 14.143 0a9.946 9.946 0 0 0 2.735-5.119A9.003 9.003 0 0 1 12 11.807z"/>
</svg>
<svg id="sun-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="display: none;">
<path d="M6.995 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007-2.246-5.007-5.007-5.007S6.995 9.239 6.995 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19H12.998V22H10.998zM10.998 2H12.998V5H10.998zM1.998 11H4.998V13H1.998zM18.998 11H21.998V13H18.998z"/>
<path transform="rotate(-45.017 5.986 18.01)" d="M4.986 17.01H6.986V19.01H4.986z"/>
<path transform="rotate(-45.001 18.008 5.99)" d="M17.008 4.99H19.008V6.99H17.008z"/>
<path transform="rotate(-134.983 5.988 5.99)" d="M4.988 4.99H6.988V6.99H4.988z"/>
<path transform="rotate(134.999 18.008 18.01)" d="M17.008 17.01H19.008V19.01H17.008z"/>
</svg>
</button>
</div>
</div>
</header>
<main>
<app-header></app-header>
</header> <main>
<div class="reference-container">
<div class="side-nav">
<h4 class="side-nav-title"></h4>
<ul>
<li><a href="#settings" class="side-nav-h2">Settings</a></li>
<li><a href="#feedback" class="side-nav-h2">Feedback</a></li>
<li><a href="#basic-behavior" class="side-nav-h2">Basic Behavior</a></li>
<li><a href="#misc-units" class="side-nav-h2">Misc Units</a></li>
<li><a href="#missions" class="side-nav-h2">Missions</a></li>
<li><a href="#blue-sams" class="side-nav-h2">Blue SAMs</a></li>
<li><a href="#airbases" class="side-nav-h2">Airbases</a></li>
<li><a href="#warehouses" class="side-nav-h2">Warehouses</a></li>
</ul>
</div>
<app-sidebar></app-sidebar>
<div class="content-wrapper">
<h1>Persistence</h1>
<p>
Spearhead comes with a custom Persistence option. <br/>
It will even save burned out vehicles to give players a consistent battlefield even after the restart. <br/>
Spearhead comes with a custom Persistence option. <br />
It will even save burned out vehicles to give players a consistent battlefield even after the
restart. <br />
</p>
<p>
Most is pretty straightforward, however, some zones are somewhat special. <br/>
Most is pretty straightforward, however, some zones are somewhat special. <br />
Underneath you'll see all special zones listed.
</p>
@@ -77,9 +44,10 @@
<h2 id="feedback">Feedback</h2>
<p>
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. <br/>
Currently implemented is local file storage. <br/>
If enough interest is expressed, cloud-based persistence would be possible. <br/>
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. <br />
Currently implemented is local file storage. <br />
If enough interest is expressed, cloud-based persistence would be possible. <br />
</p>
<pre>
@@ -102,40 +70,46 @@
<h2 id="basic-behavior">Basic Behavior</h2>
<p>
While playing the mission, Spearhead is keeping track of all units killed. <br/>
These units are stored in memory internally and written to file. <br/>
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.<br/>
While playing the mission, Spearhead is keeping track of all units killed. <br />
These units are stored in memory internally and written to file. <br />
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.<br />
</p>
<h2 id="misc-units">Misc Units</h2>
<p>
Miscellaneous units will follow basic behavior. <br/>
These are units that are part of a stage but are not in a mission or airbase. <br/>
These units will be replaced by a static "DEAD" unit after a mission restart at the location it was killed. <br/>
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. <br />
These are units that are part of a stage but are not in a mission or airbase. <br />
These units will be replaced by a static "DEAD" unit after a mission restart at the location it was
killed. <br />
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.
</p>
<h2 id="missions">Missions</h2>
<p>
Missions follow the same logic as Misc Units. <br/>
Missions follow the same logic as Misc Units. <br />
</p>
<h2 id="blue-sams">Blue SAMs</h2>
<p>
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. <br/>
This will ensure that the blue units are placed as needed. <br/>
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. <br />
This will ensure that the blue units are placed as needed. <br />
</p>
<h2 id="airbases">Airbases</h2>
<p>
Airbase units will also be checked for overlap. As the blue units will be spawned after the RED unit. <br/>
Units that were alive when the stage was completed will be removed. Units that died will have corpses spawned. <br/>
Airbase units will also be checked for overlap. As the blue units will be spawned after the RED
unit. <br />
Units that were alive when the stage was completed will be removed. Units that died will have
corpses spawned. <br />
</p>
<h2 id="warehouses">Warehouses</h2>
<p>
Currently, warehouses are not implemented and therefore warehouses are not persisted. <br/>
When supply missions and logistics get implemented, warehouses will be persisted as well. <br/>
Currently, warehouses are not implemented and therefore warehouses are not persisted. <br />
When supply missions and logistics get implemented, warehouses will be persisted as well. <br />
</p>
</div>
</div>
@@ -146,4 +120,4 @@
</body>
</html>
```
```
+7 -67
View File
@@ -4,7 +4,8 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead Reference</title>
<title>Spearhead Reference</title> <!-- Google Fonts -->
<link rel="stylesheet" href="../style/style.css">
<script src="../js/site.js"></script>
<style>
@@ -12,78 +13,17 @@
font-weight: bold;
color: #4fc3f7;
}
</style>
</style>
<script type="module" src="../js/components.js"></script>
</head>
<body>
<header>
<div class="header-box">
<a class="logo" href="../index.html">Spearhead</a>
<div class="header-right">
<nav>
<a href="../index.html">Home</a>
<a href="../pages/tutorials.html">Tutorials</a>
<a href="../pages/persistence.html">Persistence</a>
<a href="../pages/reference.html">Reference</a>
<a href="../pages/spearheadapi.html">API</a>
</nav>
<button id="theme-toggle" class="theme-toggle" title="Toggle light/dark theme">
<svg id="moon-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 11.807A9.002 9.002 0 0 1 10.049 2a9.942 9.942 0 0 0-5.12 2.735c-3.905 3.905-3.905 10.237 0 14.142 3.906 3.906 10.237 3.905 14.143 0a9.946 9.946 0 0 0 2.735-5.119A9.003 9.003 0 0 1 12 11.807z"/>
</svg>
<svg id="sun-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="display: none;">
<path d="M6.995 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007-2.246-5.007-5.007-5.007S6.995 9.239 6.995 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19H12.998V22H10.998zM10.998 2H12.998V5H10.998zM1.998 11H4.998V13H1.998zM18.998 11H21.998V13H18.998z"/>
<path transform="rotate(-45.017 5.986 18.01)" d="M4.986 17.01H6.986V19.01H4.986z"/>
<path transform="rotate(-45.001 18.008 5.99)" d="M17.008 4.99H19.008V6.99H17.008z"/>
<path transform="rotate(-134.983 5.988 5.99)" d="M4.988 4.99H6.988V6.99H4.988z"/>
<path transform="rotate(134.999 18.008 18.01)" d="M17.008 17.01H19.008V19.01H17.008z"/>
</svg>
</button>
</div>
</div>
</header>
<app-header></app-header>
</header>
<main>
<div class="reference-container">
<div class="side-nav">
<h4 class="side-nav-title"></h4>
<ul>
<li><a href="#configuration" class="side-nav-h2">Configuration</a></li>
<li><a href="#general-naming-conventions" class="side-nav-h2">Naming Conventions</a>
<ul>
<li><a href="#stage-zones" class="side-nav-h3">Stage Zones</a></li>
<li><a href="#waiting-stages" class="side-nav-h3">Waiting Stages</a></li>
<li><a href="#mission-zones" class="side-nav-h3">Mission Zones</a></li>
<li><a href="#cap-routes" class="side-nav-h3">CAP Routes</a></li>
<li><a href="#farp-zones" class="side-nav-h3">FARP Zones</a></li>
</ul>
</li>
<li><a href="#mission-types" class="side-nav-h2">Mission Types</a>
<ul>
<li><a href="#sam" class="side-nav-h3">SAM</a></li>
<li><a href="#dead" class="side-nav-h3">DEAD</a></li>
<li><a href="#bai" class="side-nav-h3">BAI</a></li>
<li><a href="#cas" class="side-nav-h3">BAI</a></li>
<li><a href="#strike" class="side-nav-h3">STRIKE</a></li>
</ul>
</li>
<li><a href="#mission-briefings" class="side-nav-h2">Mission Briefings</a>
<ul>
<li><a href="#special-fields" class="side-nav-h3">Special Fields</a></li>
</ul>
</li>
<li><a href="#cap-configuration" class="side-nav-h2">CAP Configuration</a>
<ul>
<li><a href="#cap-group-naming" class="side-nav-h3">CAP Group Naming</a></li>
<li><a href="#cap-group-config" class="side-nav-h3">CAP Group Config</a></li>
<li><a href="#active-and-backup-cap" class="side-nav-h3">Active and Backup CAP</a></li>
</ul>
</li>
<li><a href="#randomization" class="side-nav-h2">Randomization</a></li>
<li><a href="#in-stage-dependencies" class="side-nav-h2">In stage dependencies</a></li>
<li><a href="#buildable" class="side-nav-h2">Buildables</a></li>
<li><a href="#runway-bombing" class="side-nav-h2">Runway Bombing</a></li>
</ul>
</div>
<app-sidebar></app-sidebar>
<div class="content-wrapper">
<h1>Reference</h1>
+6 -34
View File
@@ -5,46 +5,18 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead API</title>
<link rel="stylesheet" href="../style/style.css">
<script src="../js/site.js"></script>
<script src="../js/site.js"></script>
<script type="module" src="../js/components.js"></script>
</head>
<body>
<header>
<div class="header-box">
<a class="logo" href="../index.html">Spearhead</a>
<div class="header-right">
<nav>
<a href="../index.html">Home</a>
<a href="../pages/tutorials.html">Tutorials</a>
<a href="../pages/persistence.html">Persistence</a>
<a href="../pages/reference.html">Reference</a>
<a href="../pages/spearheadapi.html">API</a>
</nav>
<button id="theme-toggle" class="theme-toggle" title="Toggle light/dark theme">
<svg id="moon-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 11.807A9.002 9.002 0 0 1 10.049 2a9.942 9.942 0 0 0-5.12 2.735c-3.905 3.905-3.905 10.237 0 14.142 3.906 3.906 10.237 3.905 14.143 0a9.946 9.946 0 0 0 2.735-5.119A9.003 9.003 0 0 1 12 11.807z"/>
</svg>
<svg id="sun-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="display: none;">
<path d="M6.995 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007-2.246-5.007-5.007-5.007S6.995 9.239 6.995 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19H12.998V22H10.998zM10.998 2H12.998V5H10.998zM1.998 11H4.998V13H1.998zM18.998 11H21.998V13H18.998z"/>
<path transform="rotate(-45.017 5.986 18.01)" d="M4.986 17.01H6.986V19.01H4.986z"/>
<path transform="rotate(-45.001 18.008 5.99)" d="M17.008 4.99H19.008V6.99H17.008z"/>
<path transform="rotate(-134.983 5.988 5.99)" d="M4.988 4.99H6.988V6.99H4.988z"/>
<path transform="rotate(134.999 18.008 18.01)" d="M17.008 17.01H19.008V19.01H17.008z"/>
</svg>
</button>
</div>
</div>
</header>
<main>
<app-header></app-header>
</header> <main>
<div class="reference-container">
<div class="side-nav">
<h4 class="side-nav-title"></h4>
<ul>
<li><a href="#introduction" class="side-nav-h2">Introduction</a></li>
<li><a href="#stages" class="side-nav-h2">Stages</a></li>
</ul>
</div>
<app-sidebar></app-sidebar>
<div class="content-wrapper">
<h1>Spearhead API</h1>
+6 -47
View File
@@ -5,65 +5,24 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead Tutorials</title>
<link rel="stylesheet" href="../style/style.css">
<script src="../js/site.js"></script>
<style>
.side-nav a.active {
font-weight: bold;
color: #4fc3f7;
}
}
</style>
<script type="module" src="../js/components.js"></script>
</head>
<body>
<header>
<div class="header-box">
<a class="logo" href="../index.html">Spearhead</a>
<div class="header-right">
<nav>
<a href="../index.html">Home</a>
<a href="../pages/tutorials.html">Tutorials</a>
<a href="../pages/persistence.html">Persistence</a>
<a href="../pages/reference.html">Reference</a>
<a href="../pages/spearheadapi.html">API</a>
</nav>
<button id="theme-toggle" class="theme-toggle" title="Toggle light/dark theme">
<svg id="moon-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 11.807A9.002 9.002 0 0 1 10.049 2a9.942 9.942 0 0 0-5.12 2.735c-3.905 3.905-3.905 10.237 0 14.142 3.906 3.906 10.237 3.905 14.143 0a9.946 9.946 0 0 0 2.735-5.119A9.003 9.003 0 0 1 12 11.807z"/>
</svg>
<svg id="sun-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="display: none;">
<path d="M6.995 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007-2.246-5.007-5.007-5.007S6.995 9.239 6.995 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19H12.998V22H10.998zM10.998 2H12.998V5H10.998zM1.998 11H4.998V13H1.998zM18.998 11H21.998V13H18.998z"/>
<path transform="rotate(-45.017 5.986 18.01)" d="M4.986 17.01H6.986V19.01H4.986z"/>
<path transform="rotate(-45.001 18.008 5.99)" d="M17.008 4.99H19.008V6.99H17.008z"/>
<path transform="rotate(-134.983 5.988 5.99)" d="M4.988 4.99H6.988V6.99H4.988z"/>
<path transform="rotate(134.999 18.008 18.01)" d="M17.008 17.01H19.008V19.01H17.008z"/>
</svg>
</button>
</div>
</div>
</header>
<main>
<app-header></app-header>
</header> <main>
<div class="reference-container">
<div class="side-nav">
<h4 class="side-nav-title"></h4>
<ul>
<li><a href="#include-the-script" class="side-nav-h2">Include the Script</a></li>
<li><a href="#stages" class="side-nav-h2">Stages</a></li>
<li><a href="#setting-up-cap" class="side-nav-h2">Setting up CAP</a>
<ul>
<li><a href="#creating-cap-routes" class="side-nav-h3">Creating CAP Routes</a></li>
</ul>
</li>
<li><a href="#setting-up-the-missions" class="side-nav-h2">Setting up Missions</a>
<ul>
<li><a href="#mission-1-dead" class="side-nav-h3">Mission: DEAD</a></li>
<li><a href="#mission-2-strike" class="side-nav-h3">Mission: STRIKE</a></li>
<li><a href="#mission-cas" class="side-nav-h3">Mission: CAS</a></li>
</ul>
</li>
<li><a href="#mission-briefings" class="side-nav-h2">Mission Briefings</a></li>
</ul>
</div>
<app-sidebar></app-sidebar>
<div class="content-wrapper">
<h1>Tutorials</h1>
+99 -37
View File
@@ -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);
}
}
Binary file not shown.
+178 -3
View File
@@ -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<string, CapGroup>
---@field private sweepGroupsByName table<string, SweepGroup>
---@field private interceptGroupsByName table<string, InterceptGroup>
---@field private runwayBombingTracker RunwayBombingTracker
---@field private runwayStrikeMissions table<string, RunwayStrikeMission>
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<string, Array<string>>
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
+3 -2
View File
@@ -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
+15
View File
@@ -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"
+44 -45
View File
@@ -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
@@ -0,0 +1,358 @@
---@class InterceptGroup : AirGroup
---@field private _targetNames Array<string>
---@field private _targetZoneName string
---@field private _detectionManager DetectionManager
---@field private _coalitionSide CoalitionSide
---@field private _lastKnownTargetAt number
---@field private _currentTargetName string?
---@field private _targetZoneIdPerStage table<string, string>
---@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<string>
---@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<string>
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
+120
View File
@@ -0,0 +1,120 @@
---@class SweepGroup : AirGroup
---@field _targetZoneIdPerStage table<number, string>
---@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
@@ -0,0 +1,168 @@
---@class DetectionManager
---@field private _logger Logger
---@field private _detectedUnits table<string, table<string, number>>
---@field private _detectingUnits table<string, table<string, Unit>>
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<string>
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
+399
View File
@@ -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
+287
View File
@@ -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
+63 -14
View File
@@ -7,8 +7,10 @@
---@field MissionZonesLocations table<string, Vec2> All Mission Zone Locations
---@field StageZonesByNumber table<string, Array<string>> Stage zones grouped by index number
---@field AllFarpZones Array<string>
---@field CapRoutes Array<string> All Cap route zone names
---@field capZonesByCapZoneID table<string, CapRoute> table<StageNumber, table>
---@field AllCapRoutes Array<string> All Cap route zone names
---@field AllInterceptZones Array<string> All intercept zones
---@field capZonesByCapZoneID table<string, CapRoute>
---@field interceptZonesByZoneID table<string, Array<SpearheadTriggerZone>>
---@field CarrierRouteZones Array<string> All Carrier routes zones
---@field BlueSams Array<string> All blue sam zones
---@field SupplyHubZones Array<string> All supply hub zones
@@ -43,6 +45,8 @@
---@class AirbaseData
---@field CapGroups Array<string>
---@field SweepGroups Array<string>
---@field InterceptGroups Array<string>
---@field RedGroups Array<string>
---@field BlueGroups Array<string>
---@field supplyHubNames Array<string>
@@ -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<string> result a list of stage zone names
function Database:getStagezoneNames()
return self._tables.StageZoneNames
+5
View File
@@ -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"))()
+9 -1
View File
@@ -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))