45 Commits
Author SHA1 Message Date
dutchie031 4b3d87f589 updated luals version
PR Prerequisites / Lua Check (pull_request) Failing after 10s
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 24s
2026-09-09 15:49:32 +02:00
dutchie031 1fde8ec3b1 updated luals version 2026-09-09 15:49:22 +02:00
dutchie031 68e8d1c9ac updated luals version
PR Prerequisites / Lua Check (pull_request) Failing after 12s
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 23s
2026-09-09 15:49:05 +02:00
dutchie031 ccd38078f3 updated luals version
PR Prerequisites / Lua Check (pull_request) Failing after 11s
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 24s
2026-09-09 15:47:50 +02:00
dutchie031 de1e6ffae9 updated luals version
PR Prerequisites / Lua Check (pull_request) Failing after 11s
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 29s
2026-09-09 15:37:39 +02:00
dutchie031 2a76df1e21 updated
PR Prerequisites / Lua Check (pull_request) Failing after 13s
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 27s
2026-09-09 15:36:02 +02:00
dutchie031 a55cd2bf2a trying other check
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 24s
PR Prerequisites / Lua Check (pull_request) Failing after 1m21s
2026-09-09 12:36:29 +02:00
dutchie031 d16e25531a non important trailing whitespace ignored
PR Prerequisites / Lua Check (pull_request) Failing after 24s
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 23s
2026-09-09 11:27:23 +02:00
dutchie031 30c7124a86 non important trailing whitespace ignored
PR Prerequisites / Lua Check (pull_request) Failing after 24s
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 26s
2026-09-09 11:25:23 +02:00
dutchie031 514a9a9c55 non important trailing whitespace ignored
PR Prerequisites / Lua Check (pull_request) Failing after 24s
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 24s
2026-09-09 11:24:09 +02:00
dutchie031 96fab864c3 non important trailing whitespace ignored
PR Prerequisites / Lua Check (pull_request) Failing after 21s
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 23s
2026-09-09 11:22:21 +02:00
dutchie031 0346f8a7a8 linter fixes
PR Prerequisites / Lua Check (pull_request) Failing after 20s
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 28s
2026-09-09 11:12:58 +02:00
dutchie031 ba240db310 fixing linting
PR Prerequisites / Lua Check (pull_request) Failing after 22s
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 22s
2026-09-08 21:04:40 +02:00
dutchie031 50f2a0eea0 added CI for all source files
PR Prerequisites / Lua Check (pull_request) Failing after 20s
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 24s
2026-09-08 17:35:21 +02:00
dutchie031 70d18f4ac3 updated luacheck rc
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 24s
2026-09-08 17:07:41 +02:00
dutchie031 341590e981 fixed minor bug
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 49s
2026-09-08 16:54:00 +02:00
dutchie031 233310209b changed checkout version
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 39s
2026-09-08 16:44:52 +02:00
dutchie031 2cae676a10 changed checkout version
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 6s
2026-09-08 16:41:35 +02:00
dutchie031 de81fdf419 added lua check CI step
PR Prerequisites / Bundles Without Errors (pull_request) Failing after 8s
2026-09-08 16:40:04 +02:00
dutchie031 a1355ff05d fixed minor bug
Publish Release / build (push) Failing after 12s
2026-09-08 07:44:24 +02:00
dutchie031 2e636b2ded minor udpdate to docs
Update Docs / prepare-docs (push) Successful in 30s
Update Docs / build-image (push) Successful in 9s
Update Docs / deploy-container (push) Successful in 13s
2026-09-08 07:25:29 +02:00
dutchie031 488a492e39 Custom briefings for BlueSam and FARPS (#44)
Update Docs / prepare-docs (push) Successful in 32s
Publish Release / build (push) Successful in 34s
Update Docs / build-image (push) Successful in 15s
Update Docs / deploy-container (push) Successful in 12s
Fixes #37

<img width="761" alt="image.png" src="attachments/bdc9d61a-8bbc-41f3-8de4-b97edfae28da">
<img width="1095" alt="image.png" src="attachments/9c77798d-f371-4035-84dd-7c60cb79d91f">

Reviewed-on: #44
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-09-07 17:23:30 +00:00
dutchie031 772397250f Feature/lanes (#40)
Update Docs / prepare-docs (push) Successful in 37s
Publish Release / build (push) Successful in 39s
Update Docs / build-image (push) Successful in 30s
Update Docs / deploy-container (push) Successful in 20s
this fixes #35

does not close but starts work partly on #41

Reviewed-on: #40
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-09-07 17:08:26 +00:00
dutchie031 0a7e8ea977 also added the beta block on home screen
Update Docs / prepare-docs (push) Successful in 21s
Update Docs / build-image (push) Successful in 12s
Update Docs / deploy-container (push) Successful in 12s
2026-08-30 18:25:03 +02:00
dutchie031 96507f1d34 updated the sidebar when it's a beta version
Update Docs / prepare-docs (push) Successful in 22s
Update Docs / build-image (push) Successful in 20s
Update Docs / deploy-container (push) Successful in 16s
2026-08-30 18:17:55 +02:00
dutchie031 96ada242a4 updated to change the release name so two releases can co-exist (#46)
Reviewed-on: #46
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-30 12:16:19 +00:00
dutchie031 2bce1985e2 beta.spearhead.rocks now for the beta branch (#45)
Reviewed-on: #45
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-30 11:49:52 +00:00
dutchie031 5601091b8f Merge branch 'main' into develop 2026-08-22 15:02:35 +02:00
dutchie031 18eff4b85a merged main 2026-08-20 18:50:24 +02:00
dutchie031 952daa2897 Cutting a release
Publish Release / build (push) Canceled after 22s
2026-08-20 16:58:43 +02:00
dutchie031 7c0c5e4a85 updated briefings to be shown during spawn (#31)
Publish Release / build (push) Successful in 22s
fixes #26

Reviewed-on: #31
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-20 14:06:42 +00:00
dutchie031 970f049440 Updated max distance to be configurable (#29)
Publish Release / build (push) Successful in 12s
Partly addresses #24

Does not fully fix thoughReviewed-on: #29

Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-18 18:23:07 +00:00
dutchie031 63a1ea44db Fix/ pre activation (#28)
Publish Release / build (push) Successful in 39s
fixes: #8Reviewed-on: #28

Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-18 12:50:12 +00:00
dutchie031 c8329108dd added link to the release
Publish Release / build (push) Successful in 14s
2026-08-17 12:31:25 +02:00
dutchie031 a88520a413 trigger beta branch
Publish Release / build (push) Successful in 14s
2026-08-17 12:14:32 +02:00
dutchie031 d8764c6824 Fix/briefing warnings (#20)
Reviewed-on: #20
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-17 10:13:25 +00:00
dutchie031 d0e7f2aae4 fixed briefings faulty giving warnings (#19)
Publish Release / build (push) Successful in 15s
Changed order of checking for mission briefings

fixes #9Reviewed-on: #19

Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-17 10:12:24 +00:00
dutchie031 8c75ba5027 Fix/custom drawings (#18)
Publish Release / build (push) Successful in 13s
Reviewed-on: #18
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-17 09:32:01 +00:00
dutchie031 52f97f7c92 webhook embed in release action
Publish Release / build (push) Successful in 13s
2026-08-16 14:45:14 +02:00
dutchie031 e5abfe19d6 webhook embed in release action 2026-08-16 14:44:55 +02:00
dutchie031 b34918ff28 webhook embed in release action
Publish Release / build (push) Successful in 56s
2026-08-16 14:40:49 +02:00
dutchie031 ab910b8faf webhook embed in release action 2026-08-16 14:38:46 +02:00
dutchie031 f677a83c8e Crate spawning update (#17)
Publish Release / build (push) Successful in 42s
Reviewed-on: #16
Co-authored-by: dutchie031 <timrorije@gmail.com>

Issue: #11

-[ ] Tested
-[ ] Reviewed
-[ ] Release notes updatedReviewed-on: #17
2026-08-16 11:55:34 +00:00
dutchie031 a1e36aa5d7 supplyHub Commands wiring and event triggers wired for more real time updates (#15)
Publish Release / build (push) Successful in 16s
Issue: #10

- [x] Tested
- [x] Reviewed
- [x] ReleaseNotes UpdatedReviewed-on: #15

Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-15 15:06:32 +00:00
dutchie031 bb9144e3a0 updating develop branch now automatically creates beta release (#16)
Reviewed-on: #16
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-14 18:57:33 +00:00
74 changed files with 5063 additions and 1021 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

+10
View File
@@ -21,6 +21,9 @@
<main> <main>
<div class="reference-container"> <div class="reference-container">
<div class="side-nav"> <div class="side-nav">
<div id="beta-box-div"></div>
<h4>Quick Links</h4> <h4>Quick Links</h4>
<ul> <ul>
<li><a href="/pages/tutorials.html" class="side-nav-h2">Tutorials</a></li> <li><a href="/pages/tutorials.html" class="side-nav-h2">Tutorials</a></li>
@@ -59,6 +62,13 @@
<footer> <footer>
<p>&copy; 2025 Spearhead Project</p> <p>&copy; 2025 Spearhead Project</p>
</footer> </footer>
<script>
if (window.location.hostname.toLowerCase() == 'beta.spearhead.rocks') {
betaHtml = '<note-box type="warning" title="Beta Version">You are looking at docs that are released with the beta version.</note-box>';
}
document.getElementById('beta-box-div').innerHTML = betaHtml || '';
</script>
</body> </body>
</html> </html>
+2 -1
View File
@@ -19,9 +19,10 @@ class Header extends HTMLElement {
<div class="dropdown-content"> <div class="dropdown-content">
<a href="/pages/include-script.html">Include the Script</a> <a href="/pages/include-script.html">Include the Script</a>
<a href="/pages/first-start.html">First Start</a> <a href="/pages/first-start.html">First Start</a>
<a href="/pages/advanced/map-markings.html">Map Markings</a>
<a href="/pages/advanced/CAP.html">Advanced: CAP</a> <a href="/pages/advanced/CAP.html">Advanced: CAP</a>
<a href="/pages/advanced/missions.html">Advanced: Missions</a> <a href="/pages/advanced/missions.html">Advanced: Missions</a>
<a href="/pages/advanced/map-markings.html">Map Markings</a> <a href="/pages/advanced/lanes.html">Advanced: Lanes</a>
</div> </div>
</div> </div>
@@ -49,17 +49,6 @@ class DownloadScript extends HTMLElement {
return null; return null;
} }
} }
async fetchReleaseAssets(url){
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching release assets:', error);
return null;
}
}
async connectedCallback() { async connectedCallback() {
@@ -72,10 +61,9 @@ class DownloadScript extends HTMLElement {
if (latestRelease) { if (latestRelease) {
version = latestRelease.tag_name; version = latestRelease.tag_name;
timestamp = new Date(latestRelease.published_at).toLocaleDateString('en-CA'); timestamp = new Date(latestRelease.published_at).toLocaleDateString('en-CA');
const assets = latestRelease.assets;
const assets = await this.fetchReleaseAssets(latestRelease.assets_url);
if (assets && assets.length > 0) { if (assets && assets.length > 0) {
const spearheadAsset = assets.find(asset => asset.name.includes('spearhead')); const spearheadAsset = assets.find(asset => asset.name.includes('spearhead.v'));
if (spearheadAsset) { if (spearheadAsset) {
size = (spearheadAsset.size / 1024).toFixed(2) + 'kb'; size = (spearheadAsset.size / 1024).toFixed(2) + 'kb';
href = spearheadAsset.browser_download_url; href = spearheadAsset.browser_download_url;
@@ -105,7 +93,7 @@ class DownloadScript extends HTMLElement {
<p> <p>
<span class="version-text">${version}</span> (${timestamp}) <br> <span class="version-text">${version}</span> (${timestamp}) <br>
<a style="text-decoration: underline;" target="_blank" href="${href}">Download</a> (${size}) <br> <a style="text-decoration: underline;" target="_blank" href="${href}">Download</a> (${size}) <br>
<a style="text-decoration: underline;" target="_blank" href="https://git.dutchie031.com/api/v1/repos/Spearhead/spearhead/releases">See All Versions here</a> <a style="text-decoration: underline;" target="_blank" href="https://git.dutchie031.com/Spearhead/spearhead/releases">See All Versions here</a>
</p> </p>
</div> </div>
+9
View File
@@ -168,8 +168,17 @@ class Sidebar extends HTMLElement {
} }
render() { render() {
var betaHtml = '';
if (true || window.location.hostname.toLowerCase() == 'beta.spearhead.rocks') {
betaHtml = '<note-box type="warning" title="Beta Version">You are looking at docs that are released with the beta version.</note-box>';
}
this.innerHTML = ` this.innerHTML = `
<div class="side-nav"> <div class="side-nav">
<div style="width:100%">
${betaHtml}
</div>
<h4 class="side-nav-title"></h4> <h4 class="side-nav-title"></h4>
<ul> <ul>
<!-- Navigation items will be populated automatically --> <!-- Navigation items will be populated automatically -->
+16 -7
View File
@@ -80,17 +80,12 @@
</tbody> </tbody>
</table> </table>
<note-box type="info" title="Combining Tasks"> <note-box type="info" title="Combining Tasks">
Currently a group can do 1 thing. Either be Primary, Seconday, Sweep or Intercept. <br> Currently a group can do 1 thing. Either be Primary, Seconday, Sweep or Intercept. <br>
If you want to have different types during different stages, it's best to copy paste groups and If you want to have different types during different stages, it's best to copy paste groups and
rename them accordingly. <br> rename them accordingly. <br>
</note-box> </note-box>
<note-box type="info" title="Airfield Defenses">
</note-box>
<h3 id="cap-zones-ref">Zones</h3> <h3 id="cap-zones-ref">Zones</h3>
<table class="table-no-style"> <table class="table-no-style">
<tbody> <tbody>
@@ -99,6 +94,7 @@
<td>=></td> <td>=></td>
<td>A Cap ROUTE. The two most outer points are used to define the fly-over points.</td> <td>A Cap ROUTE. The two most outer points are used to define the fly-over points.</td>
</tr> </tr>
<tr><td>______________________</td><td></td><td></td> </tr>
<tr> <tr>
<td><code-inline>INTERCEPTZONE_[ZoneID]_[NAME]</code-inline></td> <td><code-inline>INTERCEPTZONE_[ZoneID]_[NAME]</code-inline></td>
<td>=></td> <td>=></td>
@@ -112,6 +108,19 @@
</p> </p>
<h2 id="setup">Setting up groups</h2> <h2 id="setup">Setting up groups</h2>
<h3 id="airbases-grouping">Airbases</h3>
<p>
Before talking about the groups, it's good to mention that groups are currently logically grouped and managed by an Airbase. <br>
Meaning, Airbase A does not know how many flights Airbase B is sending out. <br>
This might change in the future where multiple Airbases can work together.
</p>
<note-box type="info" title="Airbase Defenses">
When an Airbase is "active" and thus has aircraft being scheduled for taskings, all RED groups at that airfield will also be spawned. <br>
Additionally, all missions of type <code-inline>SAM</code-inline> will be spawned (without being available in Primary Missions). <br>
This should hopefully give you all the tools to make distance CAP airbases still feel very well defended.
</note-box>
<h3 id="types-explained">Types explained</h3> <h3 id="types-explained">Types explained</h3>
<p> <p>
Before we go into depth on each type it's best to first look at the types briefly and what we Before we go into depth on each type it's best to first look at the types briefly and what we
@@ -140,7 +149,7 @@
The ground attack units mentioned are not in yet and thus escort groups are not either. <br> The ground attack units mentioned are not in yet and thus escort groups are not either. <br>
<note-box type="warning" title="Overriding Taskings"> <note-box type="warning" title="Overriding Taskings">
Each tasking has very specific waypoints, both for departure, arrical and ofcourse all point in between. <br> Each tasking has very specific waypoints, both for departure, arrival and ofcourse all point in between. <br>
Overriding the "tasking" will break part of the cap flow as "RTBInTen" and RTB events are triggered by waypoints. <br> Overriding the "tasking" will break part of the cap flow as "RTBInTen" and RTB events are triggered by waypoints. <br>
Overriding these waypoints and taskings will break the management by Spearhead. Overriding these waypoints and taskings will break the management by Spearhead.
</note-box> </note-box>
@@ -154,7 +163,7 @@
The way the trigger zone is mapped to a route is as follows: <br> The way the trigger zone is mapped to a route is as follows: <br>
<br> <br>
The 2 points that are furthest apart from each other are chosen as the "leg" of the route. <br> The 2 points that are furthest apart from each other are chosen as the "leg" of the route. <br>
The other 2 points are discarded. <br> The other 2 points are discarded at this time. <br>
<br> <br>
The Orbit route is then created with an "Anchored" task. With the closest point being the starting The Orbit route is then created with an "Anchored" task. With the closest point being the starting
point with an outbound leg to the furthest point. <br> point with an outbound leg to the furthest point. <br>
+183
View File
@@ -0,0 +1,183 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead Lanes</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>
<app-header></app-header>
</header>
<main>
<div class="reference-container">
<app-sidebar></app-sidebar>
<div class="content-wrapper">
<h1>Lanes</h1>
<p>
If up to now you've used mission stages like <code-inline>MISSIONSTAGE_1_STAGE</code-inline> you've been using the main lane. <br>
This is the default or "main" lane, however, if you wanted to mix things up and create a more dynamic mission, you can use additional lanes. <br>
Each lane can have its own set of mission stages, allowing for parallel or branching mission objectives.
</p>
<h2 id="how-it-works">How To</h2>
<h3 id="define-lanes">Define Lanes</h3>
<p>
Lanes are defined implicitly in the naming convention of the <code-inline>MISSIONSTAGE_</code-inline>. <br>
A lane is identified by a single character (alpha [A-Z]), which gives you 26 unique lanes. <br>
For example, <code-inline>MISSIONSTAGE_A1_[name]</code-inline> would belong to lane A. <br>
<br>
If there is no lane character specified, the stage belongs to the main lane. <br>
<br>
The 26 lane limit is only relevant if you want 26 lanes at the same time. <br>
But splitting up lanes (eg. (1-4 and 10-15) ) you can easily have more than 26 stages throughout your entire mission. <br>
<br>
Lanes don't have to start at index 1. as in the example lanes can easily start at any index. <br>
How lanes are then activated and activate other lanes is described below in the Dependencies Explained part.
</p>
<h3 id="dependencies">Dependencies Explained</h3>
<p>
When we are talking about lanes it's important to make the distinction between the main lane and additional lanes. <br>
The main lane is not only the default, but it's also the central lane that other lanes depend on. <br>
<h4>Chapters</h4>
Another term that we have used in the code is `Chapter` which refers to a grouping of mission stages within a lane. <br>
These are implied and cannot be directly controlled or altered. <br>
A chapter is a set of `MissionStage`s that are directly connected. <br>
For <code-inline>MissionStages</code-inline> <code-inline>[ A1, A2, A3, A5, A6 ]</code-inline> there are 2 chapters: <code-inline>[ A1, A2, A3 ]</code-inline> and <code-inline>[ A5, A6 ]</code-inline>. <br>
Within a chapter the stages are only directly dependent on each other. Meaning A2 will start when A1 is completed. <br>
<br>
However, the chapter start is where the inter-lane dependencies come into play. <br>
These operate a little differently. <br>
<br>
In the Visual example you can see the Stages grouped into chapters by the red boxes. <br>
<h4>Side Lane Chapter start</h4>
<p>
Side lane chapters are started when all are true:
<ul>
<li>The main lane is at or above the current stage lane number.</li>
<li>All previous chapters in the same side lane have been completed.</li>
</ul>
</p>
<h4>Main Lane Chapter start</h4>
<p>
Main lane chapters are started when all are true:
<ul>
<li>The previous chapter in the main lane has been completed.</li>
<li>All side lanes are at or above the current stage lane number or the side lane has no additional stages.</li>
</ul>
</p>
</p>
<h2 id="visual">Visual</h2>
<p>
The image below illustrates how different lanes can be used within a mission. <br>
In the example you can see how the dependency between the main lane and side lanes works. <br>
An arrow means that the completion of one stage is required before the next stage can begin.
</p>
<img src="/img/lanes.png" alt="Example of mission lanes">
<h2 id="mermaid">Mermaid</h2>
<p>
Spearhead automatically exports a spearhead diagram to the logs on starting a mission. <br>
This can be handy to visualize the logic and lanes you've created and verify your mission flow will be the way you want it. <br>
</p>
<h3 id="mermaid-log-example">Log Example</h3>
<code-block>
2026-09-01 17:53:13.213 INFO SCRIPTING (Main): [Spearhead][StageManager] ========== STAGE FLOW DIAGRAM ==========
graph TD
default_8["MAIN-2 (S8)"]
b_5["West Valley (S5)"]
default_1["MAIN-1 (S1)"]
a_5["WEST-2 (S5)"]
default_4["MAIN-2 (S4)"]
default_10["MAIN-1 (S10)"]
default_6["MAIN-2 (S6)"]
a_4["WEST-1 (S4)"]
default_3["MAIN-1 (S3)"]
default_9["MAIN-3 (S9)"]
a_7["WEST-2 (S7)"]
a_3["WEST (S3)"]
default_5["MAIN-3 (S5)"]
b_6["West Valley-1 (S6)"]
default_61["CAP-Maykop (S61)"]
default_7["MAIN-1 (S7)"]
default_0["START (S0)"]
a_6["WEST-3 (S6)"]
default_60["CAP-Nalchik (S60)"]
default_2["MAIN (S2)"]
a_2["WEST-1 (S2)"]
a_2 --> a_3
a_3 --> a_4
a_4 --> a_5
a_5 --> a_6
a_6 --> a_7
default_0 --> default_1
default_1 --> default_2
default_2 --> default_3
default_3 --> default_4
default_4 --> default_5
default_5 --> default_6
default_6 --> default_7
default_7 --> default_8
default_8 --> default_9
default_9 --> default_10
default_10 -->|chapter| default_60
default_60 --> default_61
b_5 --> b_6
default_2 -->|unlock| a_2
default_5 -->|unlock| b_5
a_7 -->|gate| default_60
b_6 -->|gate| default_60
========== END DIAGRAM ==========
</code-block>
<h3 id="mermaid-diagram-example">Diagram Example</h3>
<p>
If you want to then copy and paste everything between the <br>
<code-inline>========== STAGE FLOW DIAGRAM ==========</code-inline> and <br>
<code-inline>========== END DIAGRAM ==========</code-inline> <br>
you will be able to go to <a href="https://mermaid.live/">https://mermaid.live/</a> and paste the diagram code into the editor. <br>
It would look like this:
</p>
<img src="/img/mermaid-preview.png" alt="Example of mermaid diagram">
<h2 id="lane-limitations">Limitations</h2>
<h3 class="lane-limitations-cap">CAP</h3>
<p>
Due to the CAP naming conventions, we've opted to currently have CAP only react with the main lane. <br>
This might mean a bit less flexibility when using lanes for major mission logic, however, it's a limitation we've accepted for now. <br>
In the future we might be able to extend CAP's interaction with other lanes as well. <br>
Currently <a href="https://git.dutchie031.com/Spearhead/spearhead/issues/47">this issue</a> can be tracked here. <br>
This does not mean having both CAP and side-lanes is out of the question, since you can still have that side-lanes scouted by CAP aircraft.
It just needs a bit more pre-planning on where and how those CAP flights fly. <br>
</p>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Spearhead Project</p>
</footer>
</body>
</html>
+7
View File
@@ -253,6 +253,13 @@
To make an airbase buildable add a text box inside of the trigger zone and name it: <code-inline>buildable_[freeform]</code-inline> <br/> To make an airbase buildable add a text box inside of the trigger zone and name it: <code-inline>buildable_[freeform]</code-inline> <br/>
Then in the text box type amount of kilo's you want to be transfered before the logistisc mission is complete. <br/> Then in the text box type amount of kilo's you want to be transfered before the logistisc mission is complete. <br/>
<br>
To add a custom briefing for a buildable zone, include a text box inside the trigger zone and name it: <code-inline>supplybriefing_[freeform]</code-inline>.
<code-inline>{{coords}}</code-inline> is highly recommended so the players can easily find their landing zone.<br>
Additionally, the amount of kilos required and delivered are always shown on the bottom, as well as a note to not land in the construction zone.
<br/>
<br> <br>
The base or zone will slowly build up with each crate giving both a nice view of it happening AND it's good for performance as there's not a big addition of objects at once. <br/> The base or zone will slowly build up with each crate giving both a nice view of it happening AND it's good for performance as there's not a big addition of objects at once. <br/>
Right now a crate takes 15 seconds to unpack per 500kg. Meaning 2 crates of 1000kg will be faster than 1 crate of 2000kg. <br/> Right now a crate takes 15 seconds to unpack per 500kg. Meaning 2 crates of 1000kg will be faster than 1 crate of 2000kg. <br/>
+2 -2
View File
@@ -262,7 +262,7 @@ header nav a:hover::after {
overflow: hidden; overflow: hidden;
top: 100%; /* Position below the dropdown */ top: 100%; /* Position below the dropdown */
left: 0; /* Align left edge */ left: 0; /* Align left edge */
min-width: 160px; min-width: 240px;
background-color: var(--color-bg-secondary); background-color: var(--color-bg-secondary);
box-shadow: var(--shadow-medium); box-shadow: var(--shadow-medium);
@@ -336,8 +336,8 @@ main {
} }
.side-nav { .side-nav {
width: 200px;
min-width: 200px; min-width: 200px;
max-width: 200px;
padding-left: 0; padding-left: 0;
overflow-y: auto; overflow-y: auto;
border-right: none; border-right: none;
+73
View File
@@ -0,0 +1,73 @@
name: PR Prerequisites
on:
pull_request:
branches:
- main
- develop
env:
LUA_VERSION: "5.1"
LUA_LANGUAGE_SERVER_VERSION: "3.19.1"
jobs:
bundles_without_error:
runs-on: ubuntu-latest
name: Bundles Without Errors
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Lua
uses: leafo/gh-actions-lua@v13
with:
luaVersion: ${{ env.LUA_VERSION }}
- name: Compile Spearhead
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-action@action/v0
with:
source-root: ./src
output-file: ./output/spearhead.lua
- name: Verify Compiled Lua Syntax
run: lua -e "assert(loadfile('./output/spearhead.lua'))"
- name: Set up Luarocks
uses: leafo/gh-actions-luarocks@v4
- name: Install luacheck
run: luarocks install luacheck
- name: Lint Compiled Output
run: luacheck ./output/spearhead.lua
lua_check:
runs-on: ubuntu-latest
name: Lua Check
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Lua
uses: leafo/gh-actions-lua@v13
with:
luaVersion: ${{ env.LUA_VERSION }}
- name: Install lua-language-server
run: |
mkdir -p ./bin
# For Ubuntu/Debian
wget https://github.com/LuaLS/lua-language-server/releases/download/${{ env.LUA_LANGUAGE_SERVER_VERSION }}/lua-language-server-${{ env.LUA_LANGUAGE_SERVER_VERSION }}-linux-x64.tar.gz
tar -xzf lua-language-server-*.tar.gz -C ./bin
rm lua-language-server-*.tar.gz
echo "Contents of ./bin directory:"
ls -R ./bin
- name: Verify Lua Language Server Installation
run: ./bin/lua-language-server --version
# - name: Run Lua Language Server
# run: ./lua-language-server-*/lua-language-server --check ./src --checklevel=Information
+19 -3
View File
@@ -4,13 +4,14 @@ on:
push: push:
branches: branches:
- main - main
- develop
paths: paths:
- '.docs/**' - '.docs/**'
env: env:
DOCKER_REGISTRY: registry.dutchie031.net DOCKER_REGISTRY: registry.dutchie031.net
DOCKER_IMAGE: spearhead-docs DOCKER_IMAGE: spearhead-docs
DOCKER_TAG: latest DOCKER_TAG: ${{ github.ref_name == 'main' && 'latest' || 'beta' }}
jobs: jobs:
## Run replacements and cleanups on the docs ## Run replacements and cleanups on the docs
@@ -36,7 +37,7 @@ jobs:
sed -i '/@@API_CODE@@/d' .docs/web/pages/spearheadapi.html sed -i '/@@API_CODE@@/d' .docs/web/pages/spearheadapi.html
rm .docs/web/pages/temp_api_code.html rm .docs/web/pages/temp_api_code.html
- name: Highlight API code and update HTML - name: Highlight Config code and update HTML
run: | run: |
pygmentize -f html -l lua -O noclasses,style=monokai ./config.lua > .docs/web/pages/temp_config_code.html pygmentize -f html -l lua -O noclasses,style=monokai ./config.lua > .docs/web/pages/temp_config_code.html
# Insert the highlighted code into the placeholder in spearheadapi.html # Insert the highlighted code into the placeholder in spearheadapi.html
@@ -105,15 +106,30 @@ jobs:
chmod 600 $HOME/.kube/config chmod 600 $HOME/.kube/config
cat $HOME/.kube/config cat $HOME/.kube/config
- name: Run Helm upgrade/install - name: Run Helm upgrade/install spearhead-docs (main)
if: github.ref_name == 'main'
run: | run: |
helm upgrade --install spearhead-docs .helm \ helm upgrade --install spearhead-docs .helm \
--namespace spearhead-docs \ --namespace spearhead-docs \
--create-namespace \ --create-namespace \
-f .helm/values.yaml \
--set image.repository=${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }} \ --set image.repository=${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }} \
--set image.tag=${{ env.DOCKER_TAG }} \ --set image.tag=${{ env.DOCKER_TAG }} \
--wait --wait
- name: Run Helm upgrade/install spearhead-docs (develop)
if: github.ref_name == 'develop'
run: |
helm upgrade --install spearhead-docs-beta .helm \
--namespace spearhead-docs \
--create-namespace \
-f .helm/values.yaml \
-f .helm/values.beta.yaml \
--set image.repository=${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }} \
--set image.tag=${{ env.DOCKER_TAG }} \
--wait
# - name: 'Deploy' # - name: 'Deploy'
# uses: deliverybot/helm@v1 # uses: deliverybot/helm@v1
# with: # with:
+1
View File
@@ -13,4 +13,5 @@ sources:
- https://git.dutchie031.com/Spearhead/Spearhead - https://git.dutchie031.com/Spearhead/Spearhead
maintainers: maintainers:
- name: Spearhead Team - name: Spearhead Team
+1 -1
View File
@@ -25,7 +25,7 @@ spec:
type: RuntimeDefault type: RuntimeDefault
containers: containers:
- name: {{ .Values.app }} - name: {{ .Values.app }}
image: {{ .Values.image.repository }}:{{ .Values.image.tag }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: {{ .Values.ports.containerPort }} - containerPort: {{ .Values.ports.containerPort }}
+10 -17
View File
@@ -14,7 +14,7 @@ spec:
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: {{ .Values.app }}-ingress name: "{{ .Values.app }}-ingress"
namespace: spearhead-docs namespace: spearhead-docs
annotations: annotations:
kubernetes.io/tls-acme: "true" kubernetes.io/tls-acme: "true"
@@ -22,28 +22,21 @@ metadata:
spec: spec:
ingressClassName: nginx ingressClassName: nginx
rules: rules:
- host: spearhead.dutchie031.com {{- range .Values.ingress.hosts }}
- host: {{ . }}
http: http:
paths: paths:
- path: / - path: /
pathType: Prefix pathType: Prefix
backend: backend:
service: service:
name: {{ .Values.app }} name: {{ $.Values.app }}
port: port:
number: {{ .Values.ports.servicePort }} number: {{ $.Values.ports.servicePort }}
- host: spearhead.rocks {{- end }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .Values.app }}
port:
number: {{ .Values.ports.servicePort }}
tls: tls:
- hosts: - hosts:
- spearhead.dutchie031.com {{- range .Values.ingress.hosts }}
- spearhead.rocks - {{ . }}
secretName: spearhead-docs-tls {{- end }}
secretName: "{{ .Values.app }}-docs-tls"
+9
View File
@@ -0,0 +1,9 @@
app: spearhead-docs-beta
image:
tag: "beta"
ingress:
hosts:
- beta.spearhead.rocks
+15 -10
View File
@@ -1,10 +1,15 @@
app: spearhead-docs app: spearhead-docs
image: image:
repository: registry.dutchie031.net/spearhead-docs repository: registry.dutchie031.net/spearhead-docs
tag: latest tag: latest
ports: ports:
servicePort: 80 servicePort: 80
containerPort: 80 containerPort: 80
ingress:
hosts:
- spearhead.dutchie031.com
- spearhead.rocks
+15 -1
View File
@@ -5,11 +5,25 @@
### New Features ### New Features
- Issue: #41
Adds Stage Complete debug methods for local testing of the Stage flow.
#40
- Issue: #35
Creating Lane option for stages.
Additionally to the primary stage lane, stages can now be created in different lanes.
These lanes can be used to create different stage progression flows and create different "side-stories".
#40
- Issue: #37
Now enabled the mission editor to add a custom briefing to the "Buildable" missions.
#44
### Bug Fixes ### Bug Fixes
## [0.13.0] 2026-08 ## [0.13.0] 2026-08
A good first release that finally has all major bugs fixed that were caused by the migration from both the underlying script transpiler and the migration to Gitea. <br> A good first release that finally has all major bugs fixed that were caused by the migration from both the underlying script transpiler and the migration to Gitea.
### Breaking Changes ### Breaking Changes
+6 -3
View File
@@ -4,6 +4,9 @@ SpearheadConfig = {
---DEBUG LOGGING ---DEBUG LOGGING
debugEnabled = false, -- default false debugEnabled = false, -- default false
--- DEBUG MENU
debugMenuEnabled = false, -- default false
--- The time briefings should be displayed by default. --- The time briefings should be displayed by default.
--- Players can always "Clear Messages" through the F10 menu, so setting it to a high value can be --- Players can always "Clear Messages" through the F10 menu, so setting it to a high value can be
briefingMessageDuration = 60, --default 60 briefingMessageDuration = 60, --default 60
@@ -12,7 +15,7 @@ SpearheadConfig = {
--quickly enable of disable the entire CAP Logic --quickly enable of disable the entire CAP Logic
--(you can also just rename all units to not be named "CAP_") --(you can also just rename all units to not be named "CAP_")
enabled = true, -- default true enabled = true, -- default true
--min ground speed for CAP aircraft during patrol --min ground speed for CAP aircraft during patrol
-- unit: knots -- unit: knots
minSpeed = 400, -- default 400 minSpeed = 400, -- default 400
@@ -59,7 +62,7 @@ SpearheadConfig = {
deathDelay = 1800, -- default 1800 deathDelay = 1800, -- default 1800
}, },
StageConfig = { StageConfig = {
-- management of stages and its missions. -- management of stages and its missions.
-- This is not related to CAP managers which will continue to work even if stage management is disabled -- This is not related to CAP managers which will continue to work even if stage management is disabled
enabled = true, -- default true enabled = true, -- default true
@@ -95,7 +98,7 @@ SpearheadConfig = {
}, },
Persistence = { Persistence = {
--- io and lfs cannot be sanitized in the MissionScripting.lua --- io and lfs cannot be sanitized in the MissionScripting.lua
--- enables or disables the persistence logic in spearhead --- enables or disables the persistence logic in spearhead
enabled = false, enabled = false,
-73
View File
@@ -1,73 +0,0 @@
--assert(loadfile("C:\\Repos\\DCS\\Spearhead\\dev\\dev_classes.lua"))()
local basePath = "C:\\Repos\\DCS\\Spearhead\\"
local classPath = basePath .. "classes\\"
assert(loadfile(classPath .. "_baseClasses\\Queue.lua"))()
assert(loadfile(classPath .. "spearhead_base.lua"))()
assert(loadfile(classPath .. "spearhead_routeutil.lua"))()
assert(loadfile(classPath .. "spearhead_events.lua"))()
assert(loadfile(classPath .. "spearhead_db.lua"))()
assert(loadfile(classPath .. "fleetClasses\\FleetGroup.lua"))()
assert(loadfile(classPath .. "fleetClasses\\GlobalFleetManager.lua"))()
assert(loadfile(classPath .. "helpers\\MizGroupsManager.lua"))()
assert(loadfile(classPath .. "helpers\\SpawnManager.lua"))()
assert(loadfile(classPath .. "configuration\\CapConfig.lua"))()
assert(loadfile(classPath .. "configuration\\StageConfig.lua"))()
assert(loadfile(classPath .. "stageClasses\\GlobalStageManager.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\baseMissions\\Mission.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\ZoneMission.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\RunwayStrikeMission.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\BuildableMission.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\BaseStage\\Stage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\PrimaryStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\ExtraStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\WaitingStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Groups\\SpearheadGroup.lua"))()
assert(loadfile(classPath .. "stageClasses\\Groups\\SpearheadSceneryObject.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\MissionCommandsHelper.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\SupplyConfig.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\SupplyUnitsTracker.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\BattleManager.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\abstract\\BuildableZone.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\StageBase.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\BlueSam.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\FarpZone.lua"))()
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"))()
assert(loadfile(classPath .. "persistence\\Persistence.lua"))()
-- Startup:
assert(loadfile(basePath .. "main.lua"))()
-79
View File
@@ -1,79 +0,0 @@
SpearheadConfig = {
debugEnabled = true,
CapConfig = {
--quickly enable of disable the entire CAP Logic
--(you can also just rename all units to not be named "CAP_")
enabled = true, -- default true
--min ground speed for CAP aircraft during patrol
-- unit: knots
minSpeed = 400, -- default 400
--max speed for CAP aircraft during patrol
-- unit: knots
maxSpeed = 500, -- default 500
--minAlt for aircraft on patrol
-- unit: feet
minAlt = 18000, -- default 18000
--maxAlt for aircraft on patrol
-- unit: feet
maxAlt = 28000, -- default 28000
--Delay for aircraft from touchdown to off the chocks.
-- unit: seconds
rearmDelay = 180, -- default 600
repairDelay = 600, -- default 600
--Delay for aircraft from death to takeoff.
--When the seconds remaining is the same at the rearmDelay it will be spawned on the ramp and follow the rearm logic.
-- !! Can not be lower than rearmDelay
-- unit: seconds
deathDelay = 1800, -- default 1800
},
StageConfig = {
-- management of stages and its missions.
-- This is not related to CAP managers which will continue to work even if stage management is disabled
enabled = true, -- default true
--Will draw the active and the next stage
drawStages = true, -- default true
drawPreActivated = true,
markLastContact = true,
--AutoStages will continue to the next stage automatically on completion of the missions within the stage.
-- If you want to make it so the next stage triggers only when you want to disable it here and manually implement the actions needed.
--[[
TODO: Add manual stage transition documentation
]]
autoStages = true, --default true
--Maximum missions per stage (includes all types of missions)
maxMissionStage = 100,
--Stage starting number
startingStage = 1,
---DEBUG logging. Consider keeping this disabled
debugEnabled = true
},
Persistence = {
--- io and lfs cannot be sanitized in the MissionScripting.lua
--- enables or disables the persistence logic in spearhead
enabled = true,
--- sets the directory where the persistence file is stored
--- if nil then lfs.writedir() will be used.
--- which will
directory = nil ,
--- the filename of the persistence file. Should end with .json for convention, but any text extension should do.
fileName = "Spearhead_Persistence_Dev"
}
}
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -10,7 +10,6 @@ local MissionCompleteListeners = {}
---@class SpearheadAPIInternal ---@class SpearheadAPIInternal
---@field notifyMissionComplete fun(zone_name: string) ---@field notifyMissionComplete fun(zone_name: string)
---@type FullSpearheadAPI ---@type FullSpearheadAPI
SpearheadAPI = { SpearheadAPI = {
Stages = { Stages = {
@@ -25,12 +24,12 @@ SpearheadAPI = {
getCurrentStage = function() getCurrentStage = function()
return GlobalStageManager.getCurrentStage() or nil return GlobalStageManager.getCurrentStage() or nil
end, end,
isStageComplete = function(stageNumber) isStageComplete = function(stageNumber, stageLaneIdentifier)
if type(stageNumber) ~= "number" then if type(stageNumber) ~= "number" then
return false, "stageNumber " .. stageNumber .. " is not a valid number" return false, "stageNumber " .. stageNumber .. " is not a valid number"
end end
local isComplete = GlobalStageManager.isStageComplete(stageNumber) local isComplete = GlobalStageManager.isStageComplete(stageNumber, stageLaneIdentifier)
if isComplete == nil then if isComplete == nil then
return nil, "no stage found with number " .. stageNumber return nil, "no stage found with number " .. stageNumber
end end
+1 -2
View File
@@ -6,7 +6,7 @@ SpearheadAPI = SpearheadAPI
---@class SpearheadStagesAPI ---@class SpearheadStagesAPI
---@field changeStage fun(stageNumber: number): boolean, string @Changes the active stage of spearhead. <br/> All other stages will change based on the normal logic. (CAP, BLUE etc.) ---@field changeStage fun(stageNumber: number): boolean, string @Changes the active stage of spearhead. <br/> All other stages will change based on the normal logic. (CAP, BLUE etc.)
---@field getCurrentStage fun(): number | nil @Returns the current stange number <br/> Returns nil when the stagenumber was not set before ever, which means Spearhead was not started. ---@field getCurrentStage fun(): number | nil @Returns the current stange number <br/> Returns nil when the stagenumber was not set before ever, which means Spearhead was not started.
---@field isStageComplete fun(stageNumber: number): boolean | nil, string @returns whether a stage (by index) is complete. <br/> @param stageNumber number <br/> @return boolean | nil <br/> @return string ---@field isStageComplete fun(stageNumber: number, stageLaneIdentifier: string?): boolean | nil, string @returns whether a stage (by index) is complete. <br/> @param stageNumber number <br/> @param stageLaneIdentifier string? nil for default lane <br/> @return boolean | nil <br/> @return string
---@class OnMissionCompleteListener ---@class OnMissionCompleteListener
---@field onMissionComplete fun(self: OnMissionCompleteListener, zone_name: string) @Called when a mission is completed. @return void ---@field onMissionComplete fun(self: OnMissionCompleteListener, zone_name: string) @Called when a mission is completed. @return void
@@ -14,4 +14,3 @@ SpearheadAPI = SpearheadAPI
---@class MissionAPI ---@class MissionAPI
---@field addOnMissionCompleteListener fun(listener: OnMissionCompleteListener) @Adds a listener to the mission. <br/> @param listener OnMissionCompleteListener <br/> @return void ---@field addOnMissionCompleteListener fun(listener: OnMissionCompleteListener) @Adds a listener to the mission. <br/> @param listener OnMissionCompleteListener <br/> @return void
+21 -22
View File
@@ -2,7 +2,6 @@ local Util = require("classes.util.Util")
local CapGroup = require("classes.capClasses.airGroups.CapGroup") local CapGroup = require("classes.capClasses.airGroups.CapGroup")
local SweepGroup = require("classes.capClasses.airGroups.SweepGroup") local SweepGroup = require("classes.capClasses.airGroups.SweepGroup")
local InterceptGroup = require("classes.capClasses.airGroups.InterceptGroup") local InterceptGroup = require("classes.capClasses.airGroups.InterceptGroup")
local RunwayBombingTracker = require("classes.capClasses.runwayBombing.RunwayBombingTracker")
local SpearheadEvents = require("classes.spearhead_events") local SpearheadEvents = require("classes.spearhead_events")
local RunwayStrikeMission = require("classes.stageClasses.missions.RunwayStrikeMission") local RunwayStrikeMission = require("classes.stageClasses.missions.RunwayStrikeMission")
@@ -37,12 +36,11 @@ end
---@param database Database ---@param database Database
---@param logger table ---@param logger table
---@param capConfig table ---@param capConfig table
---@param stageConfig table
---@param runwayBombingTracker RunwayBombingTracker ---@param runwayBombingTracker RunwayBombingTracker
---@param detectionManager DetectionManager ---@param detectionManager DetectionManager
---@param spawnManager SpawnManager ---@param spawnManager SpawnManager
---@return CapBase ---@return CapBase
function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runwayBombingTracker, detectionManager, spawnManager) function CapBase.new(airbaseName, database, logger, capConfig, runwayBombingTracker, detectionManager, spawnManager)
CapBase.__index = CapBase CapBase.__index = CapBase
local self = setmetatable({}, { __index = CapBase }) --[[@as CapBase]] local self = setmetatable({}, { __index = CapBase }) --[[@as CapBase]]
@@ -61,7 +59,7 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runw
local baseData = database:getAirbaseDataForZone(airbaseName) local baseData = database:getAirbaseDataForZone(airbaseName)
if baseData and baseData.CapGroups then if baseData and baseData.CapGroups then
for key, name in pairs(baseData.CapGroups) do for _, name in pairs(baseData.CapGroups) do
local capGroup = CapGroup.New(name, capConfig, logger, spawnManager) local capGroup = CapGroup.New(name, capConfig, logger, spawnManager)
if capGroup then if capGroup then
self.capGroupsByName[name] = capGroup self.capGroupsByName[name] = capGroup
@@ -70,7 +68,7 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runw
end end
if baseData and baseData.SweepGroups then if baseData and baseData.SweepGroups then
for key, name in pairs(baseData.SweepGroups) do for _, name in pairs(baseData.SweepGroups) do
local sweepGroup = SweepGroup.New(name, capConfig, logger, spawnManager) local sweepGroup = SweepGroup.New(name, capConfig, logger, spawnManager)
if sweepGroup then if sweepGroup then
self.sweepGroupsByName[name] = sweepGroup self.sweepGroupsByName[name] = sweepGroup
@@ -79,7 +77,7 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runw
end end
if baseData and baseData.InterceptGroups then if baseData and baseData.InterceptGroups then
for key, name in pairs(baseData.InterceptGroups) do for _, name in pairs(baseData.InterceptGroups) do
local interceptGroup = InterceptGroup.New(name, capConfig, logger, detectionManager, spawnManager) local interceptGroup = InterceptGroup.New(name, capConfig, logger, detectionManager, spawnManager)
if interceptGroup then if interceptGroup then
self.interceptGroupsByName[name] = interceptGroup self.interceptGroupsByName[name] = interceptGroup
@@ -97,7 +95,6 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runw
SpearheadEvents.AddStageNumberChangedListener(self) SpearheadEvents.AddStageNumberChangedListener(self)
timer.scheduleFunction(CheckStateContinuous, self, timer.getTime() + 15) timer.scheduleFunction(CheckStateContinuous, self, timer.getTime() + 15)
return self return self
end end
@@ -127,7 +124,7 @@ end
function CapBase:SpawnIfApplicable() function CapBase:SpawnIfApplicable()
self.logger:debug("Check spawns for airbase " .. self.airbaseName) self.logger:debug("Check spawns for airbase " .. self.airbaseName)
for groupName, capGroup in pairs(self.capGroupsByName) do for _, capGroup in pairs(self.capGroupsByName) do
local targetStage = capGroup:GetZoneIDWhenStageID(tostring(self.activeStage)) local targetStage = capGroup:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetStage ~= nil and capGroup:GetState() == "UnSpawned" then if targetStage ~= nil and capGroup:GetState() == "UnSpawned" then
@@ -135,7 +132,7 @@ function CapBase:SpawnIfApplicable()
end end
end end
for groupName, sweepGroup in pairs(self.sweepGroupsByName) do for _, sweepGroup in pairs(self.sweepGroupsByName) do
local targetStage = sweepGroup:GetZoneIDWhenStageID(tostring(self.activeStage)) local targetStage = sweepGroup:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetStage ~= nil and sweepGroup:GetState() == "UnSpawned" then if targetStage ~= nil and sweepGroup:GetState() == "UnSpawned" then
@@ -143,7 +140,7 @@ function CapBase:SpawnIfApplicable()
end end
end end
for groupName, interceptGroup in pairs(self.interceptGroupsByName) do for _, interceptGroup in pairs(self.interceptGroupsByName) do
local targetStage = interceptGroup:GetZoneIDWhenStageID(tostring(self.activeStage)) local targetStage = interceptGroup:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetStage ~= nil and interceptGroup:GetState() == "UnSpawned" then if targetStage ~= nil and interceptGroup:GetState() == "UnSpawned" then
@@ -154,8 +151,9 @@ end
function CapBase:CheckAndScheduleCAP() function CapBase:CheckAndScheduleCAP()
self.logger:debug("Check taskings for airbase " .. self.airbaseName) self.logger:debug("Check taskings for airbase " .. self.airbaseName)
---@type table<string, number>
local countPerStage = {} local countPerStage = {}
---@type table<string, number>
local requiredPerStage = {} local requiredPerStage = {}
local airbase = Airbase.getByName(self.airbaseName) local airbase = Airbase.getByName(self.airbaseName)
@@ -170,7 +168,7 @@ function CapBase:CheckAndScheduleCAP()
if group:IsBackup() == true then if group:IsBackup() == true then
local state = group:GetState() local state = group:GetState()
if state == "InTransit" or state == "OnStation" or state == "RtbInTen" then if state == "InTransit" or state == "OnStation" or state == "RtbInTen" then
local supposedTargetZoneID = group:GetZoneIDWhenStageID(activeStageID) local supposedTargetZoneID = group:GetZoneIDWhenStageID(activeStageID)
local currentTargetZone = group:GetCurrentTargetZoneID() local currentTargetZone = group:GetCurrentTargetZoneID()
@@ -192,7 +190,7 @@ function CapBase:CheckAndScheduleCAP()
end end
end end
end end
if countPerStage[supposedTargetZoneID] == nil then if countPerStage[supposedTargetZoneID] == nil then
countPerStage[supposedTargetZoneID] = 0 countPerStage[supposedTargetZoneID] = 0
end end
@@ -231,7 +229,7 @@ function CapBase:CheckAndScheduleCAP()
countPerStage[supposedZone] = countPerStage[supposedZone] + 1 countPerStage[supposedZone] = countPerStage[supposedZone] + 1
end end
elseif state == "InTransit" or state == "OnStation" then elseif state == "InTransit" or state == "OnStation" then
if supposedZone ~= group:GetCurrentTargetZoneID() then if supposedZone ~= group:GetCurrentTargetZoneID() then
if countPerStage[supposedZone] < requiredPerStage[supposedZone] then if countPerStage[supposedZone] < requiredPerStage[supposedZone] then
local triggerZone = self.database:GetCapZoneForZoneID(supposedZone) local triggerZone = self.database:GetCapZoneForZoneID(supposedZone)
@@ -273,7 +271,7 @@ function CapBase:CheckAndScheduleCAP()
if triggerZone then if triggerZone then
group:SendToZone(triggerZone, supposedZone, airbase) group:SendToZone(triggerZone, supposedZone, airbase)
end end
countPerStage[supposedZone] = countPerStage[supposedZone] + 1 countPerStage[supposedZone] = countPerStage[supposedZone] + 1
end end
end end
@@ -312,8 +310,7 @@ function CapBase:CheckAndScheduleIntercept()
self.logger:debug("Check intercept taskings for airbase " .. self.airbaseName) self.logger:debug("Check intercept taskings for airbase " .. self.airbaseName)
local interceptZoneIDs = {} ---@type table<string, boolean>
local interceptZoneIDs = {} local interceptZoneIDs = {}
local airbase = Airbase.getByName(self.airbaseName) local airbase = Airbase.getByName(self.airbaseName)
@@ -321,7 +318,7 @@ function CapBase:CheckAndScheduleIntercept()
return nil return nil
end end
for name, group in pairs(self.interceptGroupsByName) do for _, group in pairs(self.interceptGroupsByName) do
local targetZoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage)) local targetZoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetZoneID then if targetZoneID then
interceptZoneIDs[targetZoneID] = true interceptZoneIDs[targetZoneID] = true
@@ -357,7 +354,7 @@ function CapBase:CheckAndScheduleIntercept()
end end
end end
end end
---RATIO. Amount of enemy fighters required per zone before another group gets added. ---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 local ratio = 4 -- Ratio of units to intercept per zone, can be adjusted
@@ -379,7 +376,6 @@ function CapBase:CheckAndScheduleIntercept()
if total < required then if total < required then
for _, group in pairs(self.interceptGroupsByName) do for _, group in pairs(self.interceptGroupsByName) do
if total < required then if total < required then
local zoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage))
if group:GetState() == "ReadyOnTheRamp" then if group:GetState() == "ReadyOnTheRamp" then
group:SendToInterceptUnits(targets, name, airbase) group:SendToInterceptUnits(targets, name, airbase)
total = total + 1 total = total + 1
@@ -392,9 +388,12 @@ function CapBase:CheckAndScheduleIntercept()
end end
end end
end end
end end
function CapBase:OnStageNumberChanged(number)
function CapBase:OnStageNumberChanged(number, laneIdentifier)
-- only react on "main" lane changes, ignore other lanes for now
if laneIdentifier ~= nil then return end
self.activeStage = number self.activeStage = number
if self:IsBaseActiveWhenStageIsActive(number) == true then if self:IsBaseActiveWhenStageIsActive(number) == true then
+4 -9
View File
@@ -8,19 +8,15 @@ local GlobalCapManager = {}
do do
local airbasesPerStage = {} local airbasesPerStage = {}
local allAirbasesByName = {} local allAirbasesByName = {}
local activeAirbasesPerActiveStage = {}
local unitsPerzonePerStage = {}
local initiated = false local initiated = false
---comment ---comment
---@param database Database ---@param database Database
---@param capConfig table ---@param capConfig table
---@param stageConfig StageConfig
---@param detectionManager DetectionManager ---@param detectionManager DetectionManager
---@param logLevel LogLevel ---@param logLevel LogLevel
---@param spawnManager SpawnManager ---@param spawnManager SpawnManager
function GlobalCapManager.start(database, capConfig, detectionManager, stageConfig, logLevel, spawnManager) function GlobalCapManager.start(database, capConfig, detectionManager, logLevel, spawnManager)
if initiated == true then return end if initiated == true then return end
local logger = Logger.new("AirbaseManager", logLevel) local logger = Logger.new("AirbaseManager", logLevel)
@@ -29,7 +25,7 @@ do
local zones = database:getStagezoneNames() local zones = database:getStagezoneNames()
if zones then if zones then
for key, stageName in pairs(zones) do for _, stageName in pairs(zones) do
if airbasesPerStage[stageName] == nil then if airbasesPerStage[stageName] == nil then
airbasesPerStage[stageName] = {} airbasesPerStage[stageName] = {}
end end
@@ -39,9 +35,8 @@ do
for _, airbaseName in pairs(airbaseNames) do for _, airbaseName in pairs(airbaseNames) do
if airbaseName then if airbaseName then
local airbaseSpecificLogger = Logger.new("CAP_" .. airbaseName, logLevel) local airbaseSpecificLogger = Logger.new("CAP_" .. airbaseName, logLevel)
local airbase = CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, stageConfig, runwayBombingTracker, detectionManager, spawnManager) local airbase = CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, runwayBombingTracker, detectionManager, spawnManager)
if airbase then if airbase then
table.insert(airbasesPerStage[stageName], airbase) table.insert(airbasesPerStage[stageName], airbase)
allAirbasesByName[airbaseName] = airbase allAirbasesByName[airbaseName] = airbase
+34 -7
View File
@@ -1,7 +1,9 @@
local SpearheadEvents = require("classes.spearhead_events") local SpearheadEvents = require("classes.spearhead_events")
local RTBMission = require("classes.capClasses.taskings.RTB") local RTBMission = require("classes.capClasses.taskings.RTB")
local Util = require("classes.util.Util") local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil") local GlobalConfig = require("classes.configuration.GlobalConfig")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
---@class AirGroup : OnUnitLostListener ---@class AirGroup : OnUnitLostListener
---@field protected _logger Logger ---@field protected _logger Logger
@@ -12,9 +14,12 @@ local DcsUtil = require("classes.util.DcsUtil")
---@field protected _config CapConfig ---@field protected _config CapConfig
---@field protected _checkLivenessNumber number ---@field protected _checkLivenessNumber number
---@field protected _spawnManager SpawnManager ---@field protected _spawnManager SpawnManager
---@field protected _routeDrawing CustomDrawing?
local AirGroup = {} local AirGroup = {}
AirGroup.__index = AirGroup AirGroup.__index = AirGroup
local globalConfig = GlobalConfig.New()
---@param logger Logger ---@param logger Logger
---@param groupName string ---@param groupName string
---@param groupType AirGroupType ---@param groupType AirGroupType
@@ -28,6 +33,7 @@ function AirGroup:New(groupName, groupType, config, logger, spawnManager)
self._config = config self._config = config
self._logger = logger self._logger = logger
self._spawnManager = spawnManager self._spawnManager = spawnManager
self._routeDrawings = {}
local group = Group.getByName(self._groupName) local group = Group.getByName(self._groupName)
if group then if group then
@@ -74,7 +80,7 @@ function AirGroup:SetMission(mission)
end end
end end
local setMissionDelayed = function(data, time) local setMissionDelayed = function(data, _)
data.self:SetMissionPrivate(data.mission) data.self:SetMissionPrivate(data.mission)
end end
@@ -92,6 +98,27 @@ function AirGroup:SetMissionPrivate(mission)
if group and mission then if group and mission then
group:getController():setTask(mission) group:getController():setTask(mission)
self._logger:debug("mission - Task set for group: " .. self._groupName) self._logger:debug("mission - Task set for group: " .. self._groupName)
if globalConfig:isDebugMenuEnabled() == true then
-- draw the mission route for debugging purposes
if self._routeDrawing then
self._routeDrawing:Remove()
end
local points = {}
if mission and mission.params and mission.params.route and mission.params.route.points then
for _, wp in pairs(mission.params.route.points) do
if wp.x and wp.y then
table.insert(points, { x = wp.x, y = wp.y })
end
end
end
local colorString = DrawingHelper.ColorTableToColorString({ 1, 0, 0, 1 })
self._routeDrawing = CustomDrawing.FromPoints(points, colorString, 5, 2)
self._routeDrawing:Draw()
end
end end
end end
@@ -329,7 +356,7 @@ do --- RESPAWN FUNCTIONS
self:SetState("Dead") self:SetState("Dead")
---@param selfA AirGroup ---@param selfA AirGroup
local respawnTask = function(selfA, time) local respawnTask = function(selfA, _)
selfA:StartRepair() selfA:StartRepair()
end end
@@ -348,8 +375,7 @@ do --- RESPAWN FUNCTIONS
end end
---comment ---comment
---@param selfA AirGroup local rearmTask = function(_, _)
local rearmTask = function(selfA, time)
self:StartRearm() self:StartRearm()
end end
@@ -374,7 +400,7 @@ do --- RESPAWN FUNCTIONS
end end
---@param selfA AirGroup ---@param selfA AirGroup
local rearmTask = function(selfA, time) local rearmTask = function(selfA, _)
selfA:MarkRearmComplete() selfA:MarkRearmComplete()
end end
@@ -387,6 +413,7 @@ end
do --EVENT LISTENERS do --EVENT LISTENERS
---@param unit Unit ---@param unit Unit
function AirGroup:OnUnitLost(unit) function AirGroup:OnUnitLost(unit)
if unit == nil then return end
self:CheckLiveness() self:CheckLiveness()
end end
@@ -406,7 +433,7 @@ do --EVENT LISTENERS
end end
end end
function AirGroup:OnUnitLanded(unit, airbase) function AirGroup:OnUnitLanded(_, _)
local anyInAir = false local anyInAir = false
local group = Group.getByName(self._groupName) local group = Group.getByName(self._groupName)
if group then if group then
@@ -103,17 +103,17 @@ function CapGroup:InitWithName(groupName)
return return
end end
local subsplit = Util.split_string(configPart, "|") local subsplit = Util.split_string(configPart, "|")
if subsplit then if subsplit then
for key, value in pairs(subsplit) do for _, value in pairs(subsplit) do
local keySplit = Util.split_string(value, "]") local keySplit = Util.split_string(value, "]")
local targetZone = keySplit[2] local targetZone = keySplit[2]
local allActives = string.sub(keySplit[1], 2, #keySplit[1]) local allActives = string.sub(keySplit[1], 2, #keySplit[1])
local commaSeperated = Util.split_string(allActives, ",") local commaSeperated = Util.split_string(allActives, ",")
for _, value in pairs(commaSeperated) do for _, childValue in pairs(commaSeperated) do
local dashSeperated = Util.split_string(value, "-") local dashSeperated = Util.split_string(childValue, "-")
if Util.tableLength(dashSeperated) > 1 then if Util.tableLength(dashSeperated) > 1 then
local from = tonumber(dashSeperated[1]) local from = tonumber(dashSeperated[1])
local till = tonumber(dashSeperated[2]) local till = tonumber(dashSeperated[2])
@@ -190,7 +190,7 @@ function InterceptGroup:UpdateTask()
self:SendRTB(self._airbase) self:SendRTB(self._airbase)
return nil -- Return to base if bingo fuel return nil -- Return to base if bingo fuel
end end
if selfDetected and self:IsInAir() == true then if selfDetected and self:IsInAir() == true then
if self._currentTargetName and self._currentTargetName == closestUnit:getName() then if self._currentTargetName and self._currentTargetName == closestUnit:getName() then
local distance = Util.VectorDistance3d(closestUnit:getPoint(), groupPoint) local distance = Util.VectorDistance3d(closestUnit:getPoint(), groupPoint)
@@ -213,8 +213,6 @@ function InterceptGroup:UpdateTask()
self._currentTargetName = nil self._currentTargetName = nil
end end
local speed = self._config:getMaxSpeed() local speed = self._config:getMaxSpeed()
local interceptPoint = self:GetInterceptPoint(groupPoint, speed, closestUnit) local interceptPoint = self:GetInterceptPoint(groupPoint, speed, closestUnit)
@@ -323,13 +321,13 @@ function InterceptGroup:InitWithName(groupName)
configPart = string.sub(configPart, 2, #configPart) configPart = string.sub(configPart, 2, #configPart)
local subsplit = Util.split_string(configPart, "|") local subsplit = Util.split_string(configPart, "|")
if subsplit then if subsplit then
for key, value in pairs(subsplit) do for _, value in pairs(subsplit) do
local keySplit = Util.split_string(value, "]") local keySplit = Util.split_string(value, "]")
local targetZone = keySplit[2] local targetZone = keySplit[2]
local allActives = string.sub(keySplit[1], 2, #keySplit[1]) local allActives = string.sub(keySplit[1], 2, #keySplit[1])
local commaSeperated = Util.split_string(allActives, ",") local commaSeperated = Util.split_string(allActives, ",")
for _, value in pairs(commaSeperated) do for _, childValue in pairs(commaSeperated) do
local dashSeperated = Util.split_string(value, "-") local dashSeperated = Util.split_string(childValue, "-")
if Util.tableLength(dashSeperated) > 1 then if Util.tableLength(dashSeperated) > 1 then
local from = tonumber(dashSeperated[1]) local from = tonumber(dashSeperated[1])
local till = tonumber(dashSeperated[2]) local till = tonumber(dashSeperated[2])
@@ -41,7 +41,7 @@ end
---@field self SweepGroup ---@field self SweepGroup
---@param params SetTaskParams ---@param params SetTaskParams
local setMissionDelayedTask = function(params, time) local setMissionDelayedTask = function(params, _)
params.self:SetMissionPrivate(params.task) params.self:SetMissionPrivate(params.task)
end end
@@ -50,8 +50,6 @@ function SweepGroup:SendToZone(zone, targetZoneID, airbase)
self._currentTargetZoneID = targetZoneID self._currentTargetZoneID = targetZoneID
local group = Group.getByName(self._groupName)
local mission = SWEEP.getAsMissionFromAirbase(self._groupName, airbase, zone, self._config) local mission = SWEEP.getAsMissionFromAirbase(self._groupName, airbase, zone, self._config)
if mission then if mission then
---@type SetTaskParams ---@type SetTaskParams
@@ -79,16 +77,16 @@ function SweepGroup:InitWithName(groupName)
local configPart = split_string[2] local configPart = split_string[2]
configPart = string.sub(configPart, 2, #configPart) configPart = string.sub(configPart, 2, #configPart)
local subsplit = Util.split_string(configPart, "|") local subsplit = Util.split_string(configPart, "|")
if subsplit then if subsplit then
for key, value in pairs(subsplit) do for _, value in pairs(subsplit) do
local keySplit = Util.split_string(value, "]") local keySplit = Util.split_string(value, "]")
local targetZone = keySplit[2] local targetZone = keySplit[2]
local allActives = string.sub(keySplit[1], 2, #keySplit[1]) local allActives = string.sub(keySplit[1], 2, #keySplit[1])
local commaSeperated = Util.split_string(allActives, ",") local commaSeperated = Util.split_string(allActives, ",")
for _, value in pairs(commaSeperated) do for _, childValue in pairs(commaSeperated) do
local dashSeperated = Util.split_string(value, "-") local dashSeperated = Util.split_string(childValue, "-")
if Util.tableLength(dashSeperated) > 1 then if Util.tableLength(dashSeperated) > 1 then
local from = tonumber(dashSeperated[1]) local from = tonumber(dashSeperated[1])
local till = tonumber(dashSeperated[2]) local till = tonumber(dashSeperated[2])
@@ -37,7 +37,6 @@ function DetectionManager.New(logger)
return time + 10 return time + 10
end end
timer.scheduleFunction(updateDetected, self, timer.getTime() + 130) timer.scheduleFunction(updateDetected, self, timer.getTime() + 130)
return self return self
end end
@@ -21,7 +21,7 @@ end
---comment ---comment
---@param weapon Weapon ---@param weapon Weapon
function RunwayBombingTracker:OnWeaponFired(unit, weapon, target) function RunwayBombingTracker:OnWeaponFired(_, weapon, _)
if weapon == nil then if weapon == nil then
return return
@@ -30,7 +30,7 @@ function RunwayBombingTracker:OnWeaponFired(unit, weapon, target)
local desc = weapon:getDesc() local desc = weapon:getDesc()
local isTrackable = desc.category == Weapon.Category.BOMB or (desc.category == Weapon.Category.MISSILE and desc.missileCategory == Weapon.MissileCategory.CRUISE) local isTrackable = desc.category == Weapon.Category.BOMB or (desc.category == Weapon.Category.MISSILE and desc.missileCategory == Weapon.MissileCategory.CRUISE)
if isTrackable == true then if isTrackable == true then
---@type WeaponTrackingArgs ---@type WeaponTrackingArgs
local weaponTrackingArgs = { local weaponTrackingArgs = {
weapon = weapon, weapon = weapon,
@@ -61,7 +61,7 @@ function RunwayBombingTracker.trackWeaponTask(weaponTrackingArgs, time)
local weapon = weaponTrackingArgs.weapon local weapon = weaponTrackingArgs.weapon
local self = weaponTrackingArgs.self local self = weaponTrackingArgs.self
if not weapon or weapon:isExist() == false then return nil end if not weapon or weapon:isExist() == false then return nil end
local pos = weapon:getPoint() local pos = weapon:getPoint()
@@ -83,11 +83,7 @@ function RunwayBombingTracker.trackWeaponTask(weaponTrackingArgs, time)
x = pos.x + velocity.x * nextInterval, x = pos.x + velocity.x * nextInterval,
y = pos.z + velocity.z * nextInterval y = pos.z + velocity.z * nextInterval
} }
self:OnWeaponImpact(weapon:getDesc(), impactPoint) self:OnWeaponImpact(weapon:getDesc(), impactPoint)
return nil return nil
end end
@@ -109,7 +105,7 @@ function RunwayBombingTracker:OnWeaponImpact(weaponDesc, impactPoint)
local warhead = weaponDesc.warhead local warhead = weaponDesc.warhead
local explosiveMass = warhead.explosiveMass or warhead.shapedExplosiveMass local explosiveMass = warhead.explosiveMass or warhead.shapedExplosiveMass
for runway, strikeMission in pairs(self.trackedRunways) do for _, strikeMission in pairs(self.trackedRunways) do
local zone= strikeMission:GetRunwayZone() local zone= strikeMission:GetRunwayZone()
-3
View File
@@ -33,9 +33,6 @@ local function GetCAPPointFromTriggerZone(airBase, capZone)
local furthestA = nil local furthestA = nil
local furthestB = nil local furthestB = nil
local furthestFromBase = nil
local furthestFromBaseDistance = 0
local furthestDistance = 0 local furthestDistance = 0
for indexA, pointA in ipairs(capZone.verts) do for indexA, pointA in ipairs(capZone.verts) do
@@ -38,7 +38,7 @@ function INTERCEPT.getMissionFromAirbase(groupName, interceptPoint, airbase, con
id = 'Mission', id = 'Mission',
params = { params = {
airborne = true, airborne = true,
route = { route = {
points = points points = points
} }
} }
@@ -159,10 +159,10 @@ function INTERCEPT.getInterceptTaskPoint(groupName, currentPoint, targetPoint, a
params = { params = {
tasks = { tasks = {
id = 'EngageTargetsInZone', id = 'EngageTargetsInZone',
params = { params = {
point = targetPoint, point = targetPoint,
zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone. zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone.
targetTypes = { targetTypes = {
[1] = "Planes", [1] = "Planes",
}, },
priority = 0 priority = 0
@@ -188,10 +188,10 @@ function INTERCEPT.getInterceptTaskPoint(groupName, currentPoint, targetPoint, a
params = { params = {
tasks = { tasks = {
id = 'EngageTargetsInZone', id = 'EngageTargetsInZone',
params = { params = {
point = targetPoint, point = targetPoint,
zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone. zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone.
targetTypes = { targetTypes = {
[1] = "Planes", [1] = "Planes",
}, },
priority = 0 priority = 0
+3 -3
View File
@@ -47,7 +47,7 @@ local getRunwayIntoWindCourseRad = function(airbase)
else else
rad = 0 - rad rad = 0 - rad
end end
local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0} local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0}
local alignment = Util.vectorAlignment(windVec, runwayVec) local alignment = Util.vectorAlignment(windVec, runwayVec)
@@ -60,14 +60,14 @@ local getRunwayIntoWindCourseRad = function(airbase)
do --inverse do --inverse
local degree = math.deg(runway.course) local degree = math.deg(runway.course)
degree = (degree + 180) % 360 degree = (degree + 180) % 360
local rad = math.rad(degree) local rad = math.rad(degree)
if rad < 0 then if rad < 0 then
rad = math.abs(rad) rad = math.abs(rad)
else else
rad = 0 - rad rad = 0 - rad
end end
local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0} local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0}
local alignment = Util.vectorAlignment(windVec, runwayVec) local alignment = Util.vectorAlignment(windVec, runwayVec)
+7 -4
View File
@@ -1,11 +1,8 @@
local briefingMessageTime = nil
---@class GlobalConfig ---@class GlobalConfig
---@field private _briefingTime number ---@field private _briefingTime number
---@field private _debugEnabled boolean ---@field private _debugEnabled boolean
---@field private _debugMenuEnabled boolean
local GlobalConfig = {} local GlobalConfig = {}
GlobalConfig.__index = GlobalConfig; GlobalConfig.__index = GlobalConfig;
@@ -27,6 +24,8 @@ function GlobalConfig.New()
self._debugEnabled = false self._debugEnabled = false
end end
self._debugMenuEnabled = SpearheadConfig.debugMenuEnabled == true
end end
return self return self
@@ -40,6 +39,10 @@ function GlobalConfig:isDebugEnabled()
return self._debugEnabled or false return self._debugEnabled or false
end end
---@return boolean
function GlobalConfig:isDebugMenuEnabled()
return self._debugMenuEnabled or false
end
return GlobalConfig return GlobalConfig
@@ -0,0 +1,31 @@
---@class PersistenceConfig
---@field private _enabled boolean
---@field private directory string?
---@field private fileName string?
local PersistenceConfig = {}
function PersistenceConfig.new()
local self = setmetatable({}, { __index = PersistenceConfig })
if not SpearheadConfig then SpearheadConfig = {} end
if not SpearheadConfig.Persistence then SpearheadConfig.Persistence = {} end
self._enabled = SpearheadConfig.Persistence.enabled == true
self.directory = SpearheadConfig.Persistence.directory
self.fileName = SpearheadConfig.Persistence.fileName
return self
end
function PersistenceConfig:isEnabled()
return self._enabled == true
end
function PersistenceConfig:getDirectory()
return self.directory
end
function PersistenceConfig:getFileName()
return self.fileName
end
return PersistenceConfig
+1 -1
View File
@@ -18,7 +18,7 @@ local _logger = Logger.new("StageConfig", Logger.LogLevel)
---@return StageConfig ---@return StageConfig
local function new() local function new()
if SpearheadConfig == nil then if SpearheadConfig == nil then
_logger:warn("SpearheadConfig is nil, creating default SpearheadConfig") _logger:warn("SpearheadConfig is nil, creating default SpearheadConfig")
SpearheadConfig = {} SpearheadConfig = {}
end end
+79
View File
@@ -0,0 +1,79 @@
local Logger = require("classes.util.Logger")
local StageRepository = require("classes.stageClasses.StageRepository")
---@class DebugMenu
---@field private _logger Logger
---@field private _stageRepository StageRepository
local DebugMenu = {}
DebugMenu.__index = DebugMenu
function DebugMenu.new()
local self = setmetatable({}, DebugMenu)
self._logger = Logger.new("DebugMenu")
self._stageRepository = StageRepository.getInstance()
return self
end
local menuName = "Spearhead Debug"
local debugMenuPath = { [1] = menuName }
function DebugMenu:RegisterMenus()
missionCommands.addSubMenu(menuName, {})
local refresh = function(params)
local selfA = params.self
selfA:RefreshMenu()
end
missionCommands.addCommand("Refresh Menu", debugMenuPath, refresh, { self = self })
self:AddStageOptions()
end
function DebugMenu:RefreshMenu()
missionCommands.removeItem(debugMenuPath)
self:RegisterMenus()
end
function DebugMenu:AddStageOptions()
local stageMenuTable = missionCommands.addSubMenu("Stages", debugMenuPath)
local stageLanes = self._stageRepository:getAllStageLanes()
for _, stageLane in pairs(stageLanes) do
local stages = stageLane:GetStagesAtIndex(stageLane:GetActiveStageIndex())
if stages then
for _, stage in pairs(stages) do
local stageLaneId = stageLane:GetStageLaneIdentifier() or ""
local stageIndex = stage:GetStageIndex()
local stageName = stage:GetStageName()
local stageMenuName = stageLaneId .. stageIndex .. "_" .. stageName
local currentStageMenuTable = missionCommands.addSubMenu(stageMenuName, stageMenuTable)
---@class CompleteStageParams
---@field self DebugMenu
---@field stage Stage
---@param params CompleteStageParams
local completeStage = function(params)
local stageA = params.stage
local missions = stageA:GetMissions()
for _, mission in pairs(missions) do
mission:ForceMissionComplete()
end
end
---@type CompleteStageParams
local params = { self = self, stage = stage }
missionCommands.addCommand("Complete Stage", currentStageMenuTable, completeStage, params)
end
else
self._logger:info("No stages found for lane: " .. (stageLane:GetStageLaneIdentifier() or "default") .. " at index: " .. (stageLane:GetActiveStageIndex() or "nil"))
end
end
end
return DebugMenu
+48 -40
View File
@@ -4,37 +4,43 @@ local MissionEditorWarning = require("classes.util.MissionEditorWarnings")
local RouteUtil = require("classes.spearhead_routeutil") local RouteUtil = require("classes.spearhead_routeutil")
local SpearheadEvents = require("classes.spearhead_events") local SpearheadEvents = require("classes.spearhead_events")
---@class FleetGroup ---@class FleetGroup : OnStageChangedListener
---@field private fleetGroupName string
---@field private logger Logger
---@field private fleetNameIdentifier string
---@field private targetZonePerStage table<string, string>
---@field private currentTargetZone string
---@field private pointsPerZone table<string, {pointA: Vec2, pointB: Vec2}>
local FleetGroup = {} local FleetGroup = {}
FleetGroup.__index = FleetGroup
---comment ---comment
---@param fleetGroupName string ---@param fleetGroupName string
---@param database Database ---@param database Database
---@param logger Logger ---@param logger Logger
---@return nil ---@return FleetGroup?
function FleetGroup:new(fleetGroupName, database, logger) function FleetGroup.new(fleetGroupName, database, logger)
local o = {}
setmetatable(o, { __index = self }) local self = setmetatable({}, FleetGroup)
o.fleetGroupName = fleetGroupName self.fleetGroupName = fleetGroupName
o.logger = logger self.logger = logger
local split_name = Util.split_string(fleetGroupName, "_") local split_name = Util.split_string(fleetGroupName, "_")
if Util.tableLength(split_name) < 2 then if Util.tableLength(split_name) < 2 then
MissionEditorWarning.Add("CARRIERGROUP should have at least 2 parts. CARRIERGROUP_<fleetname>") MissionEditorWarning.Add("CARRIERGROUP should have at least 2 parts. CARRIERGROUP_<fleetname>")
return nil return nil
end end
o.fleetNameIdentifier = split_name[2] self.fleetNameIdentifier = split_name[2]
o.targetZonePerStage = {} self.targetZonePerStage = {}
o.currentTargetZone = nil self.currentTargetZone = nil
o.pointsPerZone = {} self.pointsPerZone = {}
do --INIT do --INIT
local carrierRouteZones = database:getCarrierRouteZones() local carrierRouteZones = database:getCarrierRouteZones()
for _, zoneName in pairs(carrierRouteZones) do for _, zoneName in pairs(carrierRouteZones) do
if Util.strContains(string.lower(zoneName), "_".. string.lower(o.fleetNameIdentifier) .. "_" ) == true then if Util.strContains(string.lower(zoneName), "_".. string.lower(self.fleetNameIdentifier) .. "_" ) == true then
local zone = DcsUtil.getZoneByName(zoneName) local zone = DcsUtil.getZoneByName(zoneName)
if zone and zone.zone_type == DcsUtil.ZoneType.Polygon then if zone and zone.zone_type == DcsUtil.ZoneType.Polygon then
local split_string = Util.split_string(zoneName, "_") local split_string = Util.split_string(zoneName, "_")
@@ -43,18 +49,18 @@ function FleetGroup:new(fleetGroupName, database, logger)
"CARRIERROUTE should at least have 3 parts. Check the documentation for: " .. zoneName) "CARRIERROUTE should at least have 3 parts. Check the documentation for: " .. zoneName)
else else
---@param zone SpearheadTriggerZone ---@param zoneA SpearheadTriggerZone
---@return Vec2, Vec2 ---@return Vec2, Vec2
local function GetTwoFurthestPoints(zone) local function GetTwoFurthestPoints(zoneA)
local biggest = nil local biggest = nil
local biggestA = zone.verts[1] local biggestA = zoneA.verts[1]
local biggestB = zone.verts[2] local biggestB = zoneA.verts[2]
for i = 1, 3 do for i = 1, 3 do
for ii = i + 1, 4 do for ii = i + 1, 4 do
local a = zone.verts[i] local a = zoneA.verts[i]
local b = zone.verts[ii] local b = zoneA.verts[ii]
local dist = Util.VectorDistance2d(a, b) local dist = Util.VectorDistance2d(a, b)
if biggest == nil or dist > biggest then if biggest == nil or dist > biggest then
@@ -100,9 +106,9 @@ function FleetGroup:new(fleetGroupName, database, logger)
local first, second = getMinMaxStage(split_string[3]) local first, second = getMinMaxStage(split_string[3])
if first ~= nil and second ~= nil then if first ~= nil and second ~= nil then
for i = first, second do for i = first, second do
o.targetZonePerStage[tostring(i)] = zoneName self.targetZonePerStage[tostring(i)] = zoneName
end end
o.pointsPerZone[zoneName] = { pointA = { x = pointA.x, z = pointA.y, y = 0 }, pointB = { x = pointB.x, z = pointB.y, y = 0} } self.pointsPerZone[zoneName] = { pointA = { x = pointA.x, z = pointA.y, y = 0 }, pointB = { x = pointB.x, z = pointB.y, y = 0} }
else else
MissionEditorWarning.Add("CARRIERROUTE zone stage numbers not in the format _[<number>-<number>]: " .. zoneName) MissionEditorWarning.Add("CARRIERROUTE zone stage numbers not in the format _[<number>-<number>]: " .. zoneName)
end end
@@ -114,30 +120,32 @@ function FleetGroup:new(fleetGroupName, database, logger)
end end
end end
local SetTaskAsync = function(input, time) SpearheadEvents.AddStageNumberChangedListener(self)
local targetZone = input.targetZone return self
local task = input.task end
local groupName = input.groupName
local logger = input.logger
local group = Group.getByName(groupName) local SetTaskAsync = function(input, _)
if group then local targetZone = input.targetZone
logger:info("Sending " .. groupName .. " to " .. targetZone) local task = input.task
group:getController():setTask(task) local groupName = input.groupName
end local l_logger = input.logger
local group = Group.getByName(groupName)
if group then
l_logger:info("Sending " .. groupName .. " to " .. targetZone)
group:getController():setTask(task)
end end
end
o.OnStageNumberChanged = function(self, number) function FleetGroup:OnStageNumberChanged(number, laneIdentifier)
local targetZone = self.targetZonePerStage[tostring(number)] -- only react on "main" lane changes, ignore other lanes for now
if targetZone and targetZone ~= self.currentTargetZone then if laneIdentifier ~= nil then return end
local points = self.pointsPerZone[targetZone] local targetZone = self.targetZonePerStage[tostring(number)]
local task = RouteUtil.CreateCarrierRacetrack(points.pointA, points.pointB) if targetZone and targetZone ~= self.currentTargetZone then
timer.scheduleFunction(SetTaskAsync, { task = task, targetZone = targetZone, groupName = self.fleetGroupName, logger = self.logger }, timer.getTime() + 5) local points = self.pointsPerZone[targetZone]
end local task = RouteUtil.CreateCarrierRacetrack(points.pointA, points.pointB)
timer.scheduleFunction(SetTaskAsync, { task = task, targetZone = targetZone, groupName = self.fleetGroupName, logger = self.logger }, timer.getTime() + 5)
end end
SpearheadEvents.AddStageNumberChangedListener(o)
return o
end end
return FleetGroup return FleetGroup
@@ -17,7 +17,7 @@ GlobalFleetManager.start = function(database)
for _, groupName in pairs(all_groups) do for _, groupName in pairs(all_groups) do
if Util.startswith(string.lower(groupName), "carriergroup" ) == true then if Util.startswith(string.lower(groupName), "carriergroup" ) == true then
logger:info("Registering " .. groupName .. " as a managed fleet") logger:info("Registering " .. groupName .. " as a managed fleet")
local carrierGroup = FleetGroup:new(groupName, database, logger) local carrierGroup = FleetGroup.new(groupName, database, logger)
table.insert(fleetGroups, carrierGroup) table.insert(fleetGroups, carrierGroup)
end end
end end
+3 -4
View File
@@ -15,14 +15,13 @@ MizGroupsManager._groupNames = {}
MizGroupsManager._spawnTemplateData = {} MizGroupsManager._spawnTemplateData = {}
do --init do --init
for coalition_name, coalition_data in pairs(env.mission.coalition) do for _, coalition_data in pairs(env.mission.coalition) do
local coalition_nr = DcsUtil.stringToCoalition(coalition_name)
if coalition_data.country then if coalition_data.country then
for country_index, country_data in pairs(coalition_data.country) do for _, country_data in pairs(coalition_data.country) do
for category_name, categorydata in pairs(country_data) do for category_name, categorydata in pairs(country_data) do
local category_id = DcsUtil.stringToGroupCategory(category_name) local category_id = DcsUtil.stringToGroupCategory(category_name)
if category_id ~= nil and type(categorydata) == "table" and categorydata.group ~= nil and type(categorydata.group) == "table" then if category_id ~= nil and type(categorydata) == "table" and categorydata.group ~= nil and type(categorydata.group) == "table" then
for group_index, group in pairs(categorydata.group) do for _, group in pairs(categorydata.group) do
local name = group.name local name = group.name
local skippable = false local skippable = false
local isStatic = false local isStatic = false
+6 -7
View File
@@ -33,7 +33,7 @@ end
---@return StaticObject|Group|nil spawned ---@return StaticObject|Group|nil spawned
---@return boolean isStatic ---@return boolean isStatic
function SpawnManager:SpawnGroup(groupName, overrides, isGroupPersistant) function SpawnManager:SpawnGroup(groupName, overrides, isGroupPersistant)
local spawnData = MizGroupsManager.getSpawnTemplateData(groupName) local spawnData = MizGroupsManager.getSpawnTemplateData(groupName)
if spawnData == nil then if spawnData == nil then
@@ -44,13 +44,13 @@ function SpawnManager:SpawnGroup(groupName, overrides, isGroupPersistant)
if spawnData.isStatic == true then if spawnData.isStatic == true then
return self:SpawnStaticInternal(groupName, spawnData, overrides, isGroupPersistant), true return self:SpawnStaticInternal(groupName, spawnData, overrides, isGroupPersistant), true
else else
return self:SpawnGroupInternal(groupName, spawnData, overrides, isGroupPersistant), false return self:SpawnGroupInternal(spawnData, overrides, isGroupPersistant), false
end end
end end
function SpawnManager:DestroyGroup(groupName) function SpawnManager:DestroyGroup(groupName)
if groupName == nil then return end if groupName == nil then return end
if self:IsGroupStatic(groupName) == true then if self:IsGroupStatic(groupName) == true then
local object = StaticObject.getByName(groupName) local object = StaticObject.getByName(groupName)
if object ~= nil then if object ~= nil then
@@ -103,20 +103,20 @@ function SpawnManager:OnUnitLost(object)
end end
end end
---@param groupName string
function SpawnManager:SpawnCorpsesOnly(groupName) function SpawnManager:SpawnCorpsesOnly(groupName)
if groupName == nil then return end
end end
do --- privates do --- privates
---@private ---@private
---@param groupName string
---@param spawnData SpawnData ---@param spawnData SpawnData
---@param override SpawnOverrides? ---@param override SpawnOverrides?
---@param isPersistent boolean? ---@param isPersistent boolean?
---@return Group|nil ---@return Group|nil
function SpawnManager:SpawnGroupInternal(groupName, spawnData, override, isPersistent) function SpawnManager:SpawnGroupInternal(spawnData, override, isPersistent)
if not spawnData then return end if not spawnData then return end
local country = spawnData.country local country = spawnData.country
@@ -181,7 +181,6 @@ do --- privates
return group return group
end end
return nil return nil
end end
+30 -19
View File
@@ -13,7 +13,8 @@ do
---@field unitsStates table<string, UnitState> ---@field unitsStates table<string, UnitState>
---@field random_missions table<string, MissionState> ---@field random_missions table<string, MissionState>
---@field deliveredKilos table<string, number> ---@field deliveredKilos table<string, number>
---@field activeStage integer|nil ---@field activeStage number? required for backwards compatibility
---@field activeStageInStageLane table<string, number>
---@class UnitState ---@class UnitState
---@field isDead boolean ---@field isDead boolean
@@ -33,7 +34,8 @@ do
unitsStates = {}, unitsStates = {},
random_missions = {}, random_missions = {},
deliveredKilos = {}, deliveredKilos = {},
activeStage = nil activeStage = nil, -- Backwards compatibility for previous version without lanes
activeStageInStageLane = {}
} }
local logger = {} local logger = {}
@@ -87,7 +89,7 @@ do
else else
logger:error("Could not load persistence file, using default tables") logger:error("Could not load persistence file, using default tables")
end end
if tables.version == nil then tables.version = version end if tables.version == nil then tables.version = version end
end end
@@ -110,9 +112,9 @@ do
logger:info("Wrote persistence data to file") logger:info("Wrote persistence data to file")
end end
local UpdateContinuous = function(null, time) local UpdateContinuous = function(_, time)
env.info("[Spearhead][Persistence] Checking up on persistence state...") env.info("[Spearhead][Persistence] Checking up on persistence state...")
if Persistence._updateRequired == true then if Persistence._updateRequired == true then
local status, result = pcall(writeToFile) local status, result = pcall(writeToFile)
if status == false then if status == false then
env.error("[Spearhead][Persistence] Could not write state to file: " .. result) env.error("[Spearhead][Persistence] Could not write state to file: " .. result)
@@ -133,7 +135,7 @@ do
end end
local warnForNonPersistenceContinous = function(null, time) local warnForNonPersistenceContinous = function(_, time)
trigger.action.outText("Persistence was enabeld, however, io and lfs are not available and no persistence will be done. Make sure to either disable persistence or fix the issues before continuing.", 10) trigger.action.outText("Persistence was enabeld, however, io and lfs are not available and no persistence will be done. Make sure to either disable persistence or fix the issues before continuing.", 10)
return time + 9 return time + 9
end end
@@ -192,7 +194,7 @@ do
split[#split+1] = "0" split[#split+1] = "0"
split[#split+1] = "spearhead" split[#split+1] = "spearhead"
end end
fileName = table.concat(split, ".") fileName = table.concat(split, ".")
end end
@@ -216,12 +218,12 @@ do
logger:info("No previous persistence file found, using default: " .. fileName) logger:info("No previous persistence file found, using default: " .. fileName)
end end
logger:info("New Persistence file name: " .. tostring(fileName)) logger:info("New Persistence file name: " .. tostring(fileName))
local lastPath = dir .. "\\" .. lastFile local lastPath = dir .. "\\" .. lastFile
local path = dir .. "\\" .. fileName local path = dir .. "\\" .. fileName
Persistence._path = path Persistence._path = path
createFileIfNotExists(path) createFileIfNotExists(path)
loadTablesFromFile(lastPath) loadTablesFromFile(lastPath)
timer.scheduleFunction(UpdateContinuous, nil, timer.getTime() + 120) timer.scheduleFunction(UpdateContinuous, nil, timer.getTime() + 120)
@@ -230,12 +232,29 @@ do
end end
---Sets the stage in the persistence table ---Sets the stage in the persistence table
---@param stageLane string?
---@param stageNumber number ---@param stageNumber number
Persistence.SetActiveStage = function(stageNumber) Persistence.SetActiveStage = function(stageLane, stageNumber)
tables.activeStage = stageNumber stageLane = stageLane or "nil"
tables.activeStageInStageLane[stageLane] = stageNumber
Persistence._updateRequired = true Persistence._updateRequired = true
end end
---Get the active stage as in the persistance file
---@param stageLane string?
---@return integer|nil
Persistence.GetActiveStage = function(stageLane)
stageLane = stageLane or "nil"
-- backwards compatibility for previous versions without stage lanes
if stageLane == "nil" and tables.activeStageInStageLane[stageLane] == nil then
tables.activeStageInStageLane[stageLane] = tables.activeStage
tables.activeStage = nil -- Clear the old activeStage so it's not used or confused later
end
return tables.activeStageInStageLane[stageLane]
end
---comment ---comment
---@param missionName string ---@param missionName string
---@param pickedZone string ---@param pickedZone string
@@ -255,14 +274,6 @@ do
return tables.random_missions[string.lower(missionName)] return tables.random_missions[string.lower(missionName)]
end end
---Get the active stage as in the persistance file
---@return integer|nil
Persistence.GetActiveStage = function()
if tables.activeStage then
return tables.activeStage
end
return nil
end
---comment ---comment
---@param zoneName string ---@param zoneName string
+69 -22
View File
@@ -2,7 +2,7 @@ local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil") local DcsUtil = require("classes.util.DcsUtil")
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings") local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
local MizGroupsManager = require("classes.helpers.MizGroupsManager") local MizGroupsManager = require("classes.helpers.MizGroupsManager")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing") local StageDrawing = require("classes.stageClasses.drawings.StageDrawing")
---@class DatabaseTables ---@class DatabaseTables
@@ -27,7 +27,7 @@ local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
---@field MissionZoneData table<string, MissionZoneData> ---@field MissionZoneData table<string, MissionZoneData>
---@field FarpZoneData table<string,FarpZoneData> ---@field FarpZoneData table<string,FarpZoneData>
---@field missionCodes table<string, boolean> ---@field missionCodes table<string, boolean>
---@field CustomDrawings Array<CustomDrawing> ---@field StageDrawings Array<StageDrawing>
---@class CapRoute ---@class CapRoute
---@field zones Array<SpearheadTriggerZone> ---@field zones Array<SpearheadTriggerZone>
@@ -58,6 +58,7 @@ local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
---@class BlueSamData ---@class BlueSamData
---@field groups Array<string> ---@field groups Array<string>
---@field buildingKilos number? ---@field buildingKilos number?
---@field briefing string?
---@class MissionZoneData ---@class MissionZoneData
---@field ZoneName string ---@field ZoneName string
@@ -74,6 +75,7 @@ local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
---@field padNames Array<string> ---@field padNames Array<string>
---@field buildingKilos number? ---@field buildingKilos number?
---@field supplyHubNames Array<string> ---@field supplyHubNames Array<string>
---@field briefing string?
---@class Database ---@class Database
---@field private _tables DatabaseTables ---@field private _tables DatabaseTables
@@ -107,7 +109,7 @@ function Database.New(Logger)
FarpZoneData = {}, FarpZoneData = {},
missionCodes = {}, missionCodes = {},
SupplyHubZones = {}, SupplyHubZones = {},
CustomDrawings = {} StageDrawings = {}
} }
Database.__index = Database Database.__index = Database
@@ -119,7 +121,7 @@ function Database.New(Logger)
self._logger:debug("Initiating tables") self._logger:debug("Initiating tables")
do -- INIT ZONE TABLES do -- INIT ZONE TABLES
for zone_ind, zone_data in pairs(DcsUtil.__trigger_zones) do for _, zone_data in pairs(DcsUtil.__trigger_zones) do
local zone_name = zone_data.name local zone_name = zone_data.name
---@type Vec2 ---@type Vec2
@@ -206,9 +208,9 @@ function Database.New(Logger)
self._logger:debug("initiated zone tables, continuing with descriptions") self._logger:debug("initiated zone tables, continuing with descriptions")
do --load markers do --load markers
if env.mission.drawings and env.mission.drawings.layers then if env.mission.drawings and env.mission.drawings.layers then
for i, layer in pairs(env.mission.drawings.layers) do for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then if string.lower(layer.name) == "author" then
for key, layer_object in pairs(layer.objects) do for _, layer_object in pairs(layer.objects) do
if Util.startswith(string.lower(layer_object.name), "buildable", true) == true then if Util.startswith(string.lower(layer_object.name), "buildable", true) == true then
local airbaseData = self:getAirbaseDataForDrawLayer(layer_object) local airbaseData = self:getAirbaseDataForDrawLayer(layer_object)
if airbaseData then if airbaseData then
@@ -226,13 +228,15 @@ function Database.New(Logger)
---@type table<integer, boolean> ---@type table<integer, boolean>
do -- custom drawings do -- custom drawings
if env.mission.drawings and env.mission.drawings.layers then if env.mission.drawings and env.mission.drawings.layers then
for i, layer in pairs(env.mission.drawings.layers) do for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then if string.lower(layer.name) == "author" then
for key, layer_object in pairs(layer.objects) do for _, layer_object in pairs(layer.objects) do
if Util.startswith(layer_object.name, "drawing_", true) then if Util.startswith(layer_object.name, "drawing_", true) then
local object = layer_object --[[@as DrawingObject]] local object = layer_object --[[@as DrawingObject]]
local customDrawing = CustomDrawing.New(object) local stageDrawing = StageDrawing.New(object)
table.insert(self._tables.CustomDrawings, customDrawing) if stageDrawing then
table.insert(self._tables.StageDrawings, stageDrawing)
end
end end
end end
end end
@@ -252,9 +256,9 @@ function Database.New(Logger)
-- Find and add Briefing -- Find and add Briefing
if env.mission.drawings and env.mission.drawings.layers then if env.mission.drawings and env.mission.drawings.layers then
for i, layer in pairs(env.mission.drawings.layers) do for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then if string.lower(layer.name) == "author" then
for key, layer_object in pairs(layer.objects) do for _, layer_object in pairs(layer.objects) do
if Util.startswith(string.lower(layer_object.name), "stagebriefing_", true) == true then if Util.startswith(string.lower(layer_object.name), "stagebriefing_", true) == true then
local zone = DcsUtil.getZoneByName(stageZoneName) local zone = DcsUtil.getZoneByName(stageZoneName)
local vec2 = { x = layer_object.mapX, y = layer_object.mapY } local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
@@ -335,14 +339,14 @@ function Database.New(Logger)
end end
-- fill missions -- fill missions
for key, missionZone in pairs(self._tables.MissionZones) do for _, missionZone in pairs(self._tables.MissionZones) do
if DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then if DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then
table.insert(stageData.MissionZones, missionZone) table.insert(stageData.MissionZones, missionZone)
end end
end end
-- fill random missions -- fill random missions
for key, missionZone in pairs(self._tables.RandomMissionZones) do for _, missionZone in pairs(self._tables.RandomMissionZones) do
if DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then if DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then
table.insert(stageData.RandomMissionZones, missionZone) table.insert(stageData.RandomMissionZones, missionZone)
end end
@@ -614,6 +618,27 @@ function Database:loadBlueSamUnits()
local number = tonumber(kvPair.value) local number = tonumber(kvPair.value)
if number and number > 0 then if number and number > 0 then
samData.buildingKilos = number samData.buildingKilos = number
if env.mission.drawings and env.mission.drawings.layers then
for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for _, layer_object in pairs(layer.objects) do
local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if triggerZone and Util.is2dPointInZone(vec2, triggerZone) then
if layer_object.name and Util.startswith(layer_object.name, "supplybriefing_", true) then
local description = layer_object.text
if description and description ~= "" then
samData.briefing = description
end
end
end
end
end
end
end
else
MissionEditorWarnings.Add("Buildable number for " .. blueSamZone .. " is invalid")
end end
end end
end end
@@ -621,12 +646,12 @@ function Database:loadBlueSamUnits()
end end
end end
---@return Array<CustomDrawing> ---@return Array<StageDrawing>
function Database:getCustomDrawings() function Database:getStageDrawings()
if self._tables.CustomDrawings == nil then if self._tables.StageDrawings == nil then
return {} return {}
end end
return self._tables.CustomDrawings return self._tables.StageDrawings
end end
---Loads all units, data and briefings ---Loads all units, data and briefings
@@ -645,13 +670,13 @@ function Database:LoadZoneData(missionZoneName)
for _, groupName in pairs(groups) do for _, groupName in pairs(groups) do
if MizGroupsManager.IsGroupStatic(groupName) == true then if MizGroupsManager.IsGroupStatic(groupName) == true then
local object = StaticObject.getByName(groupName) local object = StaticObject.getByName(groupName)
if object and object:getCoalition() == coalition.side.RED then if object and object:getCoalition() == coalition.side.RED then
table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName) table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName)
elseif object then elseif object then
table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName) table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName)
end end
else else
local group = Group.getByName(groupName) local group = Group.getByName(groupName)
if group and group:getCoalition() == coalition.side.RED then if group and group:getCoalition() == coalition.side.RED then
@@ -695,9 +720,9 @@ function Database:LoadZoneData(missionZoneName)
---Adds Briefings ---Adds Briefings
if env.mission.drawings and env.mission.drawings.layers then if env.mission.drawings and env.mission.drawings.layers then
for i, layer in pairs(env.mission.drawings.layers) do for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then if string.lower(layer.name) == "author" then
for key, layer_object in pairs(layer.objects) do for _, layer_object in pairs(layer.objects) do
local vec2 = { x = layer_object.mapX, y = layer_object.mapY } local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if triggerZone and Util.is2dPointInZone(vec2, triggerZone) then if triggerZone and Util.is2dPointInZone(vec2, triggerZone) then
@@ -748,6 +773,28 @@ function Database:loadFarpData()
local number = tonumber(kvPair.value) local number = tonumber(kvPair.value)
if number and number > 0 then if number and number > 0 then
farpzoneData.buildingKilos = number farpzoneData.buildingKilos = number
-- check briefings
if env.mission.drawings and env.mission.drawings.layers then
for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for _, layer_object in pairs(layer.objects) do
local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if triggerZone and Util.is2dPointInZone(vec2, triggerZone) then
if layer_object.name and Util.startswith(layer_object.name, "supplybriefing_", true) then
local description = layer_object.text
if description and description ~= "" then
farpzoneData.briefing = description
end
end
end
end
end
end
end
else
MissionEditorWarnings.Add("Buildable number for " .. farpZone .. " is invalid.")
end end
end end
end end
+40 -46
View File
@@ -29,49 +29,44 @@ do
end end
---@class OnStageChangedListener ---@class OnStageChangedListener
---@field OnStageNumberChanged fun(self:OnStageChangedListener, number:integer) ---@field OnStageNumberChanged fun(self:OnStageChangedListener, number:integer, laneIdentifier:string?)
do -- STAGE NUMBER CHANGED do -- STAGE NUMBER CHANGED
local OnStageNumberChangedListeners = {} local OnStageNumberChangedListeners = {}
local OnStageNumberChangedHandlers = {} local OnStageNumberChangedHandlers = {}
---Add a stage zone number changed listener ---Add a stage zone number changed listener
---@param listener OnStageChangedListener object with function OnStageNumberChanged(self, number) ---@param listener OnStageChangedListener object with function OnStageNumberChanged(self, number, stageLaneIdentifier)
SpearheadEvents.AddStageNumberChangedListener = function(listener) SpearheadEvents.AddStageNumberChangedListener = function(listener)
if type(listener) ~= "table" or type(listener.OnStageNumberChanged) ~= "function" then
warn("Event handler not of type table/object with function OnStageNumberChanged(self, number, stageLaneIdentifier)")
return
end
table.insert(OnStageNumberChangedListeners, listener) table.insert(OnStageNumberChangedListeners, listener)
end end
---Add a stage zone number changed listener
---@param handler function function(number)
SpearheadEvents.AddStageNumberChangedHandler = function(handler)
if type(handler) ~= "function" then
warn("Event handler not of type function, did you mean to use listener?")
return
end
table.insert(OnStageNumberChangedHandlers, handler)
end
---@class OnStageNumberChangeCompleteListener ---@class OnStageNumberChangeCompleteListener
---@field OnStageNumberChangeComplete fun(self:OnStageNumberChangeCompleteListener, number:integer) ---@field OnStageNumberChangeComplete fun(self:OnStageNumberChangeCompleteListener, number:integer, laneIdentifier:string?)
local OnStageNumberChangeCompleteListeners = {} local OnStageNumberChangeCompleteListeners = {}
---@param listener OnStageNumberChangeCompleteListener ---@param listener OnStageNumberChangeCompleteListener
SpearheadEvents.AddStageNumberChangeCompleteListener = function(listener) SpearheadEvents.AddStageNumberChangeCompleteListener = function(listener)
if type(listener) ~= "table" or type(listener.OnStageNumberChangeComplete) ~= "function" then if type(listener) ~= "table" or type(listener.OnStageNumberChangeComplete) ~= "function" then
warn("Event handler not of type table/object with function OnStageNumberChangeComplete(self, number)") warn("Event handler not of type table/object with function OnStageNumberChangeComplete(self, number, laneIdentifier)")
return return
end end
table.insert(OnStageNumberChangeCompleteListeners, listener) table.insert(OnStageNumberChangeCompleteListeners, listener)
end end
---@param newStageNumber number ---@param newStageNumber number
SpearheadEvents.PublishStageNumberChanged = function(newStageNumber) ---@param laneIdentifier string?
SpearheadEvents.PublishStageNumberChanged = function(newStageNumber, laneIdentifier)
pcall(function () pcall(function ()
Persistence.SetActiveStage(newStageNumber) Persistence.SetActiveStage(laneIdentifier, newStageNumber)
end) end)
for _, callable in pairs(OnStageNumberChangedListeners) do for _, callable in pairs(OnStageNumberChangedListeners) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnStageNumberChanged(newStageNumber) callable:OnStageNumberChanged(newStageNumber, laneIdentifier)
end) end)
if err then if err then
logError(err) logError(err)
@@ -79,15 +74,15 @@ do
end end
for _, callable in pairs(OnStageNumberChangedHandlers) do for _, callable in pairs(OnStageNumberChangedHandlers) do
local succ, err = pcall(callable, newStageNumber) local _, err = pcall(callable, newStageNumber, laneIdentifier)
if err then if err then
logError(err) logError(err)
end end
end end
for _, callable in pairs(OnStageNumberChangeCompleteListeners) do for _, callable in pairs(OnStageNumberChangeCompleteListeners) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnStageNumberChangeComplete(newStageNumber) callable:OnStageNumberChangeComplete(newStageNumber, laneIdentifier)
end) end)
if err then if err then
logError(err) logError(err)
@@ -117,7 +112,7 @@ do
local triggerWeaponFired = function(unit, weapon, target) local triggerWeaponFired = function(unit, weapon, target)
for _, callable in pairs(onWeaponFiredListeners) do for _, callable in pairs(onWeaponFiredListeners) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnWeaponFired(unit, weapon, target) callable:OnWeaponFired(unit, weapon, target)
end) end)
@@ -126,7 +121,7 @@ do
end end
end end
end end
local onLandEventListeners = {} local onLandEventListeners = {}
---Add an event listener to a specific unit ---Add an event listener to a specific unit
@@ -191,7 +186,7 @@ do
if groupName ~= nil then if groupName ~= nil then
if OnGroupRTBListeners[groupName] then if OnGroupRTBListeners[groupName] then
for _, callable in pairs(OnGroupRTBListeners[groupName]) do for _, callable in pairs(OnGroupRTBListeners[groupName]) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnGroupRTB(groupName) callable:OnGroupRTB(groupName)
end) end)
if err then if err then
@@ -226,7 +221,7 @@ do
if groupName ~= nil then if groupName ~= nil then
if OnGroupRTBInTenListeners[groupName] then if OnGroupRTBInTenListeners[groupName] then
for _, callable in pairs(OnGroupRTBInTenListeners[groupName]) do for _, callable in pairs(OnGroupRTBInTenListeners[groupName]) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnGroupRTBInTen(groupName) callable:OnGroupRTBInTen(groupName)
end) end)
if err then if err then
@@ -262,7 +257,7 @@ do
if groupName ~= nil then if groupName ~= nil then
if OnGroupOnStationListeners[groupName] then if OnGroupOnStationListeners[groupName] then
for _, callable in pairs(OnGroupOnStationListeners[groupName]) do for _, callable in pairs(OnGroupOnStationListeners[groupName]) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnGroupOnStation(groupName) callable:OnGroupOnStation(groupName)
end) end)
if err then if err then
@@ -291,7 +286,7 @@ do
if unit ~= nil then if unit ~= nil then
if playerEnterUnitListeners then if playerEnterUnitListeners then
for _, callable in pairs(playerEnterUnitListeners) do for _, callable in pairs(playerEnterUnitListeners) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnPlayerEntersUnit(unit) callable:OnPlayerEntersUnit(unit)
end) end)
if err then if err then
@@ -304,7 +299,7 @@ do
end end
do -- Ejection events do -- Ejection events
local unitEjectListeners = {} local unitEjectListeners = {}
SpearheadEvents.AddOnUnitEjectedListener = function(listener) SpearheadEvents.AddOnUnitEjectedListener = function(listener)
if type(listener) ~= "table" then if type(listener) ~= "table" then
@@ -326,7 +321,7 @@ do
local name = unit:getName() local name = unit:getName()
if onLandEventListeners[name] then if onLandEventListeners[name] then
for _, callable in pairs(onLandEventListeners[name]) do for _, callable in pairs(onLandEventListeners[name]) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnUnitLanded(unit, airbase) callable:OnUnitLanded(unit, airbase)
end) end)
if err then if err then
@@ -346,10 +341,10 @@ do
if object and object.getName then if object and object.getName then
logDebug("Receiving death event from: " .. object:getName()) logDebug("Receiving death event from: " .. object:getName())
end end
if object and object.getName and OnUnitLostListeners[object:getName()] then if object and object.getName and OnUnitLostListeners[object:getName()] then
for _, callable in pairs(OnUnitLostListeners[object:getName()]) do for _, callable in pairs(OnUnitLostListeners[object:getName()]) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnUnitLost(object) callable:OnUnitLost(object)
end) end)
@@ -361,11 +356,11 @@ do
end end
if event.id == world.event.S_EVENT_EJECTION then if event.id == world.event.S_EVENT_EJECTION then
end end
if event.id == world.event.S_EVENT_SHOT then if event.id == world.event.S_EVENT_SHOT then
local shooter = event.initiator local shooter = event.initiator
local weapon = event.weapon local weapon = event.weapon
local target = event.target local target = event.target
@@ -379,26 +374,26 @@ do
local AI_GROUPS = {} local AI_GROUPS = {}
local function CheckAndTriggerSpawnAsync(unit, time) local function CheckAndTriggerSpawnAsync(unit, _)
local function isPlayer(unit) local function isPlayer(checkUnit)
if unit == nil then return false, "unit is nil" end if checkUnit == nil then return false, "unit is nil" end
if unit.getGroup == nil then return false, 'no get group function in unit object, most likely static' end if checkUnit.getGroup == nil then return false, 'no get group function in unit object, most likely static' end
if Object.getCategory(unit) ~= Object.Category.UNIT then if Object.getCategory(checkUnit) ~= Object.Category.UNIT then
return false, "object is not a unit" return false, "object is not a unit"
end end
if unit:isExist() ~= true then return false, "unit does not exist" end if checkUnit:isExist() ~= true then return false, "unit does not exist" end
local group = unit:getGroup() local group = checkUnit:getGroup()
if group ~= nil then if group ~= nil then
if AI_GROUPS[group:getName()] == true then if AI_GROUPS[group:getName()] == true then
return false return false
end end
local players = DcsUtil.getAllPlayerUnits() local players = DcsUtil.getAllPlayerUnits()
local unitName = unit:getName() local unitName = checkUnit:getName()
for i, unit in pairs(players) do for _, playerUnit in pairs(players) do
if unit:getName() == unitName then if playerUnit:getName() == unitName then
return true return true
end end
end end
@@ -408,7 +403,6 @@ do
end end
if isPlayer(unit) == true then if isPlayer(unit) == true then
local groupId = unit:getGroup():getID()
SpearheadEvents.TriggerPlayerEntersUnit(unit) SpearheadEvents.TriggerPlayerEntersUnit(unit)
end end
+495 -202
View File
@@ -2,23 +2,14 @@ local Events = require("classes.spearhead_events")
local Util = require("classes.util.Util") local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil") local DcsUtil = require("classes.util.DcsUtil")
local Logger = require("classes.util.Logger") local Logger = require("classes.util.Logger")
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings") local PersistenceConfig = require("classes.configuration.PersistenceConfig")
local ExtraStage = require("classes.stageClasses.Stages.ExtraStage") local ExtraStage = require("classes.stageClasses.Stages.ExtraStage")
local PrimaryStage = require("classes.stageClasses.Stages.PrimaryStage") local PrimaryStage = require("classes.stageClasses.Stages.PrimaryStage")
local WaitingStage = require("classes.stageClasses.Stages.WaitingStage") local WaitingStage = require("classes.stageClasses.Stages.WaitingStage")
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper") local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
local StageRepository = require("classes.stageClasses.StageRepository")
local StagesByName = {} local StageLane = require("classes.stageClasses.StageLane")
local Persistence = require("classes.persistence.Persistence")
---@type table<string, Array<Stage>>
local StagesByIndex = {}
---@type table<string, Array<Stage>>
local SideStageByIndex = {}
---@type table<string, Array<WaitingStage>>
local WaitingStagesByIndex = {}
local currentStage = -99 local currentStage = -99
@@ -26,29 +17,41 @@ local currentStage = -99
---@field private database Database ---@field private database Database
---@field private logger Logger ---@field private logger Logger
---@field private stageConfig StageConfig ---@field private stageConfig StageConfig
---@field private _stageRepository StageRepository
---@field private _missionCommandsHelper MissionCommandsHelper ---@field private _missionCommandsHelper MissionCommandsHelper
local GlobalStageManager = {} local GlobalStageManager = {}
GlobalStageManager.__index = GlobalStageManager GlobalStageManager.__index = GlobalStageManager
GlobalStageManager.getCurrentStage = function() return currentStage end GlobalStageManager.getCurrentStage = function() return currentStage end
---@type GlobalStageManager
local singletonInstance = nil
---comment ---comment
---@param database Database ---@param database Database
---@param stageConfig StageConfig ---@param stageConfig StageConfig
---@param logLevel LogLevel ---@param logLevel LogLevel
---@param spawnManager SpawnManager ---@param spawnManager SpawnManager
---@return nil ---@return GlobalStageManager
function GlobalStageManager.NewAndStart(database, stageConfig, logLevel, spawnManager) function GlobalStageManager.new(database, stageConfig, logLevel, spawnManager)
if singletonInstance ~= nil then
return singletonInstance
end
local logger = Logger.new("StageManager", logLevel) local logger = Logger.new("StageManager", logLevel)
logger:info("Using Stage Log Level: " .. logLevel) logger:info("Using Stage Log Level: " .. logLevel)
local self = setmetatable({}, GlobalStageManager) local self = setmetatable({}, GlobalStageManager)
singletonInstance = self
self.database = database self.database = database
self.stageConfig = stageConfig self.stageConfig = stageConfig
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate() self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
self._stageRepository = StageRepository.getInstance()
self.logger = logger self.logger = logger
if stageConfig.isAutoStages ~= true then if stageConfig.isAutoStages ~= true then
logger:warn("Spearhead will not automatically progress stages due to the given settings. If you manually have implemented this, please ignore this message") logger:warn(
"Spearhead will not automatically progress stages due to the given settings. If you manually have implemented this, please ignore this message")
end end
Events.AddStageNumberChangedListener(self) Events.AddStageNumberChangedListener(self)
@@ -56,171 +59,274 @@ function GlobalStageManager.NewAndStart(database, stageConfig, logLevel, spawnMa
for _, stageName in pairs(database:getStagezoneNames()) do for _, stageName in pairs(database:getStagezoneNames()) do
logger:debug("Found stage zone with name: " .. stageName) logger:debug("Found stage zone with name: " .. stageName)
if Util.startswith(stageName, "missionstage", true) then local parseResult = self:ParseStageName(stageName)
local valid = true if parseResult.isValid == false then
local split = Util.split_string(stageName, "_") logger:warn("Stage zone with name " .. stageName .. " is not valid: " .. parseResult.invalidReason)
if Util.tableLength(split) < 2 then else
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a order number or valid format") local initData = {
valid = false stageZoneName = parseResult.stageZoneName,
end stageNumber = parseResult.orderNumber,
stageDisplayName = parseResult.stageDisplayName,
stageLaneIdentifier = parseResult.stageLaneIdentifier
}
if Util.tableLength(split) < 3 then if parseResult.stageType == "PrimaryStage" then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a stage name") local stage = PrimaryStage.New(database, stageConfig, logger, initData, spawnManager)
end
local orderNumber = nil stage:AddStageCompleteListener(self)
local isSideStage = false self._stageRepository:AddStage(stage)
if valid == true then elseif parseResult.stageType == "ExtraStage" then
local orderNumberString = string.lower(split[2]) local stage = ExtraStage.New(database, stageConfig, logger, initData, spawnManager)
if Util.startswith(orderNumberString, "x") == true then stage:AddStageCompleteListener(self)
isSideStage = true self._stageRepository:AddStage(stage)
elseif parseResult.stageType == "WaitingStage" then
orderNumberString = string.gsub(orderNumberString, "x", "") local waitingStage = WaitingStage.New(database, stageConfig, logger, initData,
orderNumber = tonumber(orderNumberString) parseResult.waitingStageSeconds, spawnManager)
else waitingStage:AddStageCompleteListener(self)
orderNumber = tonumber(split[2]) self._stageRepository:AddStage(waitingStage)
end
if orderNumber == nil then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a valid order number : " .. split[2])
valid = false
end
end
local stageDisplayName = split[3]
local stagelogger = Logger.new(stageName, logLevel)
if valid == true and orderNumber then
---@type StageInitData
local initData = {
stageDisplayName = stageDisplayName,
stageNumber = orderNumber,
stageZoneName = stageName,
}
if isSideStage == true then
local stage = ExtraStage.New(database, stageConfig, stagelogger, initData, spawnManager)
stage:AddStageCompleteListener(self)
if SideStageByIndex[tostring(orderNumber)] == nil then SideStageByIndex[tostring(orderNumber)] = {} end
table.insert(SideStageByIndex[tostring(orderNumber)], stage)
else
local stage = PrimaryStage.New(database, stageConfig, stagelogger, initData, spawnManager)
stage:AddStageCompleteListener(self)
if StagesByIndex[tostring(orderNumber)] == nil then StagesByIndex[tostring(orderNumber)] = {} end
table.insert(StagesByIndex[tostring(orderNumber)], stage)
end
end
end
if Util.startswith(stageName, "waitingstage", true) then
local valid = true
local split = Util.split_string(stageName, "_")
if Util.tableLength(split) < 3 then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a order number or valid format")
valid = false
end
if valid == true then
local stageIndexString = split[2]
local stageIndex = tonumber(stageIndexString)
if not stageIndex then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a valid order number")
valid = false
end
local waitingSecondsString = split[3]
local waitingSeconds = tonumber(waitingSecondsString)
if not waitingSeconds then
MissionEditorWarnings.Add("Waiting Stage zone with name " .. stageName .. " does not have a valid amount of seconds parameter")
valid = false
end
if valid == true then
local stagelogger = Logger.new(stageName, logLevel)
---@type WaitingStageInitData
local initData = {
stageDisplayName = "Waiting Stage " .. stageIndex,
stageNumber = stageIndex or -99,
stageZoneName = stageName,
waitingSeconds = waitingSeconds --[[@as integer]]
}
local waitingStage = WaitingStage.New(database, stageConfig, stagelogger, initData, spawnManager)
if WaitingStagesByIndex[tostring(stageIndex)] == nil then
WaitingStagesByIndex[tostring(stageIndex)] = {}
end
table.insert(WaitingStagesByIndex[tostring(stageIndex)], waitingStage)
waitingStage:AddStageCompleteListener(self)
end
end end
end end
end end
singletonInstance = self
return self return self
end end
function GlobalStageManager:Start()
self.logger:info("Starting GlobalStageManager")
local startingStage = self.stageConfig.startingStage or 1
local persistenceConfig = PersistenceConfig.new()
if persistenceConfig:isEnabled() == true then
self.logger:info("Persistence is enabled, loading stage state from persistence")
local stageLanes = self._stageRepository:getAllStageLanes()
for _, stageLane in pairs(stageLanes) do
local stageLaneIdentifier = stageLane:GetStageLaneIdentifier()
local persistedStage = Persistence.GetActiveStage(stageLaneIdentifier)
if persistedStage then
self.logger:info("Loaded persisted stage " ..
persistedStage .. " for lane " .. (stageLaneIdentifier or "default"))
stageLane:SetActiveStageIndex(persistedStage)
Events.PublishStageNumberChanged(persistedStage, stageLaneIdentifier)
else
stageLane:SetActiveStageIndex(startingStage)
Events.PublishStageNumberChanged(startingStage, stageLaneIdentifier)
end
end
else
self.logger:info("Persistence is disabled, starting at stage " .. startingStage)
local stageLanes = self._stageRepository:getAllStageLanes()
for _, stageLane in pairs(stageLanes) do
stageLane:SetActiveStageIndex(startingStage)
Events.PublishStageNumberChanged(startingStage, stageLane:GetStageLaneIdentifier())
end
end
end
---@class StageNameParseResult
---@field isValid boolean
---@field orderNumber integer
---@field stageLaneIdentifier string?
---@field stageDisplayName string
---@field stageZoneName string
---@field stageType StageType
---@field invalidReason string?
---@field waitingStageSeconds integer?
---@param self GlobalStageManager
---@param stageName string
---@return StageNameParseResult
function GlobalStageManager:ParseStageName(stageName)
local split = Util.split_string(stageName, "_")
if Util.tableLength(split) < 3 then
return { isValid = false, invalidReason = "Stage zone with name " ..
stageName .. " does not have a order number or valid format" }
end
local typePart = string.lower(split[1])
if typePart == "missionstage" then
local orderNumberString = string.lower(split[2])
---@type StageType
local stageType = "PrimaryStage"
if Util.endsWith(orderNumberString, "x", true) == true then
stageType = "ExtraStage"
orderNumberString = orderNumberString:sub(1, -1)
end
local stageLaneIdentifier = nil
local first = orderNumberString:sub(1, 1)
if tonumber(first) == nil then -- first character is the lane identifier only if it's not a number
stageLaneIdentifier = string.lower(first)
orderNumberString = orderNumberString:sub(2)
end
local orderNumber = tonumber(orderNumberString)
if orderNumber == nil then
return { isValid = false, invalidReason = "Stage zone with name " ..
stageName .. " does not have a valid order number : " .. orderNumberString }
end
local stageDisplayName = split[3]
---@type StageNameParseResult
local result = {
isValid = true,
orderNumber = orderNumber,
stageLaneIdentifier = stageLaneIdentifier,
stageDisplayName = stageDisplayName,
stageZoneName = stageName,
stageType = stageType
}
return result
elseif typePart == "waitingstage" then
local stageType = "WaitingStage"
local orderNumberString = split[2]
local orderNumber = tonumber(orderNumberString)
if orderNumber == nil then
return { isValid = false, invalidReason = "Waiting Stage zone with name " ..
stageName .. " does not have a valid order number : " .. orderNumberString }
end
local waitingSecondsString = split[3]
local waitingSeconds = tonumber(waitingSecondsString)
if waitingSeconds == nil then
return { isValid = false, invalidReason = "Waiting Stage zone with name " ..
stageName .. " does not have a valid amount of seconds parameter : " .. waitingSecondsString }
end
local stageDisplayName = "Waiting Stage " .. orderNumber
---@type StageNameParseResult
local result = {
isValid = true,
orderNumber = orderNumber,
stageLaneIdentifier = nil,
stageDisplayName = stageDisplayName,
stageZoneName = stageName,
stageType = stageType,
waitingStageSeconds = waitingSeconds
}
return result
end
return { isValid = false, invalidReason = "Stage zone with name " ..
stageName .. " has an unrecognized type: " .. typePart }
end
---@param stage Stage
function GlobalStageManager:OnStageComplete(stage) function GlobalStageManager:OnStageComplete(stage)
self.logger:debug("Receiving stage complete event from: " .. stage.zoneName) self.logger:debug("Receiving stage complete event from: " .. stage.zoneName)
local anyIncomplete = false local stageIndex = stage:GetStageIndex()
self.logger:debug("Checking stages for index: " .. tostring(currentStage)) local laneIdentifier = stage:GetStageLaneIdentifier()
for index, stage in pairs(StagesByIndex[tostring(currentStage)]) do
if stage:IsComplete() == false then local stageLane = self._stageRepository:getStageLane(laneIdentifier)
anyIncomplete = true if not stageLane or not stageLane:IsCurrentStageIndexComplete() then
self.logger:debug("Need to wait for Stage " .. stage.zoneName .. " to be completed") self.logger:debug("Stage lane " ..
else (laneIdentifier or "default") .. " is not complete for stage index " .. stageIndex)
self.logger:debug("Stage verified to be completed: " .. stage.zoneName) return
end
end end
if anyIncomplete == false and self.stageConfig.isAutoStages == true then
-- CHECK WAITING STAGES local nextStageIndex = stageIndex + 1
local nextStage = currentStage + 1
Events.PublishStageNumberChanged(nextStageIndex, laneIdentifier)
if WaitingStagesByIndex[tostring(nextStage)] then stageLane:SetActiveStageIndex(nextStageIndex)
for _, waitingStage in pairs(WaitingStagesByIndex[tostring(nextStage)]) do
if waitingStage:IsActive() == false then if stageLane:IsDefaultStageLane() then
waitingStage:ActivateStage() -- COMPLETION IN THE DEFAULT LANE
-- 1. Check if any lane is at status "BetweenChapters" and the next chapter start is "next stage index"
local allLanes = self._stageRepository:getAllStageLanes()
for _, lane in ipairs(allLanes) do
if lane:GetStageLaneIdentifier() ~= StageLane.DefaultLaneKey and lane:GetStageLaneState() == "BetweenChapters" then
local nextChapterStart = lane:GetNextChapterStart()
if nextChapterStart == nextStageIndex then
self.logger:debug("Lane " ..
(lane:GetStageLaneIdentifier() or "default") ..
" is at chapter start for next stage index " .. nextStageIndex)
Events.PublishStageNumberChanged(nextStageIndex, lane:GetStageLaneIdentifier())
lane:SetActiveStageIndex(nextStageIndex)
end end
end end
end end
else
local anyWaiting = false -- COMPLETION IN A SIDE LANE
if WaitingStagesByIndex[tostring(nextStage)] then
for _, waitingStage in pairs(WaitingStagesByIndex[tostring(nextStage)]) do -- 1. Check if the default lane is at status "BetweenChapters"
if waitingStage:IsComplete() == false then local defaultStageLane = self._stageRepository:getStageLane(StageLane.DefaultLaneKey)
anyWaiting = true if defaultStageLane and defaultStageLane:GetStageLaneState() == "BetweenChapters" then
local nextChapterStart = defaultStageLane:GetNextChapterStart()
if nextChapterStart == nextStageIndex then
-- 2. Check if all side lanes are at or beyond the next chapter starts
local allLanes = self._stageRepository:getAllStageLanes()
local allSideLanesReady = true
for _, lane in ipairs(allLanes) do
if lane:GetStageLaneIdentifier() ~= StageLane.DefaultLaneKey then
-- Check if the lane is at or beyond the next chapter starts when "InChapter" eq "Active"
if lane:GetStageLaneState() == "InChapter" and lane:GetActiveStageIndex() < nextStageIndex then
allSideLanesReady = false
self.logger:debug("Lane " ..
(lane:GetStageLaneIdentifier() or "default") ..
" is not ready for next stage index " .. nextStageIndex)
break
-- When in between chapters, check if the next chapter start is less than the next stage index, meaning there is still stages to be completed before the main lane can be activated again
elseif lane:GetStageLaneState() == "BetweenChapters" then
local nextChapter = lane:GetNextChapterStart()
if nextChapter and nextChapter < nextStageIndex then
allSideLanesReady = false
self.logger:debug("Lane " ..
(lane:GetStageLaneIdentifier() or "default") ..
" is not ready for next stage index " .. nextStageIndex)
break
end
end
end
end
if allSideLanesReady then
self.logger:debug("All side lanes are ready for next stage index " ..
nextStageIndex .. ", activating default lane")
Events.PublishStageNumberChanged(nextStageIndex, nil)
defaultStageLane:SetActiveStageIndex(nextStageIndex)
else
self.logger:debug("Not all side lanes are ready for next stage index " ..
nextStageIndex .. ", default lane will not be activated")
end end
end end
end end
if anyWaiting == false then
local newStageNumber = currentStage + 1
self.logger:debug("Setting next stage to: " .. tostring(newStageNumber))
Events.PublishStageNumberChanged(newStageNumber)
end
end end
end end
---@param stageNumber number
---@param stageLaneIdentifier string?
---@return boolean?
function GlobalStageManager:IsStageComplete(stageNumber, stageLaneIdentifier)
local stageLane = self._stageRepository:getStageLane(stageLaneIdentifier)
if not stageLane then
self.logger:warn("Stage lane " .. (stageLaneIdentifier or "default") .. " does not exist")
return nil
end
return stageLane:IsStageIndexComplete(stageNumber)
end
---@public ---@public
function GlobalStageManager:OnStageNumberChanged(stageNumber) function GlobalStageManager:OnStageNumberChanged(stageNumber, stageLaneIdentifier)
-- only react on "main" lane changes, ignore other lanes for now
if stageLaneIdentifier ~= nil then return end
self.logger:debug("Stage number changed to: " .. tostring(stageNumber)) self.logger:debug("Stage number changed to: " .. tostring(stageNumber))
currentStage = stageNumber
self:UpdateDrawings(stageNumber) self:UpdateDrawings(stageNumber)
end end
function GlobalStageManager:OnStageNumberChangeComplete(stageNumber) function GlobalStageManager:OnStageNumberChangeComplete(stageNumber, stageLaneIdentifier)
-- only react on "main" lane changes, ignore other lanes for now
if stageLaneIdentifier ~= nil then return end
self.logger:debug("Stage number change complete to: " .. tostring(stageNumber)) self.logger:debug("Stage number change complete to: " .. tostring(stageNumber))
local groups = {} local groups = {}
for _, player in pairs(DcsUtil.getAllPlayerUnits()) do for _, player in pairs(DcsUtil.getAllPlayerUnits()) do
@@ -236,83 +342,270 @@ function GlobalStageManager:OnStageNumberChangeComplete(stageNumber)
end end
---@private ---@private
function GlobalStageManager:UpdateDrawings(stageNumber) ---@param stageNumber number
---@param stageLaneIdentifier string?
function GlobalStageManager:UpdateDrawings(stageNumber, stageLaneIdentifier)
self.logger:debug("Updating custom drawings for stage number: " .. tostring(stageNumber)) self.logger:debug("Updating custom drawings for stage number: " .. tostring(stageNumber))
local drawings = self.database:getCustomDrawings() local drawings = self.database:getStageDrawings()
for _, drawing in pairs(drawings) do for _, drawing in pairs(drawings) do
local startStage, stopStage = drawing:GetStartAndStop() local startStage = drawing:GetStartingStage()
if stageNumber >= startStage and stageNumber < stopStage then local stopStage = drawing:GetRemoveAtStage()
self.logger:debug("Drawing " .. drawing:GetName() .. " is active for stage number: " .. tostring(stageNumber)) local laneIdentifier = drawing:GetStageLaneIdentifier()
drawing:Draw()
else if laneIdentifier == stageLaneIdentifier then
self.logger:debug("Drawing " .. drawing:GetName() .. " is not active for stage number: " .. tostring(stageNumber)) if stageNumber >= startStage and stageNumber < stopStage then
drawing:Remove() self.logger:debug("Drawing " ..
drawing:GetName() .. " is active for stage number: " .. tostring(stageNumber))
drawing:Draw()
else
self.logger:debug("Drawing " ..
drawing:GetName() .. " is not active for stage number: " .. tostring(stageNumber))
drawing:Remove()
end
end end
end end
end end
function GlobalStageManager:PrintMermaidStage()
local lanes = self._stageRepository:getAllStageLanes()
GlobalStageManager.printFullOverview = function () local nodes = {}
local edges = {}
local logger = Logger.new("StageOverview", "INFO") local stageIndicesByLane = {} -- Track all stage indices per lane
logger:info("Stage overview:") local mainLaneId = "default"
local max = 0 -- Color palette for lanes (26 vibrant colors optimized for dark mode)
local lines = {} local laneColors = {
for stageIndex, stages in pairs(StagesByIndex) do "#FF6B6B", -- 1: Red
"#4ECDC4", -- 2: Teal
local totalStrike = 0 "#45B7D1", -- 3: Blue
local totalbai = 0 "#FFA502", -- 4: Orange
local totaldead = 0 "#95E1D3", -- 5: Mint
local totalMissions = 0 "#F38181", -- 6: Coral
local totalCas = 0 "#AA96DA", -- 7: Purple
"#FCBAD3", -- 8: Pink
"#A8E6CF", -- 9: Light green
"#FFD3B6", -- 10: Peach
"#FFAAA5", -- 11: Light red
"#FF8B94", -- 12: Rose
"#FFEAA7", -- 13: Butter
"#DFE6E9", -- 14: Gray
"#00B894", -- 15: Emerald
"#0984E3", -- 16: Cobalt
"#6C5CE7", -- 17: Indigo
"#A29BFE", -- 18: Lavender
"#FD79A8", -- 19: Magenta
"#FDCB6E", -- 20: Gold
"#6C757D", -- 21: Slate
"#20C997", -- 22: Seafoam
"#E74C3C", -- 23: Scarlet
"#3498DB", -- 24: Dodger blue
"#9B59B6", -- 25: Amethyst
"#1ABC9C", -- 26: Turquoise
}
local laneColorMap = {} -- Map lane ID to color
local colorIndex = 1
for _, stage in pairs(stages) do -- Build nodes and collect all stage indices per lane
for _, lane in ipairs(lanes) do
local strike, dead, bai, cas = stage:GetStageStats() local laneId = lane:GetStageLaneIdentifier() or "default"
local stageIndices = lane:GetAllStageIndices()
stageIndicesByLane[laneId] = stageIndices
totalStrike = totalStrike + strike -- Assign color to this lane
totalbai = totalbai + bai laneColorMap[laneId] = laneColors[colorIndex]
totaldead = totaldead + dead colorIndex = colorIndex + 1
totalCas = totalCas + cas if colorIndex > #laneColors then
totalMissions = totalMissions + strike + dead + bai + cas colorIndex = 1
end end
local index = tonumber(stageIndex) -- Create nodes for each stage index in this lane
if index then for _, stageIndex in ipairs(stageIndices) do
if index > max then local stages = lane:GetStagesAtIndex(stageIndex)
max = index
if stages then
for _, stage in ipairs(stages) do
local nodeId = laneId .. "_" .. stageIndex
local stageType = stage:GetStageType()
local stageName = stage.stageName or stage.zoneName
-- Generate bracket label: [1], [2] for default, [w1], [e2] for other lanes
local bracketLabel
if laneId == "default" then
bracketLabel = "[" .. stageIndex .. "]"
else
bracketLabel = "[" .. laneId .. stageIndex .. "]"
end
-- Combined label: [bracket] Name
local label = bracketLabel .. " " .. stageName
-- Different node shapes for different stage types
local nodeShape = "["
local nodeEnd = "]"
if stageType == "ExtraStage" then
nodeShape = "(["
nodeEnd = "])"
elseif stageType == "WaitingStage" then
nodeShape = "[["
nodeEnd = "]]"
end
nodes[nodeId] = string.format(' %s%s"%s"%s',
nodeId, nodeShape, label, nodeEnd)
end
end end
lines[index] ="Stage# " .. tostring(stageIndex).. " | " .. totalStrike .. " strikes | " .. totaldead .. " dead | " .. totalbai .. " BAI | " .. totalCas .. " CAS | Total:" .. totalMissions
else
logger:warn("Stage index is not a number: " .. stageIndex)
end end
end end
for i = 1, max do -- Add edges for sequential stages within the same lane
if lines[i] then for laneId, stageIndices in pairs(stageIndicesByLane) do
logger:info(lines[i]) for i = 1, #stageIndices - 1 do
local fromIdx = stageIndices[i]
local toIdx = stageIndices[i + 1]
local fromNodeId = laneId .. "_" .. fromIdx
local toNodeId = laneId .. "_" .. toIdx
-- Check if this is a chapter boundary (gap)
local isChapter = false
for _, lane in ipairs(lanes) do
local normalizedLaneId = lane:GetStageLaneIdentifier() or "default"
if normalizedLaneId == laneId then
isChapter = lane:IsChapterStart(toIdx)
break
end
end
local label = isChapter and "|chapter|" or ""
table.insert(edges, string.format(' %s -->%s %s', fromNodeId, label, toNodeId))
end end
end end
-- Get main lane reference
local mainStageLane = nil
for _, lane in ipairs(lanes) do
if lane:IsDefaultStageLane() then
mainStageLane = lane
break
end
end
-- Add dependencies from DEFAULT lane to SIDE lanes at chapter starts
-- Side lanes activate when main completes the stage BEFORE the chapter start
if mainStageLane then
for _, lane in ipairs(lanes) do
if lane:IsDefaultStageLane() == false then
local sideId = lane:GetStageLaneIdentifier()
local sideIndices = stageIndicesByLane[sideId]
-- For each stage in the side lane
for _, stageIdx in ipairs(sideIndices) do
-- Check if this is a chapter start in the side lane
if lane:IsChapterStart(stageIdx) then
-- Main lane must complete (stageIdx - 1) to activate side lane at stageIdx
local fromNodeId = mainLaneId .. "_" .. (stageIdx - 1)
local toNodeId = sideId .. "_" .. stageIdx
-- Side lane activates when main lane completes the prior stage
table.insert(edges, string.format(' %s -->|unlock| %s', fromNodeId, toNodeId))
end
end
end
end
end
-- Add dependencies from SIDE lanes gating the MAIN lane at chapter boundaries
-- Main cannot advance to the next chapter until ALL side lanes are at or above that chapter index
if mainStageLane then
local mainIndices = stageIndicesByLane[mainLaneId]
-- For each chapter start in main lane (skip first one)
for i = 2, #mainIndices do
local nextIdx = mainIndices[i]
-- Check if nextIdx is a chapter start (there's a gap before it)
if mainStageLane:IsChapterStart(nextIdx) then
-- Before main can progress to nextIdx, all side lanes must be at >= nextIdx
for _, lane in ipairs(lanes) do
if lane:IsDefaultStageLane() == false then
local sideId = lane:GetStageLaneIdentifier()
local sideIndices = stageIndicesByLane[sideId]
-- Find the appropriate gate node for this side lane
-- Use first stage >= nextIdx if it exists, otherwise use the highest stage
local gateStageIdx = nil
for _, idx in ipairs(sideIndices) do
if idx >= nextIdx then
gateStageIdx = idx
break
end
end
-- If no stage >= nextIdx, use the highest stage in this lane
if gateStageIdx == nil and #sideIndices > 0 then
gateStageIdx = sideIndices[#sideIndices]
end
if gateStageIdx then
local fromNodeId = sideId .. "_" .. gateStageIdx
local toNodeId = mainLaneId .. "_" .. nextIdx
table.insert(edges, string.format(' %s -->|gate| %s', fromNodeId, toNodeId))
end
end
end
end
end
end
-- Build complete Mermaid diagram
local diagramLines = { "graph TD" }
-- Add class definitions for each lane with colors and contrasting text
for laneId, color in pairs(laneColorMap) do
table.insert(diagramLines,
string.format(' classDef lane_%s fill:%s,stroke:#333,stroke-width:2px,color:#000', laneId, color))
end
-- Add nodes
for _, nodeStr in pairs(nodes) do
table.insert(diagramLines, nodeStr)
end
-- Add edges
for _, edgeStr in pairs(edges) do
table.insert(diagramLines, edgeStr)
end
-- Apply classes to nodes
for nodeId in pairs(nodes) do
local laneId = nodeId:match("(.+)_[0-9]+$")
if laneId then
table.insert(diagramLines, string.format(' class %s lane_%s', nodeId, laneId))
end
end
-- Print as single multi-line message
local diagram = "========== STAGE FLOW DIAGRAM ==========\n" ..
table.concat(diagramLines, "\n") .. "\n========== END DIAGRAM =========="
self.logger:info(diagram)
end end
---comment ---comment
---@param stageNumber number ---@param stageNumber number
---@param stageLaneIdentifier string? nil for default lan
---@return boolean | nil ---@return boolean | nil
GlobalStageManager.isStageComplete = function (stageNumber) GlobalStageManager.isStageComplete = function(stageNumber, stageLaneIdentifier)
if singletonInstance == nil then
local stageIndex = tostring(stageNumber) Logger.new("StageManager", "INFO"):warn(
"GlobalStageManager.isStageComplete called before GlobalStageManager was initialized. Returning nil")
if StagesByIndex[stageIndex] == nil then return nil end return nil
for _, stage in ipairs(StagesByIndex[stageIndex]) do
if stage:IsComplete() == false then
return false
end
end end
return true if stageLaneIdentifier then
stageLaneIdentifier = string.lower(stageLaneIdentifier)
end
return singletonInstance:IsStageComplete(stageNumber, stageLaneIdentifier)
end end
return GlobalStageManager return GlobalStageManager
@@ -41,7 +41,7 @@ end
function SpearheadGroup:SpawnCorpsesOnly() function SpearheadGroup:SpawnCorpsesOnly()
if self._isSpawned == true then return end if self._isSpawned == true then return end
self._spawnManager:SpawnCorpsesOnly(self._groupName) self._spawnManager:SpawnCorpsesOnly(self._groupName)
self._isSpawned = true self._isSpawned = true
@@ -57,7 +57,7 @@ function SpearheadGroup:Spawn(lateStart)
uncontrolled = lateStart, uncontrolled = lateStart,
} }
local spawnedObject, isStatic = self._spawnManager:SpawnGroup(self._groupName, overrides, self._isPersistent) local _, isStatic = self._spawnManager:SpawnGroup(self._groupName, overrides, self._isPersistent)
self._isStatic = isStatic self._isStatic = isStatic
self._isSpawned = true self._isSpawned = true
end end
@@ -97,7 +97,7 @@ function SpearheadGroup:GetObjects()
local result = {} local result = {}
if self._isStatic == true then if self._isStatic == true then
local staticObject = StaticObject.getByName(self._groupName) local staticObject = StaticObject.getByName(self._groupName)
if staticObject then if staticObject then
table.insert(result, staticObject) table.insert(result, staticObject)
end end
else else
@@ -105,7 +105,7 @@ function SpearheadGroup:GetObjects()
if not group then return {} end if not group then return {} end
for _, unit in pairs(group:getUnits()) do for _, unit in pairs(group:getUnits()) do
table.insert(result, unit) table.insert(result, unit)
end end
end end
return result return result
end end
@@ -123,7 +123,7 @@ function SpearheadGroup:GetAsUnits()
if not group then return {} end if not group then return {} end
for _, unit in pairs(group:getUnits()) do for _, unit in pairs(group:getUnits()) do
table.insert(result, unit) table.insert(result, unit)
end end
return result return result
end end
@@ -133,7 +133,7 @@ function SpearheadGroup:GetAllUnitPositions()
local result = {} local result = {}
if self._isStatic == true then if self._isStatic == true then
local staticObject = StaticObject.getByName(self._groupName) local staticObject = StaticObject.getByName(self._groupName)
if staticObject then if staticObject then
table.insert(result, staticObject:getPoint()) table.insert(result, staticObject:getPoint())
end end
else else
@@ -141,7 +141,7 @@ function SpearheadGroup:GetAllUnitPositions()
if not group then return {} end if not group then return {} end
for _, unit in pairs(group:getUnits()) do for _, unit in pairs(group:getUnits()) do
table.insert(result, unit:getPoint()) table.insert(result, unit:getPoint())
end end
end end
return result return result
end end
@@ -50,16 +50,10 @@ function BlueSam.New(database, logger, zoneName, spawnManager)
---@type table<string, Vec3> ---@type table<string, Vec3>
local redUnitsPos = {} local redUnitsPos = {}
local buildable = false
if self._buildableCrateKilos and self._buildableCrateKilos > 0 then
buildable = true
end
for _, groupName in pairs(blueSamData.groups) do for _, groupName in pairs(blueSamData.groups) do
local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true) local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true)
if spearheadGroup then if spearheadGroup then
if spearheadGroup:GetCoalition() == 2 or spearheadGroup:GetCoalition() == 0 then if spearheadGroup:GetCoalition() == 2 or spearheadGroup:GetCoalition() == 0 then
table.insert(self._blueGroups, spearheadGroup) table.insert(self._blueGroups, spearheadGroup)
end end
@@ -78,7 +72,7 @@ function BlueSam.New(database, logger, zoneName, spawnManager)
--Cleanup units --Cleanup units
local cleanup_distance = 5 local cleanup_distance = 5
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do for _, blueUnitPos in pairs(blueUnitsPos) do
for redUnitName, redUnitPos in pairs(redUnitsPos) do for redUnitName, redUnitPos in pairs(redUnitsPos) do
local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos) local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos)
if distance <= cleanup_distance then if distance <= cleanup_distance then
@@ -89,7 +83,7 @@ function BlueSam.New(database, logger, zoneName, spawnManager)
local zone = DcsUtil.getZoneByName(zoneName) local zone = DcsUtil.getZoneByName(zoneName)
if zone then if zone then
BuildableZone.New(self, zone, self._buildableCrateKilos or 0, "SAM_CRATE", self._blueGroups, logger, database) BuildableZone.New(self, zone, self._buildableCrateKilos or 0, "SAM_CRATE", self._blueGroups, logger, database, blueSamData.briefing)
end end
return self return self
@@ -140,7 +134,9 @@ end
function BlueSam:SpawnGroups() function BlueSam:SpawnGroups()
for unitName, needsCleanup in pairs(self._cleanupUnits) do for unitName, needsCleanup in pairs(self._cleanupUnits) do
DcsUtil.DestroyUnit(unitName) if needsCleanup then
DcsUtil.DestroyUnit(unitName)
end
end end
for _, group in pairs(self._blueGroups) do for _, group in pairs(self._blueGroups) do
@@ -42,11 +42,11 @@ function FarpZone.New(database, logger, zoneName, spawnManager)
self._groups = {} self._groups = {}
self._padNames = {} self._padNames = {}
self._supplyHubs = {} self._supplyHubs = {}
if farpData then if farpData then
self._padNames = farpData.padNames self._padNames = farpData.padNames
for _, supplyHubName in pairs(farpData.supplyHubNames) do for _, supplyHubName in pairs(farpData.supplyHubNames) do
local supplyHub = SupplyHub.new(database, logger, supplyHubName) local supplyHub = SupplyHub.new(database, logger, supplyHubName)
if supplyHub then if supplyHub then
@@ -55,7 +55,7 @@ function FarpZone.New(database, logger, zoneName, spawnManager)
end end
for _, groupName in pairs(farpData.groups) do for _, groupName in pairs(farpData.groups) do
local group = SpearheadGroup.New(groupName, spawnManager, true) local group = SpearheadGroup.New(groupName, spawnManager, true)
table.insert(self._groups, group) table.insert(self._groups, group)
group:Destroy() group:Destroy()
@@ -64,7 +64,7 @@ function FarpZone.New(database, logger, zoneName, spawnManager)
local zone = DcsUtil.getZoneByName(zoneName) local zone = DcsUtil.getZoneByName(zoneName)
if zone then if zone then
self._logger:debug("Creating Buildable zone: " .. zoneName .. " with " .. (farpData.buildingKilos or "nil") .. " kilos") self._logger:debug("Creating Buildable zone: " .. zoneName .. " with " .. (farpData.buildingKilos or "nil") .. " kilos")
BuildableZone.New(self, zone, farpData.buildingKilos or 0, "FARP_CRATE", self._groups, logger, database) BuildableZone.New(self, zone, farpData.buildingKilos or 0, "FARP_CRATE", self._groups, logger, database, farpData.briefing)
end end
end end
self:Deactivate() self:Deactivate()
@@ -90,7 +90,7 @@ function StageBase.New(databaseManager, logger, airbaseName, spawnManager)
local cleanup_distance = 5 local cleanup_distance = 5
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do for _, blueUnitPos in pairs(blueUnitsPos) do
for redUnitName, redUnitPos in pairs(redUnitsPos) do for redUnitName, redUnitPos in pairs(redUnitsPos) do
local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos) local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos)
if distance <= cleanup_distance then if distance <= cleanup_distance then
@@ -172,7 +172,7 @@ function StageBase:ActivateBlueStage()
else else
self:FinaliseBlueStage() self:FinaliseBlueStage()
end end
end end
function StageBase:FinaliseBlueStage() function StageBase:FinaliseBlueStage()
@@ -42,7 +42,7 @@ function SupplyHub.new(database, logger, zoneName)
end end
self._zone = DcsUtil.getZoneByName(zoneName) self._zone = DcsUtil.getZoneByName(zoneName)
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate() self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate()
self._inZone = {} self._inZone = {}
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate() self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
@@ -20,7 +20,8 @@ BuildableZone.__index = BuildableZone
---@param database Database ---@param database Database
---@param crateType SupplyType ---@param crateType SupplyType
---@param logger Logger ---@param logger Logger
function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroups, logger, database) ---@param briefing string?
function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroups, logger, database, briefing)
self._targetZone = targetZone self._targetZone = targetZone
self._requiredKilos = kilosRequired or 0 self._requiredKilos = kilosRequired or 0
self._buildableGroups = buildableGroups or {} self._buildableGroups = buildableGroups or {}
@@ -49,7 +50,7 @@ function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroup
if params.unpackedKilos >= params.kilos or spawned == false then if params.unpackedKilos >= params.kilos or spawned == false then
return return
end end
return time + 0.5 return time + 0.5
end end
@@ -69,14 +70,12 @@ function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroup
local noLandingZone = self:GetNoLandingZone() local noLandingZone = self:GetNoLandingZone()
if kilosRequired and kilosRequired > 0 then if kilosRequired and kilosRequired > 0 then
self._buildableMission = BuildableMission.new(database, logger, targetZone, noLandingZone, kilosRequired, crateType) self._buildableMission = BuildableMission.new(database, logger, targetZone, noLandingZone, kilosRequired, crateType, briefing)
self._buildableMission:AddOnCrateDroppedOfListener(self) self._buildableMission:AddOnCrateDroppedOfListener(self)
else else
self._buildableMission = nil self._buildableMission = nil
end end
if self._buildableMission == nil then if self._buildableMission == nil then
self._buildableLogger:debug("No buildable mission for zone: " .. targetZone.name) self._buildableLogger:debug("No buildable mission for zone: " .. targetZone.name)
end end
@@ -98,23 +97,11 @@ function BuildableZone:OnBuildingComplete() end
---@field unpackedItems number ---@field unpackedItems number
---@field unpackedKilos number ---@field unpackedKilos number
---@param mission BuildableMission? ---@param _ BuildableMission?
---@param kilos number ---@param kilos number
function BuildableZone:OnCrateDroppedOff(mission, kilos) function BuildableZone:OnCrateDroppedOff(_, kilos)
self._buildableLogger:debug("Crate dropped off in zone: " .. self._targetZone.name) self._buildableLogger:debug("Crate dropped off in zone: " .. self._targetZone.name)
local timeToUnpack = (kilos / 500) * 15
---@type UnpackCrateParam
local params = {
self = self,
groupsPerKilo = self._groupsPerKilo,
unpackedItems = 0,
kilosPerSecond = kilos/timeToUnpack,
unpackedKilos = 0,
kilos = kilos
}
---@param params UnpackCrateParam ---@param params UnpackCrateParam
---@param time number ---@param time number
local startUnpackingCrate = function(params, time) local startUnpackingCrate = function(params, time)
@@ -131,10 +118,21 @@ function BuildableZone:OnCrateDroppedOff(mission, kilos)
params.self:FinaliseCrate(params.kilos) params.self:FinaliseCrate(params.kilos)
return return
end end
return time + 2 return time + 2
end end
local timeToUnpack = (kilos / 500) * 15
---@type UnpackCrateParam
local params = {
self = self,
groupsPerKilo = self._groupsPerKilo,
unpackedItems = 0,
kilosPerSecond = kilos/timeToUnpack,
unpackedKilos = 0,
kilos = kilos
}
timer.scheduleFunction(startUnpackingCrate, params, timer.getTime() + 2) timer.scheduleFunction(startUnpackingCrate, params, timer.getTime() + 2)
end end
@@ -189,7 +187,7 @@ function BuildableZone:SpawnAmount(amount)
return nil return nil
end end
for i = 1, amount do for _ = 1, amount do
local spawned = spawnOne() local spawned = spawnOne()
if spawned ~= true then if spawned ~= true then
self._buildableLogger:debug("No more groups to spawn in zone: " .. self._targetZone.name) self._buildableLogger:debug("No more groups to spawn in zone: " .. self._targetZone.name)
+178
View File
@@ -0,0 +1,178 @@
---@class StageLane
---@field private _isDefaultStageLane boolean
---@field private _stageLaneIdentifier string
---@field private _activeStageIndex number
---@field private _stagesInLaneByIndex table<number, Array<Stage>>
---@field private _chapterStarts table<number, boolean>?
---@field private _stageLaneState StageLaneState
---@field private _maxStageIndex number
local StageLane = {}
StageLane.__index = StageLane
---@alias StageLaneState
---| "InChapter"
---| "BetweenChapters"
---| "Completed"
StageLane.DefaultLaneKey = nil
function StageLane.New(laneIdentifier)
if not laneIdentifier then
laneIdentifier = StageLane.DefaultLaneKey
end
local self = setmetatable({}, { __index = StageLane }) --[[@as StageLane]]
self._isDefaultStageLane = laneIdentifier == StageLane.DefaultLaneKey
self._stageLaneIdentifier = laneIdentifier
self._activeStageIndex = nil
self._stagesInLaneByIndex = {}
self._chapterStarts = nil
self._stageLaneState = "BetweenChapters"
return self
end
---@param stage Stage
function StageLane:AddStage(stage)
local stageIndex = stage:GetStageIndex()
if not self._stagesInLaneByIndex[stageIndex] then
self._stagesInLaneByIndex[stageIndex] = {}
end
if not self._maxStageIndex or stageIndex > self._maxStageIndex then
self._maxStageIndex = stageIndex
end
table.insert(self._stagesInLaneByIndex[stageIndex], stage)
end
---@param stageNumber number
---@return boolean?
function StageLane:IsStageIndexComplete(stageNumber)
local stages = self._stagesInLaneByIndex[stageNumber]
if not stages then
return nil
end
for _, stage in ipairs(stages) do
if not stage:IsComplete() then
return false
end
end
return true
end
---@return boolean
function StageLane:IsCurrentStageIndexComplete()
return self:IsStageIndexComplete(self._activeStageIndex) == true
end
---@return string?
function StageLane:GetStageLaneIdentifier()
return self._stageLaneIdentifier
end
---@return boolean
function StageLane:IsDefaultStageLane()
return self._isDefaultStageLane
end
---@return number
function StageLane:GetActiveStageIndex()
return self._activeStageIndex
end
---@param stageNumber number
function StageLane:SetActiveStageIndex(stageNumber)
if self._stagesInLaneByIndex[stageNumber] == nil then
self._stageLaneState = "BetweenChapters" -- stage number is not in this lane, so we are between chapters
elseif stageNumber > self._maxStageIndex then
self._stageLaneState = "Completed" -- stage number is beyond the max stage index
else
self._stageLaneState = "InChapter"
end
self._activeStageIndex = stageNumber
end
---@return StageLaneState
function StageLane:GetStageLaneState()
return self._stageLaneState
end
--- Checks if the given stage number is a chapter start. <br>
--- Chapter starts are the first StageIndex after a gap in stage numbers. <br>
--- For example, if the stage numbers are 1, 2, 3, 5, 6, 7, then stage number 5 is a chapter start because there is a gap between 3 and 5. <br>
---@param stageNumber number
function StageLane:IsChapterStart(stageNumber)
if self._chapterStarts == nil then
self:FillChapterStarts()
end
return self._chapterStarts[tostring(stageNumber)] == true
end
--- fills chapter starts, chapter starts are where there's a gap in between stage numbers.
--- The first stage in a lane is always a chapter start.
---@private
function StageLane:FillChapterStarts()
local previousIndex = nil
local stageIndices = {}
for stageIndex, _ in pairs(self._stagesInLaneByIndex) do
table.insert(stageIndices, tonumber(stageIndex))
end
table.sort(stageIndices)
self._chapterStarts = self._chapterStarts or {}
for _, stageIndex in ipairs(stageIndices) do
if previousIndex == nil then
-- First stage is always a chapter start
self._chapterStarts[tostring(stageIndex)] = true
elseif stageIndex > previousIndex + 1 then
-- Gap before this stage means it's a chapter start
self._chapterStarts[tostring(stageIndex)] = true
else
-- Consecutive stage, not a chapter start
self._chapterStarts[tostring(stageIndex)] = false
end
previousIndex = stageIndex
end
end
---@return Array<number>
function StageLane:GetAllStageIndices()
local indices = {}
for stageIndex, _ in pairs(self._stagesInLaneByIndex) do
table.insert(indices, tonumber(stageIndex))
end
table.sort(indices)
return indices
end
---@param stageIndex number
---@return Array<Stage>?
function StageLane:GetStagesAtIndex(stageIndex)
return self._stagesInLaneByIndex[stageIndex]
end
---@return number?
function StageLane:GetNextChapterStart()
if self._chapterStarts == nil then
self:FillChapterStarts()
end
for stageIndex, _ in pairs(self._stagesInLaneByIndex) do
if tonumber(stageIndex) > self._activeStageIndex and self._chapterStarts[tostring(stageIndex)] == true then
return tonumber(stageIndex)
end
end
return nil
end
return StageLane
@@ -0,0 +1,59 @@
local StageLane = require("classes.stageClasses.StageLane")
---@class StageRepository
---@field private StageLanes table<string, StageLane>
local StageRepository = {}
StageRepository.__index = StageRepository
local instance = nil
---@private
---@return StageRepository
function StageRepository.new()
local self = setmetatable({}, StageRepository)
self.StageLanes = {}
self.StageLanes[tostring(StageLane.DefaultLaneKey)] = StageLane.New(StageLane.DefaultLaneKey)
instance = self
return self
end
---@return StageRepository
StageRepository.getInstance = function()
if instance == nil then
instance = StageRepository.new()
end
return instance
end
---@param laneName string? `nil` for default lane
---@return StageLane?
function StageRepository:getStageLane(laneName)
if not laneName then
laneName = StageLane.DefaultLaneKey
end
return self.StageLanes[tostring(laneName)]
end
---@return Array<StageLane>
function StageRepository:getAllStageLanes()
local lanes = {}
for _, lane in pairs(self.StageLanes) do
table.insert(lanes, lane)
end
return lanes
end
---@param stage Stage
function StageRepository:AddStage(stage)
local stageLaneIdentifier = stage:GetStageLaneIdentifier() or StageLane.DefaultLaneKey
if self.StageLanes[tostring(stageLaneIdentifier)] == nil then
self.StageLanes[tostring(stageLaneIdentifier)] = StageLane.New(stageLaneIdentifier)
end
self.StageLanes[tostring(stageLaneIdentifier)]:AddStage(stage)
end
return StageRepository
@@ -13,12 +13,19 @@ local Events = require("classes.spearhead_events")
local GlobalCapManager = require("classes.capClasses.GlobalCapManager") local GlobalCapManager = require("classes.capClasses.GlobalCapManager")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing") local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper") local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
local StageRepository = require("classes.stageClasses.StageRepository")
local StageState = require("classes.stageClasses.Stages.BaseStage.StageState")
---@alias StageColor ---@alias StageColor
---| "RED" ---| "RED"
---| "BLUE" ---| "BLUE"
---| "GRAY" ---| "GRAY"
---@alias StageType
---| "PrimaryStage"
---| "ExtraStage"
---| "WaitingStage"
--- @class StageData --- @class StageData
--- @field stageBriefing string? --- @field stageBriefing string?
--- @field missionsByCode table<string, Mission> --- @field missionsByCode table<string, Mission>
@@ -33,9 +40,11 @@ local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelpe
--- @field supplyHubs Array<SupplyHub> --- @field supplyHubs Array<SupplyHub>
--- @class StageInitData --- @class StageInitData
--- @field type StageType
--- @field stageZoneName string --- @field stageZoneName string
--- @field stageNumber integer --- @field stageNumber integer
--- @field stageDisplayName string --- @field stageDisplayName string
--- @field stageLaneIdentifier string?
--- @class StageCompleteListener --- @class StageCompleteListener
@@ -45,13 +54,15 @@ local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelpe
--- @field zoneName string --- @field zoneName string
--- @field stageName string? --- @field stageName string?
--- @field stageNumber number --- @field stageNumber number
--- @field protected _stageLaneIdentifier string?
--- @field protected _stageType StageType
--- @field protected _currentStageState CurrentStageState --- @field protected _currentStageState CurrentStageState
--- @field protected _missionCommandsHelper MissionCommandsHelper --- @field protected _missionCommandsHelper MissionCommandsHelper
--- @field protected _missionPriority MissionPriority --- @field protected _missionPriority MissionPriority
--- @field protected _database Database --- @field protected _database Database
--- @field protected _stageRepository StageRepository
--- @field protected _db StageData --- @field protected _db StageData
--- @field protected _logger Logger --- @field protected _logger Logger
--- @field protected _activeStage integer
--- @field protected _stageConfig StageConfig --- @field protected _stageConfig StageConfig
--- @field protected _customDrawing CustomDrawing --- @field protected _customDrawing CustomDrawing
--- @field protected _spawnedGroups Array<string> --- @field protected _spawnedGroups Array<string>
@@ -63,14 +74,6 @@ local Stage = {}
Stage.__index = Stage Stage.__index = Stage
---@enum CurrentStageState
Stage.CurrentStageState = {
INACTIVE = 0,
PREACTIVATED = 1,
ACTIVE = 2,
BLUE = 3
}
Stage.StageColors = { Stage.StageColors = {
INVISIBLE = { 0, 0, 0, 0 }, INVISIBLE = { 0, 0, 0, 0 },
RED_ACTIVE = { 1, 0, 0, 0.20 }, RED_ACTIVE = { 1, 0, 0, 0.20 },
@@ -86,16 +89,23 @@ Stage.StageColors = {
---@param initData StageInitData ---@param initData StageInitData
---@param missionPriority MissionPriority ---@param missionPriority MissionPriority
---@param spawnManager SpawnManager ---@param spawnManager SpawnManager
---@param stageType StageType
---@return Stage ---@return Stage
function Stage:superNew(database, stageConfig, logger, initData, missionPriority, spawnManager) function Stage:superNew(database, stageConfig, logger, initData, stageType, missionPriority, spawnManager)
logger:debug("[BaseStage] Initiating stage with name: " .. initData.stageZoneName) logger:debug("[BaseStage] Initiating stage with name: " .. initData.stageZoneName)
self._currentStageState = Stage.CurrentStageState.INACTIVE self._currentStageState = StageState.Inactive
self.zoneName = initData.stageZoneName self.zoneName = initData.stageZoneName
self.stageNumber = initData.stageNumber self.stageNumber = initData.stageNumber
self._stageRepository = StageRepository.getInstance()
if initData.stageLaneIdentifier then
self._stageLaneIdentifier = string.lower(initData.stageLaneIdentifier)
else
self._stageLaneIdentifier = nil
end
self.stageName = initData.stageDisplayName self.stageName = initData.stageDisplayName
self._currentStageState = Stage.CurrentStageState.INACTIVE self._stageType = stageType
self.OnPostStageComplete = nil self.OnPostStageComplete = nil
self.OnPostBlueActivated = nil self.OnPostBlueActivated = nil
@@ -115,7 +125,6 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
supplyHubs = {} supplyHubs = {}
} }
self._activeStage = -99
self._stageConfig = stageConfig or {} self._stageConfig = stageConfig or {}
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate() self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
@@ -151,25 +160,25 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
self._logger:info("Initiating new Stage with name: " .. self.zoneName) self._logger:info("Initiating new Stage with name: " .. self.zoneName)
---comment ---comment
---@param self Stage ---@param selfA Stage
---@param time number? ---@param time number?
self.CheckContinuousAsync = function (self, time) self.CheckContinuousAsync = function (selfA, time)
self:CheckAndUpdateSelf() selfA:CheckAndUpdateSelf()
if self:IsComplete() == true then if selfA:IsComplete() == true then
self:NotifyComplete() selfA:NotifyComplete()
return nil return nil
end end
return time + 20 return time + 20
end end
do -- load tables do -- load tables
local missionZones = database:getMissionsForStage(self.zoneName) local missionZones = database:getMissionsForStage(self.zoneName)
self._logger:debug("Found " .. Util.tableLength(missionZones) .. " mission zones for stage: " .. self.zoneName) self._logger:debug("Found " .. Util.tableLength(missionZones) .. " mission zones for stage: " .. self.zoneName)
for _, missionZone in pairs(missionZones) do for _, missionZone in pairs(missionZones) do
local mission = ZoneMission.new(missionZone, self._missionPriority, database, logger, self, spawnManager) local mission = ZoneMission.new(missionZone, self._missionPriority, database, logger, self, spawnManager)
if mission then if mission then
self._db.missionsByCode[mission.code] = mission self._db.missionsByCode[mission.code] = mission
@@ -224,7 +233,7 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
table.insert(self._db.missions, mission) table.insert(self._db.missions, mission)
end end
end end
else else
self._logger:info("Using persisted random mission with name: " .. missionName .. " and zone: " .. missionZonePicked) self._logger:info("Using persisted random mission with name: " .. missionName .. " and zone: " .. missionZonePicked)
for _, mission in pairs(missions) do for _, mission in pairs(missions) do
if string.lower(mission.zoneName) == string.lower(missionZonePicked) then if string.lower(mission.zoneName) == string.lower(missionZonePicked) then
@@ -273,29 +282,59 @@ end
---@return boolean ---@return boolean
function Stage:IsComplete() function Stage:IsComplete()
if self._currentStageState == Stage.CurrentStageState.BLUE then return true end if self._currentStageState >= StageState.Blue then return true end
for i, mission in pairs(self._db.sams) do for _, mission in pairs(self._db.sams) do
local state = mission:getState() local state = mission:getState()
if state == "ACTIVE" or state == "NEW" or state =="WAITING" then if state == "ACTIVE" or state == "NEW" or state =="WAITING" then
return false return false
end end
end end
for i, mission in pairs(self._db.missions) do for _, mission in pairs(self._db.missions) do
local state = mission:getState() local state = mission:getState()
if state == "ACTIVE" or state == "NEW" then if state == "ACTIVE" or state == "NEW" then
return false return false
end end
end end
self._currentStageState = Stage.CurrentStageState.BLUE
return true return true
end end
---@return boolean ---@return boolean
function Stage:IsActive() function Stage:IsActive()
return self._currentStageState == Stage.CurrentStageState.ACTIVE return self._currentStageState == StageState.Activated
end
---@return StageType
function Stage:GetStageType()
return self._stageType
end
---@return string?
function Stage:GetStageLaneIdentifier()
return self._stageLaneIdentifier
end
---@return number
function Stage:GetStageIndex()
return self.stageNumber
end
---@return string
function Stage:GetStageName()
return self.stageName
end
function Stage:GetMissions()
local missions = {}
for _, mission in pairs(self._db.missions) do
table.insert(missions, mission)
end
for _, sam in pairs(self._db.sams) do
table.insert(missions, sam)
end
return missions
end end
---comment ---comment
@@ -331,9 +370,9 @@ function Stage:CheckAndUpdateSelf()
local availableMissionsCount = Util.tableLength(getAvailableMissions()) local availableMissionsCount = Util.tableLength(getAvailableMissions())
local activeCount = getActiveMissionsCount() local activeCount = getActiveMissionsCount()
if activeCount < max and availableMissionsCount > 0 then if activeCount < max and availableMissionsCount > 0 then
for i = activeCount+1, max do for _ = activeCount+1, max do
if availableMissionsCount == 0 then if availableMissionsCount == 0 then
i = max+1 --exits this loop break
else else
local mission = Util.randomFromList(getAvailableMissions()) --[[@as Mission]] local mission = Util.randomFromList(getAvailableMissions()) --[[@as Mission]]
if mission then if mission then
@@ -358,7 +397,7 @@ function Stage:IsMissionComplete(missionName)
return mission:getState() == "COMPLETED" return mission:getState() == "COMPLETED"
end end
---private use only -- private usage only
function Stage:NotifyComplete() function Stage:NotifyComplete()
self._logger:info("Stage complete: " .. (self.stageName or self.stageNumber or "unknown")) self._logger:info("Stage complete: " .. (self.stageName or self.stageNumber or "unknown"))
@@ -381,19 +420,22 @@ end
---Activates all SAMS, Airbase units etc all at once. ---Activates all SAMS, Airbase units etc all at once.
function Stage:PreActivate() function Stage:PreActivate()
if self._currentStageState == Stage.CurrentStageState.INACTIVE then
self._currentStageState = Stage.CurrentStageState.PREACTIVATED
for key, mission in pairs(self._db.sams) do
if mission then
mission:SpawnInactive()
end
end
for _, airbase in pairs(self._db.airbases) do if self._currentStageState >= StageState.PreActivated then
airbase:ActivateRedStage() return
end
self._currentStageState = StageState.PreActivated
for _, mission in pairs(self._db.sams) do
if mission then
mission:SpawnInactive()
end end
end end
for _, airbase in pairs(self._db.airbases) do
airbase:ActivateRedStage()
end
self:MarkStage() self:MarkStage()
end end
@@ -407,22 +449,22 @@ function Stage:MarkStage()
end end
if self._stageConfig.isDrawStagesEnabled == false then return end if self._stageConfig.isDrawStagesEnabled == false then return end
if self._stageConfig.isDrawPreActivatedEnabled == false and self._currentStageState == Stage.CurrentStageState.PREACTIVATED then if self._stageConfig.isDrawPreActivatedEnabled == false and self._currentStageState == StageState.PreActivated then
return return
end end
if self._customDrawing then if self._customDrawing then
self._customDrawing:UpdateDrawingObject(function(drawingObject) self._customDrawing:UpdateDrawingObject(function(drawingObject)
local drawing = drawingObject --[[@as Polygon]] local drawing = drawingObject --[[@as Polygon]]
if self._currentStageState == Stage.CurrentStageState.ACTIVE then if self._currentStageState == StageState.Activated then
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_ACTIVE) drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_ACTIVE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_ACTIVE) drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_ACTIVE)
drawing.style = "dot dash" drawing.style = "dot dash"
elseif self._currentStageState == Stage.CurrentStageState.PREACTIVATED then elseif self._currentStageState == StageState.PreActivated then
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_PREACTIVE) drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_PREACTIVE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_PREACTIVE) drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_PREACTIVE)
drawing.style = "no line" drawing.style = "no line"
elseif self._currentStageState == Stage.CurrentStageState.BLUE then elseif self._currentStageState == StageState.Blue then
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.BLUE) drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.BLUE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.BLUE) drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.BLUE)
drawing.style = "two dash" drawing.style = "two dash"
@@ -431,7 +473,7 @@ function Stage:MarkStage()
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE) drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE)
drawing.style = "no line" drawing.style = "no line"
end end
return drawing return drawing
end) end)
self._customDrawing:Draw() self._customDrawing:Draw()
@@ -440,10 +482,13 @@ function Stage:MarkStage()
end end
function Stage:ActivateStage() function Stage:ActivateStage()
self._currentStageState = Stage.CurrentStageState.ACTIVE
if self._currentStageState >= StageState.Activated then
return
end
self:PreActivate() self:PreActivate()
self._currentStageState = StageState.Activated
pcall(function() pcall(function()
self:MarkStage() self:MarkStage()
end) end)
@@ -471,6 +516,10 @@ function Stage:ActivateStage()
end end
end end
if self._db and self._db.stageBriefing then
self._missionCommandsHelper:AddStageBriefing(self.zoneName, self._db.stageBriefing)
end
timer.scheduleFunction(self.CheckContinuousAsync, self, timer.getTime() + 3) timer.scheduleFunction(self.CheckContinuousAsync, self, timer.getTime() + 3)
end end
@@ -483,52 +532,68 @@ end
---comment ---comment
---@param self Stage ---@param self Stage
---@param number integer ---@param number integer
function Stage:OnStageNumberChanged(number) ---@param stageLaneIdentifier string?
function Stage:OnStageNumberChanged(number, stageLaneIdentifier)
if self._activeStage == number then --only activate once for a stage ---@return boolean
return local needsPreActivation = function()
if self._stageLaneIdentifier == stageLaneIdentifier then
if self.stageNumber <= number + self._stageConfig.AmountPreactivateStage then
return true
end
elseif self._stageLaneIdentifier == nil then
--main lane
--Check if this stage is a chapter stage
if self._stageRepository:getStageLane(self._stageLaneIdentifier):IsChapterStart(self.stageNumber)
and self.stageNumber < number + self._stageConfig.AmountPreactivateStage
then
return true
end
if GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
return true
end
end
return false
end end
local previousActive = self._activeStage ---@return boolean
self._activeStage = number local needsActivation = function()
if self._stageLaneIdentifier == stageLaneIdentifier and self.stageNumber == number then
return true
end
return false
end
if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then ---@return boolean
self._logger:debug("Pre-activating stage: " .. self.zoneName .. " with number: " .. number) local needsBlueActivation = function()
self:PreActivate() if self._stageLaneIdentifier == stageLaneIdentifier and self.stageNumber < number then
elseif GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then return true
end
return false
end
if needsPreActivation() == true then
self:PreActivate() self:PreActivate()
end end
if number == self.stageNumber then if needsActivation() == true then
self:ActivateStage() self:ActivateStage()
if self._db and self._db.stageBriefing then
self._missionCommandsHelper:AddStageBriefing(self.zoneName, self._db.stageBriefing)
end
end end
if previousActive <= self.stageNumber then if needsBlueActivation() == true then
if number > self.stageNumber then self:ActivateBlueStage()
self:ActivateBlueStage()
end
end end
if number > self.stageNumber then
self._missionCommandsHelper:RemoveStageBriefing(self.zoneName)
end
end
function Stage:GetBriefing()
return "Briefing For "
end end
---@param self Stage ---@param self Stage
---@param mission Mission ---@param _ Mission
Stage.OnMissionComplete = function(self, mission) Stage.OnMissionComplete = function(self, _)
self:CheckAndUpdateSelf() self:CheckAndUpdateSelf()
end end
---private use only ---private use only
function Stage:ActivateBlueGroups() function Stage:ActivateBlueGroups()
@@ -580,7 +645,7 @@ function Stage:GetStageStats()
end end
end end
for _, mission in pairs(self._db.sams) do for _, _ in pairs(self._db.sams) do
dead = dead + 1 dead = dead + 1
end end
@@ -590,8 +655,12 @@ end
function Stage:ActivateBlueStage() function Stage:ActivateBlueStage()
if self._currentStageState >= StageState.Blue then
return
end
self._logger:debug("Setting stage '" .. Util.toString(self.zoneName) .. "' to blue") self._logger:debug("Setting stage '" .. Util.toString(self.zoneName) .. "' to blue")
self._currentStageState = Stage.CurrentStageState.BLUE self._currentStageState = StageState.Blue
for _, mission in pairs(self._db.missions) do for _, mission in pairs(self._db.missions) do
mission:SpawnPersistedState() mission:SpawnPersistedState()
end end
@@ -604,17 +673,19 @@ function Stage:ActivateBlueStage()
miscGroup:Spawn() miscGroup:Spawn()
end end
---@param self Stage ---@param selfA Stage
local ActivateBlueAsync = function(self) local ActivateBlueAsync = function(selfA)
pcall(function() pcall(function()
self:MarkStage() selfA:MarkStage()
end) end)
self:ActivateBlueGroups() selfA:ActivateBlueGroups()
return nil return nil
end end
self._missionCommandsHelper:RemoveStageBriefing(self.zoneName)
timer.scheduleFunction(ActivateBlueAsync, self, timer.getTime() + 3) timer.scheduleFunction(ActivateBlueAsync, self, timer.getTime() + 3)
end end
@@ -0,0 +1,12 @@
--- StageState enumeration for the current stage state.
--- Chronological order, meaning that higher values should represent later stages in the sequence.
---@enum CurrentStageState
local StageState = {
Inactive = 0,
PreActivated = 1,
Activated = 2,
Blue = 3
}
return StageState
+10 -6
View File
@@ -1,6 +1,7 @@
local Stage = require("classes.stageClasses.Stages.BaseStage.Stage") local Stage = require("classes.stageClasses.Stages.BaseStage.Stage")
local GlobalCapManager = require("classes.capClasses.GlobalCapManager") local GlobalCapManager = require("classes.capClasses.GlobalCapManager")
local StageState = require("classes.stageClasses.Stages.BaseStage.StageState")
---@class ExtraStage : Stage ---@class ExtraStage : Stage
local ExtraStage = {} local ExtraStage = {}
@@ -19,13 +20,12 @@ function ExtraStage.New(database, stageConfig, logger, initData, spawnManager)
setmetatable(ExtraStage, Stage) setmetatable(ExtraStage, Stage)
local self = setmetatable({}, { __index = ExtraStage }) --[[@as ExtraStage]] local self = setmetatable({}, { __index = ExtraStage }) --[[@as ExtraStage]]
self:superNew(database, stageConfig, logger, initData, "secondary", spawnManager) self:superNew(database, stageConfig, logger, initData, "ExtraStage", "secondary", spawnManager)
self.OnPostBlueActivated = function (selfStage) self.OnPostBlueActivated = function (selfStage)
selfStage:MarkStage() selfStage:MarkStage()
end end
self.OnPostStageComplete = function (selfStage) self.OnPostStageComplete = function (selfStage)
selfStage:ActivateBlueStage() selfStage:ActivateBlueStage()
end end
@@ -36,13 +36,17 @@ end
---comment ---comment
---@param self ExtraStage ---@param self ExtraStage
---@param number integer ---@param number integer
function ExtraStage:OnStageNumberChanged(number) ---@param stageLaneIdentifier string?
function ExtraStage:OnStageNumberChanged(number, stageLaneIdentifier)
if stageLaneIdentifier ~= self._stageLaneIdentifier then
return
end
if self._activeStage == number then --only activate once for a stage if self._activeStage == number then --only activate once for a stage
return return
end end
local previousActive = self._activeStage
self._activeStage = number self._activeStage = number
if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then
@@ -56,7 +60,7 @@ function ExtraStage:OnStageNumberChanged(number)
self:ActivateStage() self:ActivateStage()
end end
if self._currentStageState == Stage.CurrentStageState.BLUE then if self._currentStageState == StageState.Blue then
self:ActivateBlueStage() self:ActivateBlueStage()
end end
@@ -18,7 +18,7 @@ function PrimaryStage.New(database, stageConfig, logger, initData, spawnManager)
setmetatable(PrimaryStage, Stage) setmetatable(PrimaryStage, Stage)
local self = setmetatable({}, { __index = PrimaryStage }) --[[@as PrimaryStage]] local self = setmetatable({}, { __index = PrimaryStage }) --[[@as PrimaryStage]]
self:superNew(database, stageConfig, logger, initData, "primary", spawnManager) self:superNew(database, stageConfig, logger, initData, "PrimaryStage", "primary", spawnManager)
return self return self
end end
@@ -4,32 +4,28 @@ local Stage = require("classes.stageClasses.Stages.BaseStage.Stage")
---@field private _waitTimeSeconds integer ---@field private _waitTimeSeconds integer
---@field private _startTime number ---@field private _startTime number
local WaitingStage = {} local WaitingStage = {}
WaitingStage.__index = WaitingStage WaitingStage.__index = WaitingStage
---@class WaitingStageInitData : StageInitData
---@field waitingSeconds integer
local WaitingStageInitData = {}
---comment ---comment
---@param database Database ---@param database Database
---@param stageConfig StageConfig ---@param stageConfig StageConfig
---@param logger any ---@param logger any
---@param initData WaitingStageInitData ---@param initData StageInitData
---@param spawnManager SpawnManager ---@param spawnManager SpawnManager
---@param waitingSeconds integer
---@return WaitingStage ---@return WaitingStage
function WaitingStage.New(database, stageConfig, logger, initData, spawnManager) function WaitingStage.New(database, stageConfig, logger, initData, waitingSeconds, spawnManager)
setmetatable(WaitingStage, Stage) setmetatable(WaitingStage, Stage)
local self = setmetatable({}, { __index = WaitingStage }) --[[@as WaitingStage]] local self = setmetatable({}, { __index = WaitingStage }) --[[@as WaitingStage]]
self:superNew(database, stageConfig, logger, initData, "none", spawnManager) self:superNew(database, stageConfig, logger, initData, "WaitingStage", "none", spawnManager)
self._waitTimeSeconds = 5 self._waitTimeSeconds = 5
if initData.waitingSeconds and initData.waitingSeconds > 5 then self._waitTimeSeconds = initData.waitingSeconds end if waitingSeconds and waitingSeconds > 5 then self._waitTimeSeconds = waitingSeconds end
self._startTime = nil self._startTime = nil
self.CheckContinuousAsync = function (selfA, time) self.CheckContinuousAsync = function (selfA, time)
if selfA:IsComplete() == true then if selfA:IsComplete() == true then
selfA:NotifyComplete() selfA:NotifyComplete()
return nil return nil
@@ -51,7 +47,7 @@ function WaitingStage:ActivateStage()
timer.scheduleFunction(self.CheckContinuousAsync, self, self._startTime + self._waitTimeSeconds) timer.scheduleFunction(self.CheckContinuousAsync, self, self._startTime + self._waitTimeSeconds)
end end
function WaitingStage:IsComplete() function WaitingStage:IsComplete()
if timer.getTime() > (self._startTime + self._waitTimeSeconds) then return true end if timer.getTime() > (self._startTime + self._waitTimeSeconds) then return true end
return false return false
end end
@@ -60,12 +56,12 @@ function WaitingStage:OnStageNumberChanged()
self._logger:debug("Waiting Stage OnStageNumberChanged override") self._logger:debug("Waiting Stage OnStageNumberChanged override")
end end
function WaitingStage:MarkStage(stageColor) function WaitingStage:MarkStage(_)
self._logger:debug("Waiting Stage MarkStage override") self._logger:debug("Waiting Stage MarkStage override")
end end
function WaitingStage:GetExpectedTime() function WaitingStage:GetExpectedTime()
return self._startTime + self._waitTimeSeconds return self._startTime + self._waitTimeSeconds
end end
return WaitingStage return WaitingStage
@@ -1,38 +1,56 @@
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper") local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
local Util = require("classes.util.Util")
local Logger = require("classes.util.Logger") local Logger = require("classes.util.Logger")
local drawingLogger = Logger.new("CustomDrawing") local drawingLogger = Logger.new("CustomDrawing")
---@class CustomDrawing ---@class CustomDrawing
---@field private _id integer? ---@field protected _IDs Array<integer>?
---@field private _drawingObject DrawingObject ---@field protected _name string?
---@field private _startingStage number ---@field protected _drawingObject DrawingObject
---@field private _removeAtStage number
---@field private _logger Logger
local CustomDrawing = {} local CustomDrawing = {}
CustomDrawing.__index = CustomDrawing CustomDrawing.__index = CustomDrawing
---@protected
---@param drawingObject DrawingObject ---@param drawingObject DrawingObject
---@return CustomDrawing ---@return CustomDrawing?
function CustomDrawing.New(drawingObject) function CustomDrawing.New(drawingObject)
local self = setmetatable({}, CustomDrawing) local self = setmetatable({}, CustomDrawing)
self._drawingObject = drawingObject self._drawingObject = drawingObject
self._id = nil -- initiate ID as nil, when drawn it will be set to the ID returned by DrawingHelper.Draw self._IDs = {} -- initiate IDs as an empty array, when drawn it will be set to the IDs returned by DrawingHelper.Draw
self._logger = drawingLogger self._name = drawingObject.name
local name = drawingObject.name
local split = Util.split_string(name or "", "_")
local secondPart = split[2] or "1"
local splitPart = Util.split_string(secondPart, ":")
self._startingStage = tonumber(splitPart[1]) or 1
self._removeAtStage = tonumber(splitPart[2]) or math.huge
return self return self
end end
---@param points Array<Vec2>
---@param colorString string
---@param lineStyle LineType
---@param lineThickness number
---@return CustomDrawing?
function CustomDrawing.FromPoints(points, colorString, lineStyle, lineThickness)
if points == nil or #points < 3 then
drawingLogger:warn("CustomDrawing.FromPoints called with nil or less than 3 points")
return nil
end
---@type FreeLine
local drawingObject = {
mapX = 0,
mapY = 0,
points = points,
lineMode = "free",
name = "custom_drawing_" .. tostring(math.random(1000000)),
primitiveType = "Polygon",
polygonMode = "free",
visible = true,
style = DrawingHelper.ToLineStyleString(lineStyle),
colorString = colorString,
thickness = lineThickness,
closed = false
}
return CustomDrawing.New(drawingObject)
end
---@param zone SpearheadTriggerZone ---@param zone SpearheadTriggerZone
---@param colorString string ---@param colorString string
---@param fillColorString string ---@param fillColorString string
@@ -65,7 +83,6 @@ function CustomDrawing.FromZone(zone, colorString, fillColorString, lineStyle, l
end end
if zone.zone_type == "Polygon" then if zone.zone_type == "Polygon" then
---@type Free ---@type Free
local drawingObject = { local drawingObject = {
mapX = 0, mapX = 0,
@@ -80,7 +97,7 @@ function CustomDrawing.FromZone(zone, colorString, fillColorString, lineStyle, l
fillColorString = fillColorString, fillColorString = fillColorString,
thickness = lineThickness, thickness = lineThickness,
} }
return CustomDrawing.New(drawingObject) return CustomDrawing.New(drawingObject)
end end
end end
@@ -90,27 +107,25 @@ function CustomDrawing:GetName()
return self._drawingObject.name return self._drawingObject.name
end end
---@return number start
---@return number stop
function CustomDrawing:GetStartAndStop()
return self._startingStage, self._removeAtStage
end
function CustomDrawing:Draw() function CustomDrawing:Draw()
self._logger:debug("Drawing custom drawing with ID " .. tostring(self._id)) drawingLogger:debug("Drawing custom drawing with IDs " .. table.concat(self._IDs, ", "))
if self._id ~= nil then if self._IDs ~= nil then
DrawingHelper.Remove(self._id) for _, id in ipairs(self._IDs) do
DrawingHelper.Remove(id)
end
end end
self._id = DrawingHelper.Draw(self._drawingObject, self._logger) self._IDs = DrawingHelper.Draw(self._drawingObject)
end end
function CustomDrawing:Remove() function CustomDrawing:Remove()
if self._id ~= nil then if self._IDs ~= nil then
self._logger:debug("Removing custom drawing with ID " .. tostring(self._id)) for _, id in ipairs(self._IDs) do
DrawingHelper.Remove(self._id) drawingLogger:debug("Removing custom drawing with ID " .. tostring(id))
self._id = nil DrawingHelper.Remove(id)
end
self._IDs = {}
end end
end end
@@ -119,4 +134,4 @@ function CustomDrawing:UpdateDrawingObject(updateFunc)
self._drawingObject = updateFunc(self._drawingObject) self._drawingObject = updateFunc(self._drawingObject)
end end
return CustomDrawing return CustomDrawing
@@ -0,0 +1,93 @@
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local Util = require("classes.util.Util")
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
---@class StageDrawing : CustomDrawing
---@field private _startingStage number
---@field private _removeAtStage number
---@field private _stageLaneIdentifier string?
local StageDrawing = {}
StageDrawing.__index = StageDrawing
setmetatable(StageDrawing, CustomDrawing)
---@param drawingObject DrawingObject
---@return StageDrawing?
function StageDrawing.New(drawingObject)
local super = CustomDrawing.New(drawingObject)
if not super then
return nil
end
local self = setmetatable(super, StageDrawing) --[[@as StageDrawing]]
local split = Util.split_string(self._name or "", "_")
local secondPart = split[2] or "1"
local splitPart = Util.split_string(secondPart, ":")
if tonumber(splitPart[1]) == nil then
local laneIdentifier = string.sub(splitPart[1], 1, 1)
if tonumber(laneIdentifier) == nil then
self._stageLaneIdentifier = laneIdentifier
end
local startNumber = string.sub(splitPart[1], 2)
local startNumberVal = tonumber(startNumber)
if startNumberVal == nil then
MissionEditorWarnings.Add("Start number for " ..
self._name .. " is not a valid number: " .. tostring(startNumber))
return nil
end
self._startingStage = startNumberVal
else
local startNumber = tonumber(splitPart[1])
if startNumber == nil then
MissionEditorWarnings.Add("Start number for " ..
self._name .. " is not a valid number: " .. tostring(splitPart[1]))
return nil
end
self._startingStage = startNumber
end
if tonumber(splitPart[2]) == nil then
local laneIdentifier = string.sub(splitPart[2], 1, 1)
if tonumber(laneIdentifier) == nil then
if self._stageLaneIdentifier and self._stageLaneIdentifier ~= laneIdentifier then
MissionEditorWarnings.Add("Lane identifiers do not match for " ..
self._name ..
": " ..
self._stageLaneIdentifier ..
" vs " .. laneIdentifier .. ". Will only use stage " .. self._stageLaneIdentifier)
end
end
local stopNumber = string.sub(splitPart[2], 2)
local stopNumberVal = tonumber(stopNumber)
if stopNumberVal == nil then
MissionEditorWarnings.Add("Stop number for " ..
self._name .. " is not a valid number: " .. tostring(stopNumber))
return nil
end
self._removeAtStage = stopNumberVal
else
local stopNumber = tonumber(splitPart[2])
if stopNumber == nil then
MissionEditorWarnings.Add("Stop number for " ..
self._name .. " is not a valid number: " .. tostring(splitPart[2]))
return nil
end
self._removeAtStage = stopNumber
end
return self
end
function StageDrawing:GetStageLaneIdentifier()
return self._stageLaneIdentifier
end
function StageDrawing:GetStartingStage()
return self._startingStage
end
function StageDrawing:GetRemoveAtStage()
return self._removeAtStage
end
return StageDrawing
@@ -1,57 +1,44 @@
local Util = require("classes.util.Util") local DcsUtil = require("classes.util.DcsUtil")
local Logger = require("classes.util.Logger") local Logger = require("classes.util.Logger")
local GlobalConfig = require("classes.configuration.GlobalConfig") local GlobalConfig = require("classes.configuration.GlobalConfig")
---@type LogLevel ---@type LogLevel
local level = "INFO" local level = "INFO"
if GlobalConfig:isDebugEnabled() then if GlobalConfig.New():isDebugEnabled() then
level = "DEBUG" level = "DEBUG"
end end
local drawingLogger = Logger.new("DrawingHelper", level) local logger = Logger.new("DrawingHelper", level)
---@class DrawingHelper ---@class DrawingHelper
local DrawingHelper = {} local DrawingHelper = {}
DrawingHelper.__index = DrawingHelper DrawingHelper.__index = DrawingHelper
local customDrawingIdIncrementer = 4210
---@param object DrawingObject ---@param object DrawingObject
---@param logger Logger? ---@return Array<integer> id
---@return integer? id function DrawingHelper.Draw(object)
function DrawingHelper.Draw(object, logger)
if object == nil then if object == nil then
if logger then if logger then
logger:warn("DrawingHelper.Draw called with nil object") logger:warn("DrawingHelper.Draw called with nil object")
end end
return nil return {}
end
local id = DrawingHelper.GetAndAddId()
if logger then
logger:debug("Drawing object with ID " .. tostring(id) .. " and primitive type: " .. tostring(object.primitiveType))
end end
if(object.primitiveType == "Polygon") then if(object.primitiveType == "Polygon") then
DrawingHelper.DrawPolygon(object--[[@as Polygon]], id, logger) return DrawingHelper.DrawPolygon(object--[[@as Polygon]])
elseif(object.primitiveType == "Line") then elseif(object.primitiveType == "Line") then
DrawingHelper.DrawLine(object--[[@as Line]], id, logger) return DrawingHelper.DrawLine(object--[[@as Line]])
elseif(object.primitiveType == "TextBox") then elseif(object.primitiveType == "TextBox") then
DrawingHelper.DrawTextBox(object--[[@as TextBox]], id) return DrawingHelper.DrawTextBox(object--[[@as TextBox]])
else else
if logger then logger:warn("Unknown primitive type: " .. tostring(object.primitiveType))
logger:warn("Unknown primitive type: " .. tostring(object.primitiveType))
end
end end
return id return {}
end end
function DrawingHelper.GetAndAddId() function DrawingHelper.GetAndAddId()
customDrawingIdIncrementer = customDrawingIdIncrementer + 1 return DcsUtil.GetNextDrawID()
return customDrawingIdIncrementer
end end
---@private ---@private
@@ -62,8 +49,7 @@ end
---@param lineColor table ---@param lineColor table
---@param lineStyle LineType ---@param lineStyle LineType
---@param lineThickness number ---@param lineThickness number
---@param logger Logger? local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineStyle, lineThickness)
local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineStyle, lineThickness, logger)
if lineThickness == nil or lineThickness <= 0 then if lineThickness == nil or lineThickness <= 0 then
lineStyle = 0 lineStyle = 0
@@ -76,37 +62,34 @@ local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineS
end end
functionString = functionString .. "{0,1,0,1}, {0,1,0,1}, " .. lineStyle .. ")" functionString = functionString .. "{0,1,0,1}, {0,1,0,1}, " .. lineStyle .. ")"
if logger then logger:debug("Drawing complex drawing with ID " .. tostring(drawID) .. " and function string: " .. functionString)
logger:debug("Drawing complex drawing with ID " .. tostring(drawID) .. " and function string: " .. functionString)
end
---@diagnostic disable-next-line: deprecated ---@diagnostic disable-next-line: deprecated
local f, err = loadstring(functionString) local f, err = loadstring(functionString)
if f then if f then
f() f()
else else
env.error("Something failed when drawing complex drawing" .. err) logger:error("Something failed when drawing complex drawing" .. err)
end
if logger then
logger:debug("Drawing with fill color: " .. table.concat(fillColor, ",") .. " and line color: " .. table.concat(lineColor, ",") .. " and line style: " .. tostring(lineStyle) .. " and line thickness: " .. tostring(lineThickness))
end end
trigger.action.setMarkupColorFill(drawID, fillColor) if fillColor then
trigger.action.setMarkupColorFill(drawID, fillColor)
end
trigger.action.setMarkupColor(drawID, lineColor) trigger.action.setMarkupColor(drawID, lineColor)
trigger.action.setMarkupTypeLine(drawID, lineStyle) trigger.action.setMarkupTypeLine(drawID, lineStyle)
end end
---@private ---@private
---@param object Polygon ---@param object Polygon
---@param id integer ---@return Array<integer>
---@param logger Logger? function DrawingHelper.DrawPolygon(object)
function DrawingHelper.DrawPolygon(object, id, logger)
if object == nil then if object == nil then
return return {}
end end
local id = DrawingHelper.GetAndAddId()
---@param circle Circle ---@param circle Circle
local function DrawCircle(circle) local function DrawCircle(circle)
local vec3 = { x = circle.mapX, y = 0, z = circle.mapY } local vec3 = { x = circle.mapX, y = 0, z = circle.mapY }
@@ -115,15 +98,15 @@ function DrawingHelper.DrawPolygon(object, id, logger)
local style = DrawingHelper.ToLineStyleInteger(circle.style) local style = DrawingHelper.ToLineStyleInteger(circle.style)
trigger.action.circleToAll(-1, id, vec3, circle.radius, colorString, fillColor, style, true) trigger.action.circleToAll(-1, id, vec3, circle.radius, colorString, fillColor, style, true)
end end
---@param oval Oval ---@param oval Oval
---@param logger Logger? ---@return integer?, Array<CustomDrawing>?
local function DrawOval(oval, logger) local function DrawOval(oval)
---@type Array<Vec3> ---@type Array<Vec3>
local points = {} local points = {}
local pointsNo = 30 local pointsNo = 30
local angleStep = (2 * math.pi) / pointsNo local angleStep = (2 * math.pi) / pointsNo
local fillColor = DrawingHelper.ColorToColorTable(oval.fillColorString) local fillColor = DrawingHelper.ColorToColorTable(oval.fillColorString)
local color = DrawingHelper.ColorToColorTable(oval.colorString) local color = DrawingHelper.ColorToColorTable(oval.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(oval.style) local lineStyle = DrawingHelper.ToLineStyleInteger(oval.style)
@@ -134,12 +117,11 @@ function DrawingHelper.DrawPolygon(object, id, logger)
local y = oval.mapY + (oval.r2 * math.sin(angle)) local y = oval.mapY + (oval.r2 * math.sin(angle))
table.insert(points, { x = x, y = 0, z = y } ) table.insert(points, { x = x, y = 0, z = y } )
end end
MarkupToAll(7, id, points, fillColor, color, lineStyle, oval.thickness, logger) MarkupToAll(7, id, points, fillColor, color, lineStyle, oval.thickness)
end end
---@param free Free ---@param free Free
---@param logger Logger? local function DrawFree(free)
local function DrawFree(free, logger)
local fillColor = DrawingHelper.ColorToColorTable(free.fillColorString) local fillColor = DrawingHelper.ColorToColorTable(free.fillColorString)
local color = DrawingHelper.ColorToColorTable(free.colorString) local color = DrawingHelper.ColorToColorTable(free.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(free.style) local lineStyle = DrawingHelper.ToLineStyleInteger(free.style)
@@ -163,7 +145,7 @@ function DrawingHelper.DrawPolygon(object, id, logger)
end end
end end
MarkupToAll(7, id, points, fillColor, color, lineStyle, free.thickness, logger) MarkupToAll(7, id, points, fillColor, color, lineStyle, free.thickness)
end end
---@param rect Rect ---@param rect Rect
@@ -175,19 +157,16 @@ function DrawingHelper.DrawPolygon(object, id, logger)
local pointA = { x = rect.mapX, y = 0, z = rect.mapY } local pointA = { x = rect.mapX, y = 0, z = rect.mapY }
local pointB = { x = rect.mapX + rect.width, y = 0, z = rect.mapY + rect.height } local pointB = { x = rect.mapX + rect.width, y = 0, z = rect.mapY + rect.height }
trigger.action.rectToAll(-1, id, pointA, pointB, color, fillColor, lineStyle, true) trigger.action.rectToAll(-1, id, pointA, pointB, color, fillColor, lineStyle, true)
end end
---@param arrow Arrow ---@param arrow Arrow
---@param logger Logger? local function DrawArrow(arrow)
local function DrawArrow(arrow, logger)
local fillColor = DrawingHelper.ColorToColorTable(arrow.fillColorString) local fillColor = DrawingHelper.ColorToColorTable(arrow.fillColorString)
local color = DrawingHelper.ColorToColorTable(arrow.colorString) local color = DrawingHelper.ColorToColorTable(arrow.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(arrow.style) local lineStyle = DrawingHelper.ToLineStyleInteger(arrow.style)
if logger then logger:debug("Drawing arrow with start point: " .. tostring(arrow.mapX) .. ", " .. tostring(arrow.mapY) .. " and angle: " .. tostring(arrow.angle) .. " and length: " .. tostring(arrow.length))
logger:debug("Drawing arrow with start point: " .. tostring(arrow.mapX) .. ", " .. tostring(arrow.mapY) .. " and angle: " .. tostring(arrow.angle) .. " and length: " .. tostring(arrow.length))
end
local endPoint = { x = arrow.mapX, y = 0, z = arrow.mapY } local endPoint = { x = arrow.mapX, y = 0, z = arrow.mapY }
local rad = math.rad(arrow.angle or 0) local rad = math.rad(arrow.angle or 0)
@@ -203,44 +182,56 @@ function DrawingHelper.DrawPolygon(object, id, logger)
if object.polygonMode == "circle" then if object.polygonMode == "circle" then
DrawCircle(object--[[@as Circle]]) DrawCircle(object--[[@as Circle]])
elseif object.polygonMode == "oval" then elseif object.polygonMode == "oval" then
DrawOval(object--[[@as Oval]], logger) DrawOval(object--[[@as Oval]])
elseif object.polygonMode == "free" then elseif object.polygonMode == "free" then
DrawFree(object--[[@as Free]], logger) DrawFree(object--[[@as Free]])
elseif object.polygonMode == "rect" then elseif object.polygonMode == "rect" then
DrawRect(object--[[@as Rect]]) DrawRect(object--[[@as Rect]])
elseif object.polygonMode == "arrow" then elseif object.polygonMode == "arrow" then
DrawArrow(object--[[@as Arrow]], logger) DrawArrow(object--[[@as Arrow]])
end end
return {id}
end end
---@private ---@private
---@param object Line ---@param object Line
---@param id integer ---@return Array<integer>
---@param logger Logger? function DrawingHelper.DrawLine(object)
function DrawingHelper.DrawLine(object, id, logger)
---@type Array<Vec3> ---@type Array<Vec3>
local points = {} local points = {}
local ids = {}
for _, point in ipairs(object.points) do for _, point in ipairs(object.points) do
table.insert(points, { x = point.x, y = 0, z = point.y } ) table.insert(points, { x = object.mapX + point.x, y = 0, z = object.mapY + point.y } )
end end
local color = DrawingHelper.ColorToColorTable(object.colorString) local color = DrawingHelper.ColorToColorTable(object.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(object.style) local lineStyle = DrawingHelper.ToLineStyleInteger(object.style)
MarkupToAll(1, id, points, color, color, lineStyle, object.thickness, logger)
for i = 1, #points - 1 do
local id = DrawingHelper.GetAndAddId()
trigger.action.lineToAll(-1, id, points[i], points[i + 1], color, lineStyle, true)
table.insert(ids, id)
end
return ids
end end
---@private ---@private
---@param object TextBox ---@param object TextBox
---@param id integer ---@return Array<integer>
function DrawingHelper.DrawTextBox(object, id) function DrawingHelper.DrawTextBox(object)
local id = DrawingHelper.GetAndAddId()
trigger.action.textToAll(-1, id, { x= object.mapX, y = 0, z = object.mapY }, trigger.action.textToAll(-1, id, { x= object.mapX, y = 0, z = object.mapY },
DrawingHelper.ColorToColorTable(object.colorString), DrawingHelper.ColorToColorTable(object.colorString),
DrawingHelper.ColorToColorTable(object.fillColorString), DrawingHelper.ColorToColorTable(object.fillColorString),
object.fontSize or 12, object.fontSize or 12,
true, true,
object.text or "") object.text or "")
return {id}
end end
function DrawingHelper.Remove(id) function DrawingHelper.Remove(id)
@@ -252,7 +243,7 @@ end
function DrawingHelper.ColorToColorTable(hexStr) function DrawingHelper.ColorToColorTable(hexStr)
if hexStr == nil then if hexStr == nil then
drawingLogger:warn("ColorToColorTable called with nil hexStr, returning default color {0, 0, 0, 0}") logger:warn("ColorToColorTable called with nil hexStr, returning default color {0, 0, 0, 0}")
return { 0, 0, 0, 0 } return { 0, 0, 0, 0 }
end end
@@ -266,14 +257,14 @@ function DrawingHelper.ColorToColorTable(hexStr)
return { r, g , b , a } return { r, g , b , a }
end end
---@param rgba table ---@param rgba table { r, g, b, a }
---@return string ---@return string
function DrawingHelper.ColorTableToColorString(rgba) function DrawingHelper.ColorTableToColorString(rgba)
if rgba == nil or #rgba < 4 or rgba[1] == nil or rgba[2] == nil or rgba[3] == nil or rgba[4] == nil then if rgba == nil or #rgba < 4 or rgba[1] == nil or rgba[2] == nil or rgba[3] == nil or rgba[4] == nil then
drawingLogger:warn("ColorTableToColorString called with invalid rgba table, returning default color string '0x00000000'") logger:warn("ColorTableToColorString called with invalid rgba table, returning default color string '0x00000000'")
return "0x00000000" return "0x00000000"
end end
local r = string.format("%02X", math.floor(rgba[1] * 255)) local r = string.format("%02X", math.floor(rgba[1] * 255))
local g = string.format("%02X", math.floor(rgba[2] * 255)) local g = string.format("%02X", math.floor(rgba[2] * 255))
local b = string.format("%02X", math.floor(rgba[3] * 255)) local b = string.format("%02X", math.floor(rgba[3] * 255))
@@ -293,11 +284,11 @@ function DrawingHelper.ToLineStyleInteger(lineStyle)
return 2 return 2
elseif lineStyle == "dotted" then elseif lineStyle == "dotted" then
return 3 return 3
elseif lineStyle == "dot dash" then elseif lineStyle == "dotdash" then
return 4 return 4
elseif lineStyle == "long dash" then elseif lineStyle == "longdash" then
return 5 return 5
elseif lineStyle == "two dash" then elseif lineStyle == "twodash" then
return 6 return 6
else else
return 0 return 0
@@ -314,11 +305,11 @@ function DrawingHelper.ToLineStyleString(lineStyle)
elseif lineStyle == 3 then elseif lineStyle == 3 then
return "dotted" return "dotted"
elseif lineStyle == 4 then elseif lineStyle == 4 then
return "dot dash" return "dotdash"
elseif lineStyle == 5 then elseif lineStyle == 5 then
return "long dash" return "longdash"
elseif lineStyle == 6 then elseif lineStyle == 6 then
return "two dash" return "twodash"
else else
return "no line" return "no line"
end end
@@ -1,6 +1,5 @@
local Logger = require("classes.util.Logger") local Logger = require("classes.util.Logger")
local Util = require("classes.util.Util") local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
---@class BattleManager ---@class BattleManager
---@field private _name string ---@field private _name string
@@ -13,8 +12,6 @@ local DcsUtil = require("classes.util.DcsUtil")
local BattleManager = {} local BattleManager = {}
BattleManager.__index = BattleManager BattleManager.__index = BattleManager
local debugDrawing = false
---@param redGroups Array<SpearheadGroup> ---@param redGroups Array<SpearheadGroup>
---@param blueGroups Array<SpearheadGroup> ---@param blueGroups Array<SpearheadGroup>
---@param name string ---@param name string
@@ -30,8 +27,8 @@ function BattleManager.New(redGroups, blueGroups, name, logLevel)
self._redGroups = redGroups self._redGroups = redGroups
self._blueGroups = blueGroups self._blueGroups = blueGroups
self._logger:debug("BattleManager created with name: " .. self._name self._logger:debug("BattleManager created with name: " .. self._name
.. ", red groups: " .. #self._redGroups .. ", red groups: " .. #self._redGroups
.. ", blue groups: " .. #self._blueGroups) .. ", blue groups: " .. #self._blueGroups)
return self return self
@@ -90,8 +87,6 @@ function BattleManager:Update()
self._logger:debug("BattleManager Update called for " .. self._name) self._logger:debug("BattleManager Update called for " .. self._name)
local shootChance = 1 -- Adjust this value to control the shooting probability (0.0 to 1.0)
self:LetUnitsShoot(self._redGroups, self._blueGroups) self:LetUnitsShoot(self._redGroups, self._blueGroups)
self:LetUnitsShoot(self._blueGroups, self._redGroups) self:LetUnitsShoot(self._blueGroups, self._redGroups)
@@ -136,7 +131,7 @@ function BattleManager:LetUnitsShoot(groups, targetGroups)
expendQtyEnabled = true expendQtyEnabled = true
} }
} }
local controller = unit:getController() local controller = unit:getController()
if controller then if controller then
controller:setTask(shootTask) controller:setTask(shootTask)
@@ -146,7 +141,7 @@ function BattleManager:LetUnitsShoot(groups, targetGroups)
end end
end end
end end
end end
---@param unit Unit ---@param unit Unit
---@return number ---@return number
@@ -196,7 +191,7 @@ function BattleManager:IsUnitApplicable(unit)
end end
return true return true
end end
---@private ---@private
@@ -236,7 +231,7 @@ end
---@param groupHulls Array<Array<Vec2>> ---@param groupHulls Array<Array<Vec2>>
---@return Vec2? ---@return Vec2?
function BattleManager:GetRandomPoint(origin, groupHulls) function BattleManager:GetRandomPoint(origin, groupHulls)
local hull = Util.randomFromList(groupHulls) --[[@as Array<Vec2>]] local hull = Util.randomFromList(groupHulls) --[[@as Array<Vec2>]]
if not hull then return nil end if not hull then return nil end
local shootPoints = Util.GetTangentHullPointsFromOrigin(hull, origin) local shootPoints = Util.GetTangentHullPointsFromOrigin(hull, origin)
@@ -31,8 +31,6 @@ local function sortMissions(list, groupPos)
end) end)
end end
local id = 0
local instance = nil local instance = nil
---@return MissionCommandsHelper ---@return MissionCommandsHelper
@@ -57,16 +55,16 @@ function MissionCommandsHelper.getOrCreate()
instance._supplyUnitsTracker:AddOnSupplyUnitEventListener( instance._supplyUnitsTracker:AddOnSupplyUnitEventListener(
{ {
enteredSupplyHub = function(self, unit) enteredSupplyHub = function(_, unit)
if unit == nil then return end if unit == nil then return end
instance.updateNeeded = true instance.updateNeeded = true
instance:updateCommandsForGroup(unit:getGroup():getID()) instance:updateCommandsForGroup(unit:getGroup():getID())
end, end,
exitedSupplyHub = function(self, unit) exitedSupplyHub = function(_, unit)
instance.updateNeeded = true instance.updateNeeded = true
instance:updateCommandsForGroup(unit:getGroup():getID()) instance:updateCommandsForGroup(unit:getGroup():getID())
end, end,
supplyUnitSpawned = function(self, unit) supplyUnitSpawned = function(_, unit)
instance.updateNeeded = true instance.updateNeeded = true
instance:updateCommandsForGroup(unit:getGroup():getID()) instance:updateCommandsForGroup(unit:getGroup():getID())
end end
@@ -509,7 +507,6 @@ function MissionCommandsHelper:AddCargoCommands(groupID)
local unloadCargoCommand = function(params) local unloadCargoCommand = function(params)
local unitID = params.unitID local unitID = params.unitID
local crateType = params.crateType local crateType = params.crateType
local supplyUnitsTracker = params.supplyUnitsTracker
params.supplyUnitsTracker:UnloadRequested(unitID, crateType, params.commandHelper) params.supplyUnitsTracker:UnloadRequested(unitID, crateType, params.commandHelper)
end end
@@ -518,7 +515,7 @@ function MissionCommandsHelper:AddCargoCommands(groupID)
for cargoType, amount in pairs(cargo) do for cargoType, amount in pairs(cargo) do
local cargoConfig = SupplyConfigHelper.getSupplyConfig(cargoType) local cargoConfig = SupplyConfigHelper.getSupplyConfig(cargoType)
if cargoConfig then if cargoConfig then
for i = 1, amount do for _ = 1, amount do
local path = { [1] = folderNames.cargo } local path = { [1] = folderNames.cargo }
---@type UnloadCargoCommandParams ---@type UnloadCargoCommandParams
local params = { unitID = unit:getID(), crateType = cargoType, supplyUnitsTracker = self local params = { unitID = unit:getID(), crateType = cargoType, supplyUnitsTracker = self
@@ -29,7 +29,7 @@ local SupplyLoadConfig = {
{ centerAngle = 270, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }, { centerAngle = 270, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
{ centerAngle = 90, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }, { centerAngle = 90, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
} }
}, },
["UH-1H"] = { ["UH-1H"] = {
maxInternalLoad = 2000, maxInternalLoad = 2000,
dropZones = { dropZones = {
@@ -185,7 +185,7 @@ function SupplyUnitsTracker:RemoveCargoFromUnit(unitID, crateType)
self._cargoInUnits[unitIDStr][crateType] = self._cargoInUnits[unitIDStr][crateType] - 1 self._cargoInUnits[unitIDStr][crateType] = self._cargoInUnits[unitIDStr][crateType] - 1
local hasCargo = false local hasCargo = false
for type, count in pairs(self._cargoInUnits[unitIDStr]) do for _, count in pairs(self._cargoInUnits[unitIDStr]) do
if count > 0 then if count > 0 then
hasCargo = true hasCargo = true
break break
@@ -216,13 +216,11 @@ end
function SupplyUnitsTracker:CheckUnitsInZones() function SupplyUnitsTracker:CheckUnitsInZones()
for name, unit in pairs(self._supplyUnitsByName) do for _, unit in pairs(self._supplyUnitsByName) do
if unit ~= nil and unit:isExist() == true then if unit ~= nil and unit:isExist() == true then
self._logger:debug("Checking unit: " .. unit:getName()) self._logger:debug("Checking unit: " .. unit:getName())
local pos = unit:getPoint() local pos = unit:getPoint()
local group = unit:getGroup()
for hub, enabled in pairs(self._registeredHubs) do for hub, enabled in pairs(self._registeredHubs) do
if enabled == true then if enabled == true then
local zone = hub:GetZone() local zone = hub:GetZone()
@@ -238,7 +236,7 @@ function SupplyUnitsTracker:CheckUnitsInZones()
end) end)
end end
end end
else else
if self._unitInSupplyHub[tostring(unit:getID())] == true then if self._unitInSupplyHub[tostring(unit:getID())] == true then
self._unitInSupplyHub[tostring(unit:getID())] = false self._unitInSupplyHub[tostring(unit:getID())] = false
@@ -292,11 +290,11 @@ local cargoCount = 0
---@param crateType CrateType ---@param crateType CrateType
---@param missionCommandsHelper MissionCommandsHelper ---@param missionCommandsHelper MissionCommandsHelper
function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHelper) function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHelper)
self._logger:debug("Unload requested for unit: " .. unitID .. " crateType: " .. crateType) self._logger:debug("Unload requested for unit: " .. unitID .. " crateType: " .. crateType)
local unit = DcsUtil.GetPlayerUnitByID(unitID) local unit = DcsUtil.GetPlayerUnitByID(unitID)
if unit == nil or unit:isExist() == false then if unit == nil or unit:isExist() == false then
self._logger:warn("Unload requested for non-existent unit: " .. unitID) self._logger:warn("Unload requested for non-existent unit: " .. unitID)
return return
end end
@@ -305,7 +303,7 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe
self._logger:warn("Unload requested for unit with no group: " .. unit:getName()) self._logger:warn("Unload requested for unit with no group: " .. unit:getName())
return return
end end
local cargoConfig = SupplyConfigHelper.getSupplyConfig(crateType) local cargoConfig = SupplyConfigHelper.getSupplyConfig(crateType)
if cargoConfig == nil then if cargoConfig == nil then
@@ -362,11 +360,11 @@ function SupplyUnitsTracker:UnitRequestCrateLoading(groupID, crateType, missionC
end end
local unit = group:getUnit(1) local unit = group:getUnit(1)
if unit == nil then return end if unit == nil then return end
if unit:isExist() == false then return end if unit:isExist() == false then return end
if unit:inAir() == true then if unit:inAir() == true then
trigger.action.outTextForUnit(unit:getID(), "Land first before crates can be loaded", 10) trigger.action.outTextForUnit(unit:getID(), "Land first before crates can be loaded", 10)
return return
@@ -383,7 +381,7 @@ function SupplyUnitsTracker:UnitRequestCrateLoading(groupID, crateType, missionC
---@param params LoadCargoParams ---@param params LoadCargoParams
local LoadCrateTask = function(params) local LoadCrateTask = function(params)
local loaded = params.self:TryLoadCrateInUnit(params.unit, params.crateType, params.commandHelper) local loaded = params.self:TryLoadCrateInUnit(params.unit, params.crateType, params.commandHelper)
if loaded ~= false then if loaded ~= false then
trigger.action.outTextForUnit(unit:getID(), "Loaded crate :" .. params.crateType, 10) trigger.action.outTextForUnit(unit:getID(), "Loaded crate :" .. params.crateType, 10)
@@ -410,7 +408,7 @@ end
---@param commandHelper MissionCommandsHelper ---@param commandHelper MissionCommandsHelper
---@return boolean ---@return boolean
function SupplyUnitsTracker:TryLoadCrateInUnit(unit, crateType, commandHelper) function SupplyUnitsTracker:TryLoadCrateInUnit(unit, crateType, commandHelper)
local crateConfigA = SupplyConfigHelper.getSupplyConfig(crateType) local crateConfigA = SupplyConfigHelper.getSupplyConfig(crateType)
if crateConfigA == nil then if crateConfigA == nil then
trigger.action.outTextForUnit(unit:getID(), "Invalid crate type: " .. crateType, 5) trigger.action.outTextForUnit(unit:getID(), "Invalid crate type: " .. crateType, 5)
@@ -444,7 +442,7 @@ function SupplyUnitsTracker:TryLoadCrateInUnit(unit, crateType, commandHelper)
if group == nil then return false end if group == nil then return false end
local groupID = group:getID() local groupID = group:getID()
commandHelper:updateCommandsForGroup(groupID) commandHelper:updateCommandsForGroup(groupID)
return true return true
end end
@@ -462,7 +460,7 @@ function SupplyUnitsTracker:UnitRequestCrateSpawn(groupID, crateType)
return return
end end
end end
end end
@@ -484,11 +482,11 @@ function SupplyUnitsTracker:GetBoundingBoxes(foundObject)
if desc == nil or desc.box == nil then if desc == nil or desc.box == nil then
return nil return nil
end end
local objPos = foundObject:getPoint() local objPos = foundObject:getPoint()
local box = desc.box local box = desc.box
local heading = 0 local heading = 0
-- Try to get object heading from position vector's forward direction -- Try to get object heading from position vector's forward direction
-- This works for units and other objects that support getPosition -- This works for units and other objects that support getPosition
pcall(function() pcall(function()
@@ -497,13 +495,13 @@ function SupplyUnitsTracker:GetBoundingBoxes(foundObject)
heading = math.atan2(objPosition.x.z, objPosition.x.x) heading = math.atan2(objPosition.x.z, objPosition.x.x)
end end
end) end)
-- For rotated objects, we need to rotate the bounding box -- For rotated objects, we need to rotate the bounding box
local minX = box.min.x local minX = box.min.x
local maxX = box.max.x local maxX = box.max.x
local minZ = box.min.z local minZ = box.min.z
local maxZ = box.max.z local maxZ = box.max.z
-- If object has significant rotation, apply rotation to bbox corners -- If object has significant rotation, apply rotation to bbox corners
if math.abs(heading) > 0.1 then if math.abs(heading) > 0.1 then
-- Get all 4 corners of bbox in local space -- Get all 4 corners of bbox in local space
@@ -513,11 +511,11 @@ function SupplyUnitsTracker:GetBoundingBoxes(foundObject)
{maxX, minZ}, {maxX, minZ},
{maxX, maxZ} {maxX, maxZ}
} }
-- Rotate corners and find new min/max -- Rotate corners and find new min/max
minX, maxX = math.huge, -math.huge minX, maxX = math.huge, -math.huge
minZ, maxZ = math.huge, -math.huge minZ, maxZ = math.huge, -math.huge
for _, corner in ipairs(corners) do for _, corner in ipairs(corners) do
local rotX = corner[1] * math.cos(heading) - corner[2] * math.sin(heading) local rotX = corner[1] * math.cos(heading) - corner[2] * math.sin(heading)
local rotZ = corner[1] * math.sin(heading) + corner[2] * math.cos(heading) local rotZ = corner[1] * math.sin(heading) + corner[2] * math.cos(heading)
@@ -527,7 +525,7 @@ function SupplyUnitsTracker:GetBoundingBoxes(foundObject)
maxZ = math.max(maxZ, rotZ) maxZ = math.max(maxZ, rotZ)
end end
end end
-- Convert relative bbox to world space by adding object position -- Convert relative bbox to world space by adding object position
---@type SupplyUnitsBoundingBox ---@type SupplyUnitsBoundingBox
return { return {
@@ -551,7 +549,7 @@ end
---@param safetyMargin number Safety margin around objects ---@param safetyMargin number Safety margin around objects
---@return boolean True if collision detected ---@return boolean True if collision detected
function SupplyUnitsTracker:CheckBBoxCollision(crateBBox, objBBox, safetyMargin) function SupplyUnitsTracker:CheckBBoxCollision(crateBBox, objBBox, safetyMargin)
-- Apply safety margin to object bbox -- Apply safety margin to object bbox
local objMin = { local objMin = {
x = objBBox.min.x - safetyMargin, x = objBBox.min.x - safetyMargin,
@@ -563,7 +561,7 @@ function SupplyUnitsTracker:CheckBBoxCollision(crateBBox, objBBox, safetyMargin)
y = objBBox.max.y + safetyMargin, y = objBBox.max.y + safetyMargin,
z = objBBox.max.z + safetyMargin z = objBBox.max.z + safetyMargin
} }
-- AABB collision detection -- AABB collision detection
return crateBBox.min.x <= objMax.x and crateBBox.max.x >= objMin.x and return crateBBox.min.x <= objMax.x and crateBBox.max.x >= objMin.x and
crateBBox.min.y <= objMax.y and crateBBox.max.y >= objMin.y and crateBBox.min.y <= objMax.y and crateBBox.max.y >= objMin.y and
@@ -577,7 +575,7 @@ end
function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName) function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName)
local unitPos = unit:getPosition() local unitPos = unit:getPosition()
-- Get unit's heading from the forward vector (x component) -- Get unit's heading from the forward vector (x component)
-- Heading is calculated as: atan2(forward.z, forward.x) -- Heading is calculated as: atan2(forward.z, forward.x)
local unitHeading = math.atan2(unitPos.x.z, unitPos.x.x) local unitHeading = math.atan2(unitPos.x.z, unitPos.x.x)
@@ -588,14 +586,14 @@ function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName)
self._logger:error("Could not get bbox for crate type: " .. crateTypeName) self._logger:error("Could not get bbox for crate type: " .. crateTypeName)
return nil return nil
end end
local crateRelativeBBox = crateDesc.box local crateRelativeBBox = crateDesc.box
-- Get drop zone config for this unit -- Get drop zone config for this unit
local dropZones = { local dropZones = {
{ centerAngle = 180, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 } { centerAngle = 180, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }
} }
if SupplyLoadConfig[unit:getTypeName()] ~= nil then if SupplyLoadConfig[unit:getTypeName()] ~= nil then
dropZones = SupplyLoadConfig[unit:getTypeName()].dropZones dropZones = SupplyLoadConfig[unit:getTypeName()].dropZones
end end
@@ -614,7 +612,7 @@ function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName)
} }
local occupiedObjects = {} local occupiedObjects = {}
local found = function(foundItem, val) local found = function(foundItem, _)
local bbox = self:GetBoundingBoxes(foundItem) local bbox = self:GetBoundingBoxes(foundItem)
if bbox then if bbox then
self._logger:debug("Found object: " .. foundItem:getTypeName() .. " at (" .. foundItem:getPoint().x .. ", " .. foundItem:getPoint().z .. ")") self._logger:debug("Found object: " .. foundItem:getTypeName() .. " at (" .. foundItem:getPoint().x .. ", " .. foundItem:getPoint().z .. ")")
@@ -648,15 +646,15 @@ function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName)
for distance = zone.minRadius, zone.maxRadius, zone.spacing do for distance = zone.minRadius, zone.maxRadius, zone.spacing do
-- Check multiple positions within the angular slice -- Check multiple positions within the angular slice
local angleStep = math.min(15, zone.angleWidth / 3) -- Divide slice into sections local angleStep = math.min(15, zone.angleWidth / 3) -- Divide slice into sections
for angle = minAngle, maxAngle, angleStep do for angle = minAngle, maxAngle, angleStep do
local radians = math.rad(angle) local radians = math.rad(angle)
-- Calculate position at this angle and distance, relative to unit's heading -- Calculate position at this angle and distance, relative to unit's heading
-- Angle 0 = forward, 90 = right, 180 = rear, 270 = left -- Angle 0 = forward, 90 = right, 180 = rear, 270 = left
-- Apply unit heading to make angles relative to unit orientation -- Apply unit heading to make angles relative to unit orientation
local worldAngle = radians + unitHeading local worldAngle = radians + unitHeading
local candidateX = unitPos.p.x + distance * math.sin(worldAngle) local candidateX = unitPos.p.x + distance * math.sin(worldAngle)
local candidateZ = unitPos.p.z + distance * math.cos(worldAngle) local candidateZ = unitPos.p.z + distance * math.cos(worldAngle)
local candidateY = land.getHeight({ x = candidateX, y = candidateZ }) local candidateY = land.getHeight({ x = candidateX, y = candidateZ })
@@ -21,9 +21,18 @@ local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelpe
---@field private _dropOffZone SpearheadTriggerZone? ---@field private _dropOffZone SpearheadTriggerZone?
---@field private _noLandingZoneDrawing CustomDrawing? ---@field private _noLandingZoneDrawing CustomDrawing?
---@field private _dropOffZoneDrawing CustomDrawing? ---@field private _dropOffZoneDrawing CustomDrawing?
---@field private _briefing string?
local BuildableMission = {} local BuildableMission = {}
BuildableMission.__index = BuildableMission BuildableMission.__index = BuildableMission
---@return string
local function getDefaultBriefing(siteType, coords)
return "We've dispatched forward units to find a proper spot for a new " .. siteType .. "." ..
"\nYou will need to drop off supplies so they can start building." ..
"\nThe coords are: " .. coords ..
"\n\n"
end
---@class OnCrateDroppedListener ---@class OnCrateDroppedListener
---@field OnCrateDroppedOff fun(self:OnCrateDroppedListener, mission:BuildableMission, kilos:number) ---@field OnCrateDroppedOff fun(self:OnCrateDroppedListener, mission:BuildableMission, kilos:number)
@@ -33,16 +42,18 @@ BuildableMission.__index = BuildableMission
---@param requiredCrateType SupplyType ---@param requiredCrateType SupplyType
---@param noLandingZone SpearheadTriggerZone? ---@param noLandingZone SpearheadTriggerZone?
---@param logger Logger ---@param logger Logger
function BuildableMission.new(database, logger, targetZone, noLandingZone, requiredKilos, requiredCrateType) ---@param briefing string?
function BuildableMission.new(database, logger, targetZone, noLandingZone, requiredKilos, requiredCrateType, briefing)
setmetatable(BuildableMission, Mission) setmetatable(BuildableMission, Mission)
local self = setmetatable({}, { __index = BuildableMission }) local self = setmetatable({}, { __index = BuildableMission })
self._targetZone = targetZone self._targetZone = targetZone
self._database = database self._database = database
self._requiredKilos = requiredKilos self._requiredKilos = requiredKilos
self._droppedKilos = 0 self._droppedKilos = 0
self._briefing = briefing
self._noLandingZone = noLandingZone self._noLandingZone = noLandingZone
@@ -64,13 +75,12 @@ function BuildableMission.new(database, logger, targetZone, noLandingZone, requi
end end
self.code = tostring(database:GetNewMissionCode()) self.code = tostring(database:GetNewMissionCode())
self.name = "Resupply"
local type = "site" local splitTargetZoneName = Util.split_string(targetZone.name, "_")
if requiredCrateType == "SAM_CRATE" then if splitTargetZoneName and splitTargetZoneName[3] and splitTargetZoneName[3] ~= "" then
type = "SAM site" self.name = splitTargetZoneName[3]
elseif requiredCrateType == "FARP_CRATE" then else
type = "FARP" self.name = "Resupply"
end end
self.zoneName = targetZone.name .. "_supply" self.zoneName = targetZone.name .. "_supply"
@@ -81,7 +91,6 @@ function BuildableMission.new(database, logger, targetZone, noLandingZone, requi
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate() self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate()
self._state = "NEW" self._state = "NEW"
self.location = targetZone.location self.location = targetZone.location
self.missionType = "LOGISTICS" self.missionType = "LOGISTICS"
@@ -116,11 +125,16 @@ function BuildableMission:ShowBriefing(groupID)
siteType = "airbase" siteType = "airbase"
end end
local briefing = "Mission [" .. self.code .. "] " .. self.name .. local briefingPart = self._briefing
if briefingPart then
briefingPart = Util.replaceString(briefingPart, "{{coords}}", coords)
else
briefingPart = getDefaultBriefing(siteType, coords)
end
local briefing = "Mission [" .. self.code .. "] " .. self.name ..
"\n \n" .. "\n \n" ..
"We've dispatched forward units to find a proper spot for a new " .. siteType .. "." .. briefingPart ..
"\nYou will need to drop off supplies so they can start building." ..
"\nThe coords are: " .. coords ..
"\n\n" .. "\n\n" ..
"\nKilos still required: " .. self._requiredKilos - self._droppedKilos .. "\nKilos still required: " .. self._requiredKilos - self._droppedKilos ..
"\n\n" .. "\n\n" ..
@@ -180,7 +194,7 @@ function BuildableMission:SpawnActive()
local fillColor2 = DrawingHelper.ColorTableToColorString({ 0, 0, 1, 0}) local fillColor2 = DrawingHelper.ColorTableToColorString({ 0, 0, 1, 0})
self._dropOffZoneDrawing = CustomDrawing.FromZone(self._dropOffZone, lineColor2, fillColor2, 2, 6) self._dropOffZoneDrawing = CustomDrawing.FromZone(self._dropOffZone, lineColor2, fillColor2, 2, 6)
self._dropOffZoneDrawing:Draw() self._dropOffZoneDrawing:Draw()
---@param selfA BuildableMission ---@param selfA BuildableMission
---@param time number ---@param time number
local checkForCrateTasks = function (selfA, time) local checkForCrateTasks = function (selfA, time)
@@ -245,8 +259,8 @@ function BuildableMission:CheckCratesInZone()
end end
end end
end end
for _, foundCrate in pairs(foundCrates) do for _, foundCrate in pairs(foundCrates) do
local crateConfig = SupplyConfigHelper.fromObjectName(foundCrate:getName()) local crateConfig = SupplyConfigHelper.fromObjectName(foundCrate:getName())
if crateConfig then if crateConfig then
self._droppedKilos = self._droppedKilos + crateConfig.weight self._droppedKilos = self._droppedKilos + crateConfig.weight
@@ -261,7 +275,7 @@ function BuildableMission:CheckCratesInZone()
self:NotifyMissionComplete() self:NotifyMissionComplete()
self._state = "COMPLETED" self._state = "COMPLETED"
end end
if self._state == "COMPLETED" then if self._state == "COMPLETED" then
for groupID, markID in pairs(self._markIDsPerGroup) do for groupID, markID in pairs(self._markIDsPerGroup) do
if markID then if markID then
@@ -18,7 +18,7 @@ local RunwayStrikeMission = {}
---@field corners Array<Vec2> ---@field corners Array<Vec2>
---@field kilosHit number ---@field kilosHit number
---@field repairGroups Array<string> ---@field repairGroups Array<string>
---@field drawID number? ---@field drawID Array<number>
---@param runway Runway ---@param runway Runway
@@ -42,7 +42,7 @@ function RunwayStrikeMission.new(runway, airbaseName, database, logger, runwayBo
self._runwayZone = self:RunwayToSpearheadZone(runway) self._runwayZone = self:RunwayToSpearheadZone(runway)
self._repairInProgress = false self._repairInProgress = false
self._minKilosForDamage = 100 self._minKilosForDamage = 100
local sections = self:ToSections(runway, 5) local sections = self:ToSections(runway, 5)
--[[ --[[
+-----------+-----------+-----------+-----------+-----------+ +-----------+-----------+-----------+-----------+-----------+
@@ -84,7 +84,6 @@ function RunwayStrikeMission:RunwayHit(impactPoint, explosiveMass)
self._logger:debug("Runway hit: " .. self._airportName .. ":" .. self._runway.Name) self._logger:debug("Runway hit: " .. self._airportName .. ":" .. self._runway.Name)
for _, section in pairs(self._runwaySections) do for _, section in pairs(self._runwaySections) do
local zone = self:SectionToSpearheadZone(section) local zone = self:SectionToSpearheadZone(section)
if Util.is3dPointInZone({ x = impactPoint.x, z = impactPoint.y, y = 0 }, zone) then if Util.is3dPointInZone({ x = impactPoint.x, z = impactPoint.y, y = 0 }, zone) then
if section.kilosHit == nil then if section.kilosHit == nil then
@@ -96,7 +95,7 @@ function RunwayStrikeMission:RunwayHit(impactPoint, explosiveMass)
end end
---@param selfA RunwayStrikeMission ---@param selfA RunwayStrikeMission
local updateState = function(selfA, time) local updateState = function(selfA, _)
selfA:UpdateState() selfA:UpdateState()
end end
@@ -154,7 +153,7 @@ function RunwayStrikeMission:Draw()
if runwaySection.drawID == nil then if runwaySection.drawID == nil then
local zone = self:SectionToSpearheadZone(runwaySection) local zone = self:SectionToSpearheadZone(runwaySection)
---@type Free ---@type Free
local drawObject = { local drawObject = {
primitiveType = "Polygon", primitiveType = "Polygon",
@@ -172,8 +171,10 @@ function RunwayStrikeMission:Draw()
runwaySection.drawID = DrawingHelper.Draw(drawObject) runwaySection.drawID = DrawingHelper.Draw(drawObject)
else else
DcsUtil.SetFillColor(runwaySection.drawID, fillColor) for _, drawID in pairs(runwaySection.drawID) do
DcsUtil.SetLineColor(runwaySection.drawID, lineColor) DcsUtil.SetFillColor(drawID, fillColor)
DcsUtil.SetLineColor(drawID, lineColor)
end
end end
end end
@@ -211,7 +212,6 @@ function RunwayStrikeMission:StartRepair()
if interval == nil then return nil end if interval == nil then return nil end
return time + interval return time + interval
end end
timer.scheduleFunction(repairTask, self, timer.getTime() + 5) timer.scheduleFunction(repairTask, self, timer.getTime() + 5)
end end
@@ -497,6 +497,7 @@ function RunwayStrikeMission:ToSections(runway, numSections)
corners = corners, corners = corners,
kilosHit = 0, kilosHit = 0,
repairGroups = {}, repairGroups = {},
drawID = {},
} }
table.insert(sections, section) table.insert(sections, section)
@@ -211,7 +211,7 @@ end
---@return boolean ---@return boolean
function ZoneMission:AllDependenciesMet() function ZoneMission:AllDependenciesMet()
local allDependenciesMet = true local allDependenciesMet = true
for missionName, value in pairs(self._dependencies) do for missionName, _ in pairs(self._dependencies) do
if self._parentStage:IsMissionComplete(missionName) == false then if self._parentStage:IsMissionComplete(missionName) == false then
allDependenciesMet = false allDependenciesMet = false
self._dependencies[missionName] = false self._dependencies[missionName] = false
@@ -503,12 +503,12 @@ end
---@param unit Object ---@param unit Object
function ZoneMission:MarkLastContact(unit) function ZoneMission:MarkLastContact(unit)
if not unit then if not unit then
self._logger:error("MarkLastContact called with nil unit") self._logger:error("MarkLastContact called with nil unit")
return return
end end
local point = unit:getPoint() local point = unit:getPoint()
if not point then if not point then
self._logger:error("MarkLastContact called with unit without point") self._logger:error("MarkLastContact called with unit without point")
return return
@@ -51,7 +51,7 @@ function Mission.newSuper(self, zoneName, missionName, missionType, missionBrief
self.location = database:GetLocationForMissionZone(zoneName) self.location = database:GetLocationForMissionZone(zoneName)
self.missionTypeDisplay = self.missionType self.missionTypeDisplay = self.missionType
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate() self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
return true, "success" return true, "success"
@@ -70,6 +70,7 @@ function Mission:SpawnActive() end
---comment ---comment
---@param checkHealth boolean ---@param checkHealth boolean
---@param messageIfDone boolean ---@param messageIfDone boolean
---@diagnostic disable-next-line: unused-local
function Mission:UpdateState(checkHealth, messageIfDone) end function Mission:UpdateState(checkHealth, messageIfDone) end
function Mission:StartCheckingContinuous() end function Mission:StartCheckingContinuous() end
@@ -120,12 +121,19 @@ function Mission:NotifyMissionComplete()
end) end)
end end
local succ, err = pcall(function() local _, _ = pcall(function()
SpearheadAPI.Internal.notifyMissionComplete(self.zoneName) SpearheadAPI.Internal.notifyMissionComplete(self.zoneName)
end) end)
end end
function Mission:ForceMissionComplete()
self._state = "COMPLETED"
self:NotifyMissionComplete()
end
---@param groupId number
---@diagnostic disable-next-line: unused-local
function Mission:MarkMissionAreaToGroup(groupId) end function Mission:MarkMissionAreaToGroup(groupId) end
---endregion ---endregion
+29 -20
View File
@@ -77,7 +77,7 @@ do -- INIT DCS_UTIL
do -- INITS ALL TABLES WITH DATA THAT's from the MIZ environment do -- INITS ALL TABLES WITH DATA THAT's from the MIZ environment
do --init trigger zones do --init trigger zones
for i, trigger_zone in pairs(env.mission.triggers.zones) do for _, trigger_zone in pairs(env.mission.triggers.zones) do
-- reorder verts as they are not ordered correctly in the ME -- reorder verts as they are not ordered correctly in the ME
local verts = {} local verts = {}
if Util.tableLength(trigger_zone.verticies) >= 4 then if Util.tableLength(trigger_zone.verticies) >= 4 then
@@ -192,7 +192,7 @@ do -- INIT DCS_UTIL
BLUE = 2 BLUE = 2
} }
]] -- ]] --
local input = string.lower(input) input = string.lower(input)
if input == 'neutrals' or input == "neutral" or input == "0" then if input == 'neutrals' or input == "neutral" or input == "0" then
return DCS_UTIL.Coalition.NEUTRAL return DCS_UTIL.Coalition.NEUTRAL
end end
@@ -235,7 +235,7 @@ do -- INIT DCS_UTIL
end end
end end
for index, zone_name in pairs(zone_names) do for _, zone_name in pairs(zone_names) do
local zone = DCS_UTIL.__trigger_zones[zone_name] local zone = DCS_UTIL.__trigger_zones[zone_name]
if zone then if zone then
zones[#zones + 1] = zone zones[#zones + 1] = zone
@@ -247,7 +247,7 @@ do -- INIT DCS_UTIL
local lUnit = units[units_ind] local lUnit = units[units_ind]
local unit_pos = lUnit:getPosition().p local unit_pos = lUnit:getPosition().p
local lCat = Object.getCategory(lUnit) local lCat = Object.getCategory(lUnit)
for zone_name, zone in pairs(zones) do for _, zone in pairs(zones) do
if unit_pos and ((lCat == 1 and lUnit:isActive() == true) or lCat ~= 1) then -- it is a unit and is active or it is not a unit if unit_pos and ((lCat == 1 and lUnit:isActive() == true) or lCat ~= 1) then -- it is a unit and is active or it is not a unit
local isInZone = Util.is3dPointInZone(unit_pos, zone) local isInZone = Util.is3dPointInZone(unit_pos, zone)
if isInZone == true then if isInZone == true then
@@ -313,7 +313,7 @@ do -- INIT DCS_UTIL
function DCS_UTIL.isPositionInZones(x, z, zone_names) function DCS_UTIL.isPositionInZones(x, z, zone_names)
---@type Array<SpearheadTriggerZone> ---@type Array<SpearheadTriggerZone>
local zones = {} local zones = {}
for index, zone_name in pairs(zone_names) do for _, zone_name in pairs(zone_names) do
local zone = DCS_UTIL.__trigger_zones[zone_name] local zone = DCS_UTIL.__trigger_zones[zone_name]
if zone then if zone then
zones[#zones + 1] = zone zones[#zones + 1] = zone
@@ -321,7 +321,7 @@ do -- INIT DCS_UTIL
end end
local result_zones = {} local result_zones = {}
for zone_name, zone in pairs(zones) do for _, zone in pairs(zones) do
if Util.is3dPointInZone({ x = x, z = z, y = 0 }, zone) == true then if Util.is3dPointInZone({ x = x, z = z, y = 0 }, zone) == true then
result_zones[#result_zones + 1] = zone.name result_zones[#result_zones + 1] = zone.name
end end
@@ -414,7 +414,7 @@ do -- INIT DCS_UTIL
local height = land.getHeight(location) local height = land.getHeight(location)
local vec3 = { x = location.x, y = height, z = location.y } local vec3 = { x = location.x, y = height, z = location.y }
local unitType = string.lower(unitType or "") unitType = string.lower(unitType or "")
local conversionType = config[unitType] local conversionType = config[unitType]
if not conversionType then conversionType = "DDM" end if not conversionType then conversionType = "DDM" end
@@ -471,7 +471,7 @@ do -- INIT DCS_UTIL
lon_hemisphere, math.abs(lon_deg), lon_min_display, lon_sec_display, lon_hemisphere, math.abs(lon_deg), lon_min_display, lon_sec_display,
altitude * 3,28084) altitude * 3,28084)
end end
end end
---@param zone SpearheadTriggerZone ---@param zone SpearheadTriggerZone
@@ -574,7 +574,7 @@ do -- INIT DCS_UTIL
local units = {} local units = {}
for i = 0, 2 do for i = 0, 2 do
local players = coalition.getPlayers(i) local players = coalition.getPlayers(i)
for key, unit in pairs(players) do for _, unit in pairs(players) do
units[#units + 1] = unit units[#units + 1] = unit
end end
end end
@@ -617,8 +617,7 @@ do -- INIT DCS_UTIL
end end
function DCS_UTIL.CleanCorpse(unitName) function DCS_UTIL.CleanCorpse(unitName)
local unitName = "dead_" .. unitName unitName = "dead_" .. unitName
local object = StaticObject.getByName(unitName) local object = StaticObject.getByName(unitName)
if object then if object then
@@ -632,15 +631,21 @@ do -- INIT DCS_UTIL
---@field b number ---@field b number
---@field a number ---@field a number
local __drawID = 4210
function DCS_UTIL.GetNextDrawID()
__drawID = __drawID + 1
return __drawID
end
---@param groupID number ---@param groupID number
---@param text string ---@param text string
---@param location Vec3 ---@param location Vec3
---@return number markID ---@return number markID
function DCS_UTIL.AddMarkToGroup(groupID, text, location) function DCS_UTIL.AddMarkToGroup(groupID, text, location)
drawID = drawID + 1 local nextId = DCS_UTIL.GetNextDrawID()
trigger.action.markToGroup(drawID, text, location, groupID, true, nil) trigger.action.markToGroup(nextId, text, location, groupID, true, nil)
return drawID return nextId
end end
---comment ---comment
@@ -648,9 +653,9 @@ do -- INIT DCS_UTIL
---@param location Vec3 ---@param location Vec3
---@return integer ---@return integer
function DCS_UTIL.AddMarkToAll(text, location) function DCS_UTIL.AddMarkToAll(text, location)
drawID = drawID + 1 local nextId = DCS_UTIL.GetNextDrawID()
trigger.action.markToAll(drawID, text, location, true, nil) trigger.action.markToAll(nextId, text, location, true, nil)
return drawID return nextId
end end
---@param markId number ---@param markId number
@@ -677,6 +682,10 @@ do -- INIT DCS_UTIL
---@param drawID number ---@param drawID number
---@param fillColor DrawColor ---@param fillColor DrawColor
function DCS_UTIL.SetFillColor(drawID, fillColor) function DCS_UTIL.SetFillColor(drawID, fillColor)
if fillColor == nil then
return
end
local lineColorMapped = { local lineColorMapped = {
fillColor.r or 0, fillColor.r or 0,
fillColor.g or 0, fillColor.g or 0,
@@ -695,7 +704,7 @@ do -- INIT DCS_UTIL
---@return number? id ---@return number? id
function DCS_UTIL.GetNeutralCountry() function DCS_UTIL.GetNeutralCountry()
for name, id in pairs(country.id) do for _, id in pairs(country.id) do
if coalition.getCountryCoalition(id) == DCS_UTIL.Coalition.NEUTRAL then if coalition.getCountryCoalition(id) == DCS_UTIL.Coalition.NEUTRAL then
return id return id
end end
@@ -749,7 +758,7 @@ do -- INIT DCS_UTIL
function DCS_UTIL.GetPlayerGroupByGroupID(groupId) function DCS_UTIL.GetPlayerGroupByGroupID(groupId)
for i = 0, 2 do for i = 0, 2 do
local players = coalition.getPlayers(i) local players = coalition.getPlayers(i)
for key, unit in pairs(players) do for _, unit in pairs(players) do
if unit and unit:isExist() == true then if unit and unit:isExist() == true then
local group = unit:getGroup() local group = unit:getGroup()
if group and group:getID() == groupId then if group and group:getID() == groupId then
@@ -765,7 +774,7 @@ do -- INIT DCS_UTIL
function DCS_UTIL.GetPlayerUnitByID(unitID) function DCS_UTIL.GetPlayerUnitByID(unitID)
for i = 0, 2 do for i = 0, 2 do
local players = coalition.getPlayers(i) local players = coalition.getPlayers(i)
for key, unit in pairs(players) do for _, unit in pairs(players) do
if unit and unit:getID() == unitID then if unit and unit:getID() == unitID then
return unit return unit
end end
+17 -2
View File
@@ -128,6 +128,14 @@ do -- INIT UTIL
return str:find('^' .. findable) ~= nil return str:find('^' .. findable) ~= nil
end end
UTIL.endsWith = function(str, findable, ignoreCase)
if ignoreCase == true then
return string.lower(str):find(string.lower(findable) .. '$') ~= nil
end
return str:find(findable .. '$') ~= nil
end
---comment ---comment
---@param str string ---@param str string
---@param findable string ---@param findable string
@@ -141,7 +149,7 @@ do -- INIT UTIL
---@param findableTable table ---@param findableTable table
---@return boolean ---@return boolean
UTIL.startswithAny = function(str, findableTable) UTIL.startswithAny = function(str, findableTable)
for key, value in pairs(findableTable) do for _, value in pairs(findableTable) do
if type(value) == "string" and UTIL.startswith(str, value) then return true end if type(value) == "string" and UTIL.startswith(str, value) then return true end
end end
return false return false
@@ -229,8 +237,13 @@ do -- INIT UTIL
return ((vec1Norm.x * vec2Norm.x) + (vec1Norm.y * vec2Norm.y) + (vec1Norm.z * vec2Norm.z)) return ((vec1Norm.x * vec2Norm.x) + (vec1Norm.y * vec2Norm.y) + (vec1Norm.z * vec2Norm.z))
end end
---@param polygon Array<Vec2> of pairs { x, y }
---@param x number X location
---@param y number Y location
---@return boolean
local function isInComplexPolygon(polygon, x, y) local function isInComplexPolygon(polygon, x, y)
---@param poly Array<Vec2> of pairs { x, y }
local function getEdges(poly) local function getEdges(poly)
local result = {} local result = {}
for i = 1, #poly do for i = 1, #poly do
@@ -391,7 +404,9 @@ do -- INIT UTIL
if #points == 0 then return {} end if #points == 0 then return {} end
-- Simple clustering: group points that are within minSeparation of each other -- Simple clustering: group points that are within minSeparation of each other
---@type Array<Array<Vec2>>
local clusters = {} local clusters = {}
---@type table<integer, boolean>
local assigned = {} local assigned = {}
for i, p in ipairs(points) do for i, p in ipairs(points) do
+22 -22
View File
@@ -1,13 +1,14 @@
--Single player purpose --Single player purpose
local Logger = require("classes.util.Logger") local Logger = require("classes.util.Logger")
local DcsUtil = require("classes.util.DcsUtil")
local Database = require("classes.spearhead_db") local Database = require("classes.spearhead_db")
local SpearheadEvents = require("classes.spearhead_events") local SpearheadEvents = require("classes.spearhead_events")
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper") local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
local GlobalConfig = require("classes.configuration.GlobalConfig")
local CapConfig = require("classes.configuration.CapConfig") local CapConfig = require("classes.configuration.CapConfig")
local StageConfig = require("classes.configuration.StageConfig") local StageConfig = require("classes.configuration.StageConfig")
local Persistence = require("classes.persistence.Persistence") local Persistence = require("classes.persistence.Persistence")
local PersistenceConfig = require("classes.configuration.PersistenceConfig")
local SpawnManager = require("classes.helpers.SpawnManager") local SpawnManager = require("classes.helpers.SpawnManager")
local DetectionManager = require("classes.capClasses.detection.DetectionManager") local DetectionManager = require("classes.capClasses.detection.DetectionManager")
local GlobalCapManager = require("classes.capClasses.GlobalCapManager") local GlobalCapManager = require("classes.capClasses.GlobalCapManager")
@@ -33,19 +34,11 @@ MissionCommandsHelper.getOrCreate() -- initiate
local capConfig = CapConfig:new(); local capConfig = CapConfig:new();
local stageConfig = StageConfig:getInstance(); local stageConfig = StageConfig:getInstance();
local startingStage = stageConfig.startingStage or 1 local persistenceConfig = PersistenceConfig.new()
if SpearheadConfig and SpearheadConfig.Persistence and SpearheadConfig.Persistence.enabled == true then
standardLogger:info("Persistence enabled") if persistenceConfig and persistenceConfig:isEnabled() == true then
local persistenceLogger = Logger.new("Persistence", defaultLogLevel) local persistenceLogger = Logger.new("Persistence", defaultLogLevel)
Persistence.Init(persistenceLogger) Persistence.Init(persistenceLogger)
local persistanceStage = Persistence.GetActiveStage()
if persistanceStage then
standardLogger:info("Persistance activated and using persistant active stage: " .. persistanceStage)
startingStage = persistanceStage
end
else
standardLogger:info("Persistence disabled")
end end
local spawnLogger = Logger.new("SpawnManager", defaultLogLevel) local spawnLogger = Logger.new("SpawnManager", defaultLogLevel)
@@ -53,22 +46,29 @@ local spawnManager = SpawnManager.new(spawnLogger)
local detectionLogger = Logger.new("DetectionManager", defaultLogLevel) local detectionLogger = Logger.new("DetectionManager", defaultLogLevel)
local detectionManager = DetectionManager.New(detectionLogger) local detectionManager = DetectionManager.New(detectionLogger)
GlobalCapManager.start(databaseManager, capConfig, detectionManager, stageConfig, defaultLogLevel, spawnManager) GlobalCapManager.start(databaseManager, capConfig, detectionManager, defaultLogLevel, spawnManager)
GlobalStageManager.NewAndStart(databaseManager, stageConfig, defaultLogLevel, spawnManager) local globalStageManager = GlobalStageManager.new(databaseManager, stageConfig, defaultLogLevel, spawnManager)
GlobalFleetManager.start(databaseManager) GlobalFleetManager.start(databaseManager)
local SetStageDelayed = function(number, time)
SpearheadEvents.PublishStageNumberChanged(number)
return nil
end
timer.scheduleFunction(SetStageDelayed, startingStage, timer.getTime() + 3)
env.info(startTime .. "ms / " .. timer.getTime() * 1000 .. "ms") env.info(startTime .. "ms / " .. timer.getTime() * 1000 .. "ms")
local duration = (timer.getTime() * 1000) - startTime local duration = (timer.getTime() * 1000) - startTime
standardLogger:info("Spearhead Initialisation duration: " .. tostring(duration) .. "ms") standardLogger:info("Spearhead Initialisation duration: " .. tostring(duration) .. "ms")
local missionEditorWarningsLogger = Logger.new("MissionEditorWarnings", defaultLogLevel) local missionEditorWarningsLogger = Logger.new("MissionEditorWarnings", defaultLogLevel)
MissionEditorWarnings.WriteAll(missionEditorWarningsLogger) MissionEditorWarnings.WriteAll(missionEditorWarningsLogger)
GlobalStageManager:printFullOverview() globalStageManager:PrintMermaidStage()
local startDelayed = function()
globalStageManager:Start()
return nil
end
timer.scheduleFunction(startDelayed, nil, timer.getTime() + 5) -- delay start by 5 seconds so every deleted item it first removed entirely
local globalConfig = GlobalConfig.New()
if globalConfig and globalConfig:isDebugMenuEnabled() == true then
local DebugMenu = require("classes.debug.DebugMenu")
local debugMenu = DebugMenu.new()
debugMenu:RegisterMenus()
end