13 Commits
Author SHA1 Message Date
dutchie031 36ff621759 updated the config 2026-08-21 09:48:25 +02:00
dutchie031 ee5a591c8e fixed release pipeline
Publish Release / build (push) Successful in 1m8s
2026-08-20 17:03:58 +02:00
dutchie031 dd0d16cd1a fixed release pipeline
Publish Release / build (push) Successful in 17s
2026-08-20 17:02:05 +02:00
dutchie031 e67200bddd fixed release pipeline 2026-08-20 17:01:35 +02:00
dutchie031 f7ff367f5b Develop to Main (#32)
Reviewed-on: #32
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-20 15:00:07 +00:00
dutchie031 7475c03db2 updated docs 2026-07-30 14:59:28 +02:00
dutchie031 6390aeb527 updated docs 2026-07-30 14:55:53 +02:00
dutchie031 109db7cc6e updated docs
Update Docs / prepare-docs (push) Successful in 36s
Update Docs / build-image (push) Successful in 17s
Update Docs / deploy-container (push) Failing after 16s
2026-07-30 14:53:44 +02:00
dutchie031 5277c5e566 updated docs
Update Docs / prepare-docs (push) Successful in 19s
Update Docs / build-image (push) Canceled after 0s
Update Docs / deploy-container (push) Canceled after 0s
2026-07-30 14:53:33 +02:00
dutchie031 e3c53c253d pull policy updated 2026-07-30 14:40:02 +02:00
dutchie031 f6012031e2 pull policy updated
Update Docs / prepare-docs (push) Successful in 34s
Update Docs / build-image (push) Successful in 22s
Update Docs / deploy-container (push) Successful in 18s
2026-07-30 14:37:10 +02:00
dutchie031 09bc2629bb Merge branch 'main' of https://git.dutchie031.com/Spearhead/spearhead 2026-07-30 14:29:51 +02:00
dutchie031 5859c889e7 Merge branch 'main' of https://git.dutchie031.com/Spearhead/spearhead 2026-07-26 17:55:20 +02:00
51 changed files with 1666 additions and 1018 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

+19 -14
View File
@@ -32,23 +32,28 @@
<div class="content-wrapper">
<h1 id="welcome">Welcome to Spearhead!</h1>
<h1 id="welcome">Welcome to Spearhead!</h1>
<p>Welcome to the story teller friendly mission framework in DCS (at least, that's what we hope to create).</p>
<p>Together with mission editors we've tried to build a framework that makes creating multiplayer missions as easy as possible.
With the help of naming conventions, trigger zones, pre-configured logic and custom configuration items, it gives a huge amount of versatility.
we're trying to strike a balance between no-code and low-code with the possibility to hook into it for the hard core scripters (like ourselves).
<note-box type="info">
<b>For the scripters</b>: If you yourself are a scripter, and you're missing interfaces, please reach out to us! We want to make sure that the framework is as easy to use for scripters as it is for non-scripters, and we can't do that without your help!
</note-box>
</p>
<p>
Whether this is your first mission or your hundredth, we hope Spearhead will make your life easier.
</p>
<download-spearhead></download-spearhead>
<p>Welcome to the story teller friendly mission framework in DCS (at least, that's what we hope to create).</p>
<p>Together with mission editors we've tried to build a framework that
makes creating multiplayer missions as easy as possible.
With the help of naming conventions, trigger zones, pre-configured
logic and custom configuration items, it gives a huge amount of versatility.
we're trying to strike a balance between no-code and low-code with the possibility to hook into it for
the hard core scripters.
</p>
<p>
Whether this is your first mission or your hundredth, we hope Spearhead will make your life easier.
</p>
<download-spearhead></download-spearhead>
</div>
</div>
</main>
<footer>
+29 -29
View File
@@ -1,29 +1,29 @@
class CodeBlock extends HTMLElement {
connectedCallback() {
// Get the code content as text, preserving whitespace
let code = this.textContent.replace(/\r\n?/g, "\n");
// Remove leading/trailing blank lines
code = code.replace(/^\s*\n/, '').replace(/\n\s*$/, '');
// Find leading spaces from the first non-empty line
const lines = code.split("\n");
const firstLine = lines.find(line => line.trim().length > 0) || '';
const leadingSpaces = firstLine.match(/^\s*/)[0].length;
// Remove that many leading spaces from all lines
const stripped = lines.map(line => line.slice(leadingSpaces)).join("\n");
// Escape HTML special characters
const escaped = stripped
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const lang = this.getAttribute('lang') || '';
const langClass = lang ? `language-${lang}` : '';
this.innerHTML = `<pre class=\"custom-code-block\"><code class=\"${langClass}\">${escaped}</code></pre>`;
// If Prism or highlight.js is present, trigger highlighting
if (window.Prism && Prism.highlightElement) {
Prism.highlightElement(this.querySelector('code'));
} else if (window.hljs && hljs.highlightElement) {
hljs.highlightElement(this.querySelector('code'));
}
}
}
customElements.define('code-block', CodeBlock);
class CodeBlock extends HTMLElement {
connectedCallback() {
// Get the code content as text, preserving whitespace
let code = this.textContent.replace(/\r\n?/g, "\n");
// Remove leading/trailing blank lines
code = code.replace(/^\s*\n/, '').replace(/\n\s*$/, '');
// Find leading spaces from the first non-empty line
const lines = code.split("\n");
const firstLine = lines.find(line => line.trim().length > 0) || '';
const leadingSpaces = firstLine.match(/^\s*/)[0].length;
// Remove that many leading spaces from all lines
const stripped = lines.map(line => line.slice(leadingSpaces)).join("\n");
// Escape HTML special characters
const escaped = stripped
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const lang = this.getAttribute('lang') || '';
const langClass = lang ? `language-${lang}` : '';
this.innerHTML = `<pre class=\"custom-code-block\"><code class=\"${langClass}\">${escaped}</code></pre>`;
// If Prism or highlight.js is present, trigger highlighting
if (window.Prism && Prism.highlightElement) {
Prism.highlightElement(this.querySelector('code'));
} else if (window.hljs && hljs.highlightElement) {
hljs.highlightElement(this.querySelector('code'));
}
}
}
customElements.define('code-block', CodeBlock);
+32 -32
View File
@@ -1,32 +1,32 @@
class CodeInline extends HTMLElement {
connectedCallback() {
// Get the code content as text
let code = this.textContent;
// Escape HTML special characters
code = code
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
// Highlight variables: $variable, {variable}, or %variable%
code = code.replace(/(\$[a-zA-Z_][\w]*)|(\{[^\}]+\})|(%[^%]+%)/g, match =>
`<span class="inline-var">${match}</span>`
);
// Get language attribute for potential syntax highlighting
const lang = this.getAttribute('lang') || '';
const langClass = lang ? `language-${lang}` : '';
this.innerHTML = `<code class="custom-inline-code ${langClass}">${code}</code>`;
// If Prism or highlight.js is present, trigger highlighting
if (window.Prism && Prism.highlightElement) {
Prism.highlightElement(this.querySelector('code'));
} else if (window.hljs && hljs.highlightElement) {
hljs.highlightElement(this.querySelector('code'));
}
}
}
customElements.define('code-inline', CodeInline);
class CodeInline extends HTMLElement {
connectedCallback() {
// Get the code content as text
let code = this.textContent;
// Escape HTML special characters
code = code
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
// Highlight variables: $variable, {variable}, or %variable%
code = code.replace(/(\$[a-zA-Z_][\w]*)|(\{[^\}]+\})|(%[^%]+%)/g, match =>
`<span class="inline-var">${match}</span>`
);
// Get language attribute for potential syntax highlighting
const lang = this.getAttribute('lang') || '';
const langClass = lang ? `language-${lang}` : '';
this.innerHTML = `<code class="custom-inline-code ${langClass}">${code}</code>`;
// If Prism or highlight.js is present, trigger highlighting
if (window.Prism && Prism.highlightElement) {
Prism.highlightElement(this.querySelector('code'));
} else if (window.hljs && hljs.highlightElement) {
hljs.highlightElement(this.querySelector('code'));
}
}
}
customElements.define('code-inline', CodeInline);
@@ -66,7 +66,7 @@ class DownloadScript extends HTMLElement {
var version = "?";
var timestamp = "?";
var size = "?";
var href = "https://git.dutchie031.com/Spearhead/spearhead/releases"
var href = "https://git.dutchie031.com/api/v1/repos/Spearhead/spearhead/releases"
const latestRelease = await this.fetchLatestRelease();
if (latestRelease) {
@@ -105,7 +105,7 @@ class DownloadScript extends HTMLElement {
<p>
<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="https://git.dutchie031.com/Spearhead/spearhead/releases">See All Versions here</a>
<a style="text-decoration: underline;" target="_blank" href="https://git.dutchie031.com/api/v1/repos/Spearhead/spearhead/releases">See All Versions here</a>
</p>
</div>
+35 -35
View File
@@ -1,35 +1,35 @@
class Note extends HTMLElement {
connectedCallback() {
// Get the note content
const content = this.innerHTML;
// Get optional type attribute for different note styles
const type = this.getAttribute('type') || 'default';
// Get optional title attribute
const title = this.getAttribute('title');
// Build the note HTML
let noteHTML = '<div class="note';
// Add type-specific class if provided
if (type !== 'default') {
noteHTML += ` note-${type}`;
}
noteHTML += '">';
// Add title if provided
if (title) {
noteHTML += `<h4 class="note-title">${title}</h4>`;
}
// Add the content
noteHTML += content;
noteHTML += '</div>';
this.innerHTML = noteHTML;
}
}
customElements.define('note-box', Note);
class Note extends HTMLElement {
connectedCallback() {
// Get the note content
const content = this.innerHTML;
// Get optional type attribute for different note styles
const type = this.getAttribute('type') || 'default';
// Get optional title attribute
const title = this.getAttribute('title');
// Build the note HTML
let noteHTML = '<div class="note';
// Add type-specific class if provided
if (type !== 'default') {
noteHTML += ` note-${type}`;
}
noteHTML += '">';
// Add title if provided
if (title) {
noteHTML += `<h4 class="note-title">${title}</h4>`;
}
// Add the content
noteHTML += content;
noteHTML += '</div>';
this.innerHTML = noteHTML;
}
}
customElements.define('note-box', Note);
+197 -197
View File
@@ -1,198 +1,198 @@
class Sidebar extends HTMLElement {
constructor() {
super();
this.navItems = [];
}
connectedCallback() {
this.render();
this.scanHeaders();
this.setupScrollListener();
this.highlightActiveSection();
}
scanHeaders() {
// Clear existing nav items
this.navItems = [];
// Find all h2, h3, and h4 elements with IDs in the content area
const contentWrapper = document.querySelector('.content-wrapper');
if (!contentWrapper) return;
const headers = contentWrapper.querySelectorAll('h2[id], h3[id], h4[id]');
headers.forEach(header => {
const id = header.getAttribute('id');
const text = header.textContent.trim();
const level = header.tagName.toLowerCase();
this.navItems.push({
id,
text,
level,
element: header
});
});
this.renderNavigation();
}
renderNavigation() {
const ul = this.querySelector('ul');
if (!ul) return;
// Clear existing content
ul.innerHTML = '';
let currentH2Li = null;
let currentH3Li = null;
this.navItems.forEach(item => {
if (item.level === 'h2') {
// Create h2 item
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `#${item.id}`;
a.className = 'side-nav-h2';
a.textContent = item.text;
a.addEventListener('click', (e) => this.handleNavClick(e, item.id));
li.appendChild(a);
ul.appendChild(li);
currentH2Li = li;
currentH3Li = null;
} else if (item.level === 'h3' && currentH2Li) {
// Create h3 item under current h2
let subUl = currentH2Li.querySelector('ul');
if (!subUl) {
subUl = document.createElement('ul');
currentH2Li.appendChild(subUl);
}
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `#${item.id}`;
a.className = 'side-nav-h3';
a.textContent = item.text;
a.addEventListener('click', (e) => this.handleNavClick(e, item.id));
li.appendChild(a);
subUl.appendChild(li);
currentH3Li = li;
} else if (item.level === 'h4' && currentH3Li) {
// Create h4 item under current h3
let subUl = currentH3Li.querySelector('ul');
if (!subUl) {
subUl = document.createElement('ul');
currentH3Li.appendChild(subUl);
}
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `#${item.id}`;
a.className = 'side-nav-h4';
a.textContent = item.text;
a.addEventListener('click', (e) => this.handleNavClick(e, item.id));
li.appendChild(a);
subUl.appendChild(li);
}
});
}
handleNavClick(e, targetId) {
e.preventDefault();
// Remove active class from all links
this.querySelectorAll('a').forEach(link => {
link.classList.remove('active');
});
// Add active class to clicked link
e.target.classList.add('active');
// Smooth scroll to target
const targetElement = document.getElementById(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
setupScrollListener() {
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
this.highlightActiveSection();
ticking = false;
});
ticking = true;
}
};
window.addEventListener('scroll', handleScroll);
}
highlightActiveSection() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const offset = 100; // Offset for highlighting
let activeId = '';
// Find the currently visible section
this.navItems.forEach(item => {
const element = item.element;
const rect = element.getBoundingClientRect();
const elementTop = rect.top + scrollTop;
if (elementTop <= scrollTop + offset) {
activeId = item.id;
}
});
// Update active state
this.querySelectorAll('a').forEach(link => {
link.classList.remove('active');
});
if (activeId !== nil && activeId !== '') {
const activeLink = this.querySelector(`a[href="#${activeId}"]`);
if (activeLink) {
activeLink.classList.add('active');
}
}
}
render() {
this.innerHTML = `
<div class="side-nav">
<h4 class="side-nav-title"></h4>
<ul>
<!-- Navigation items will be populated automatically -->
</ul>
</div>
`;
}
// Method to refresh the sidebar when content changes
refresh() {
this.scanHeaders();
}
// Method to set the sidebar title
setTitle(title) {
const titleElement = this.querySelector('.side-nav-title');
if (titleElement) {
titleElement.textContent = title;
}
}
}
// Register the custom element
customElements.define('app-sidebar', Sidebar);
class Sidebar extends HTMLElement {
constructor() {
super();
this.navItems = [];
}
connectedCallback() {
this.render();
this.scanHeaders();
this.setupScrollListener();
this.highlightActiveSection();
}
scanHeaders() {
// Clear existing nav items
this.navItems = [];
// Find all h2, h3, and h4 elements with IDs in the content area
const contentWrapper = document.querySelector('.content-wrapper');
if (!contentWrapper) return;
const headers = contentWrapper.querySelectorAll('h2[id], h3[id], h4[id]');
headers.forEach(header => {
const id = header.getAttribute('id');
const text = header.textContent.trim();
const level = header.tagName.toLowerCase();
this.navItems.push({
id,
text,
level,
element: header
});
});
this.renderNavigation();
}
renderNavigation() {
const ul = this.querySelector('ul');
if (!ul) return;
// Clear existing content
ul.innerHTML = '';
let currentH2Li = null;
let currentH3Li = null;
this.navItems.forEach(item => {
if (item.level === 'h2') {
// Create h2 item
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `#${item.id}`;
a.className = 'side-nav-h2';
a.textContent = item.text;
a.addEventListener('click', (e) => this.handleNavClick(e, item.id));
li.appendChild(a);
ul.appendChild(li);
currentH2Li = li;
currentH3Li = null;
} else if (item.level === 'h3' && currentH2Li) {
// Create h3 item under current h2
let subUl = currentH2Li.querySelector('ul');
if (!subUl) {
subUl = document.createElement('ul');
currentH2Li.appendChild(subUl);
}
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `#${item.id}`;
a.className = 'side-nav-h3';
a.textContent = item.text;
a.addEventListener('click', (e) => this.handleNavClick(e, item.id));
li.appendChild(a);
subUl.appendChild(li);
currentH3Li = li;
} else if (item.level === 'h4' && currentH3Li) {
// Create h4 item under current h3
let subUl = currentH3Li.querySelector('ul');
if (!subUl) {
subUl = document.createElement('ul');
currentH3Li.appendChild(subUl);
}
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `#${item.id}`;
a.className = 'side-nav-h4';
a.textContent = item.text;
a.addEventListener('click', (e) => this.handleNavClick(e, item.id));
li.appendChild(a);
subUl.appendChild(li);
}
});
}
handleNavClick(e, targetId) {
e.preventDefault();
// Remove active class from all links
this.querySelectorAll('a').forEach(link => {
link.classList.remove('active');
});
// Add active class to clicked link
e.target.classList.add('active');
// Smooth scroll to target
const targetElement = document.getElementById(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
setupScrollListener() {
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
this.highlightActiveSection();
ticking = false;
});
ticking = true;
}
};
window.addEventListener('scroll', handleScroll);
}
highlightActiveSection() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const offset = 100; // Offset for highlighting
let activeId = '';
// Find the currently visible section
this.navItems.forEach(item => {
const element = item.element;
const rect = element.getBoundingClientRect();
const elementTop = rect.top + scrollTop;
if (elementTop <= scrollTop + offset) {
activeId = item.id;
}
});
// Update active state
this.querySelectorAll('a').forEach(link => {
link.classList.remove('active');
});
if (activeId !== nil && activeId !== '') {
const activeLink = this.querySelector(`a[href="#${activeId}"]`);
if (activeLink) {
activeLink.classList.add('active');
}
}
}
render() {
this.innerHTML = `
<div class="side-nav">
<h4 class="side-nav-title"></h4>
<ul>
<!-- Navigation items will be populated automatically -->
</ul>
</div>
`;
}
// Method to refresh the sidebar when content changes
refresh() {
this.scanHeaders();
}
// Method to set the sidebar title
setTitle(title) {
const titleElement = this.querySelector('.side-nav-title');
if (titleElement) {
titleElement.textContent = title;
}
}
}
// Register the custom element
customElements.define('app-sidebar', Sidebar);
export default Sidebar;
+10 -10
View File
File diff suppressed because one or more lines are too long
+90 -90
View File
@@ -1,91 +1,91 @@
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.getElementById('theme-toggle');
const sunIcon = document.getElementById('sun-icon');
const moonIcon = document.getElementById('moon-icon');
if (!themeToggle || !sunIcon || !moonIcon) return;
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
sunIcon.style.display = theme === 'light' ? 'block' : 'none';
moonIcon.style.display = theme === 'light' ? 'none' : 'block';
}
themeToggle.addEventListener('click', function() {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
setTheme(current === 'dark' ? 'light' : 'dark');
});
// Initialize
const saved = localStorage.getItem('theme') || 'dark';
setTheme(saved);
// Set side-nav-title to the first h1's text
var h1 = document.querySelector('.content-wrapper h1');
var sideNavTitle = document.querySelector('.side-nav-title');
if (h1 && sideNavTitle) {
sideNavTitle.textContent = h1.textContent;
}
});
// Highlight sidenav link on scroll (shared for all pages)
function setupSideNavHighlight() {
const navLinks = document.querySelectorAll('.side-nav a');
if (!navLinks.length) return;
const sections = Array.from(navLinks).map(link => {
const id = link.getAttribute('href').replace('#', '');
return document.getElementById(id);
});
function getHeaderOffset() {
const header = document.querySelector('header');
return header ? header.offsetHeight : 0;
}
// Custom scroll on nav click
navLinks.forEach((link, i) => {
link.addEventListener('click', function(e) {
const section = sections[i];
if (section) {
e.preventDefault();
const headerOffset = getHeaderOffset();
const targetY = window.innerHeight * 0.10;
const sectionTop = section.getBoundingClientRect().top + window.scrollY;
const scrollTo = sectionTop - headerOffset - targetY;
window.scrollTo({ top: scrollTo, behavior: 'smooth' });
}
});
});
function onScroll() {
const headerOffset = getHeaderOffset();
const targetY = window.innerHeight * 0.1; // 30% from the top
let closestIdx = 0;
let minDist = Infinity;
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
if (section) {
const dist = Math.abs(section.getBoundingClientRect().top - headerOffset - targetY);
if (dist < minDist) {
minDist = dist;
closestIdx = i;
}
}
}
navLinks.forEach((link, i) => {
if (i === closestIdx) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
}
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
onScroll(); // Initial call
}
// Run on DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupSideNavHighlight);
} else {
setupSideNavHighlight();
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.getElementById('theme-toggle');
const sunIcon = document.getElementById('sun-icon');
const moonIcon = document.getElementById('moon-icon');
if (!themeToggle || !sunIcon || !moonIcon) return;
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
sunIcon.style.display = theme === 'light' ? 'block' : 'none';
moonIcon.style.display = theme === 'light' ? 'none' : 'block';
}
themeToggle.addEventListener('click', function() {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
setTheme(current === 'dark' ? 'light' : 'dark');
});
// Initialize
const saved = localStorage.getItem('theme') || 'dark';
setTheme(saved);
// Set side-nav-title to the first h1's text
var h1 = document.querySelector('.content-wrapper h1');
var sideNavTitle = document.querySelector('.side-nav-title');
if (h1 && sideNavTitle) {
sideNavTitle.textContent = h1.textContent;
}
});
// Highlight sidenav link on scroll (shared for all pages)
function setupSideNavHighlight() {
const navLinks = document.querySelectorAll('.side-nav a');
if (!navLinks.length) return;
const sections = Array.from(navLinks).map(link => {
const id = link.getAttribute('href').replace('#', '');
return document.getElementById(id);
});
function getHeaderOffset() {
const header = document.querySelector('header');
return header ? header.offsetHeight : 0;
}
// Custom scroll on nav click
navLinks.forEach((link, i) => {
link.addEventListener('click', function(e) {
const section = sections[i];
if (section) {
e.preventDefault();
const headerOffset = getHeaderOffset();
const targetY = window.innerHeight * 0.10;
const sectionTop = section.getBoundingClientRect().top + window.scrollY;
const scrollTo = sectionTop - headerOffset - targetY;
window.scrollTo({ top: scrollTo, behavior: 'smooth' });
}
});
});
function onScroll() {
const headerOffset = getHeaderOffset();
const targetY = window.innerHeight * 0.1; // 30% from the top
let closestIdx = 0;
let minDist = Infinity;
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
if (section) {
const dist = Math.abs(section.getBoundingClientRect().top - headerOffset - targetY);
if (dist < minDist) {
minDist = dist;
closestIdx = i;
}
}
}
navLinks.forEach((link, i) => {
if (i === closestIdx) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
}
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
onScroll(); // Initial call
}
// Run on DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupSideNavHighlight);
} else {
setupSideNavHighlight();
}
+2 -4
View File
@@ -37,10 +37,8 @@
Even though, at the time of writing this, I've written every single line of code myself, I could not have done it without the help of these members.<br>
Therefore it's something WE created.
Our goal was very simple, make it possible to make big dynamic missions, without having to either give away all controll or having to get knee deep into scripting. <br>
We struck a balance.
Our goal was very simple, make it possible to make big dynamic missions, without having to either give away all control or having to get knee deep into scripting. <br>
We struck a balance.
</p>
+2 -2
View File
@@ -90,13 +90,13 @@
Missions are also defined by trigger zones. <br>
They'll be picked up by Spearhead when named according to the naming convention: <br>
<code-inline>MISSION_[Type]_[FreeForm]</code-inline> <br>
Where <code-inline>[Type]</code-inline> is the type of mission. <br>
Where <code-inline>[Type]</code-inline> is the type of mission. <a href="./reference.html#mission-zones">See all</a><br>
</p>
<h3 id="mission-cas">CAS</h3>
<p>
Personally CAS is one of our favorite mission types. <br>
Personally CAS is one of my favorite mission types. <br>
Mostly because it's easy to set up and creates a truly immersive experience. <br>
</p>
+12 -1
View File
@@ -80,11 +80,22 @@
<strong>Example:</strong> <code-inline>MISSION_DEAD_BYRON</code-inline>
</p>
<p>
Missions are completable objectives with specific types, such as DEAD, BAI, STRIKE, or SAM. <br />
Missions are completable objectives with specific types. <br />
Randomized missions can be defined using the format: <span
class="inline-lua"><span class="lua-variable">RANDOMMISSION_[Type]_[Name]_[Index]</span></span>.
</p>
<p>
Valid Types:
<ul>
<li>SAM</li>
<li>DEAD</li>
<li>BAI</li>
<li>CAS</li>
<li>STRIKE</li>
</ul>
</p>
<h3 id="cap-routes">CAP Routes</h3>
<p>
<strong>Format:</strong> <code-inline>CAPROUTE_[routeID]_[Name]</code-inline> <br />
+59 -59
View File
@@ -1,60 +1,60 @@
<!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 API</title>
<link rel="stylesheet" href="/style/prism.css">
<link rel="stylesheet" href="/style/style.css">
<script src="/js/prism.js"></script>
<script src="/js/site.js"></script>
<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>Spearhead API</h1>
<div class="note">
<p><strong>NOTE:</strong> The Spearhead.API space is only released in the Beta branch at the moment.</p>
</div>
<h2 id="introduction">Introduction</h2>
<p>
The <span class="inline-lua"><span class="lua-variable">Spearhead.API</span></span> space is specifically created to make sure mission makers can interact with the framework.
</p>
<p>
Simply alter logic, get the current state in Spearhead, and give the whole Mission Editor more control.
</p>
<p>
For example, late activate the entire framework by calling <span class="inline-lua"><span class="lua-function">Spearhead.API.Stages.changeStage</span>(<span class="lua-variable">1</span>)</span> later or on demand and setting the starting config stage to -1 in the Spearhead configuration file.
</p>
<h2 id="stages">Stages</h2>
<pre>
@@API_CODE@@
</pre>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Spearhead Project</p>
</footer>
<style>
.side-nav a.active {
font-weight: bold;
color: #4fc3f7;
}
</style>
</body>
<!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 API</title>
<link rel="stylesheet" href="/style/prism.css">
<link rel="stylesheet" href="/style/style.css">
<script src="/js/prism.js"></script>
<script src="/js/site.js"></script>
<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>Spearhead API</h1>
<div class="note">
<p><strong>NOTE:</strong> The Spearhead.API space is only released in the Beta branch at the moment.</p>
</div>
<h2 id="introduction">Introduction</h2>
<p>
The <span class="inline-lua"><span class="lua-variable">Spearhead.API</span></span> space is specifically created to make sure mission makers can interact with the framework.
</p>
<p>
Simply alter logic, get the current state in Spearhead, and give the whole Mission Editor more control.
</p>
<p>
For example, late activate the entire framework by calling <span class="inline-lua"><span class="lua-function">Spearhead.API.Stages.changeStage</span>(<span class="lua-variable">1</span>)</span> later or on demand and setting the starting config stage to -1 in the Spearhead configuration file.
</p>
<h2 id="stages">Stages</h2>
<pre>
@@API_CODE@@
</pre>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Spearhead Project</p>
</footer>
<style>
.side-nav a.active {
font-weight: bold;
color: #4fc3f7;
}
</style>
</body>
</html>
+3 -3
View File
@@ -1,3 +1,3 @@
/* PrismJS 1.30.0
https://prismjs.com/download#themes=prism-okaidia&languages=lua */
code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
/* PrismJS 1.30.0
https://prismjs.com/download#themes=prism-okaidia&languages=lua */
code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
+105
View File
@@ -0,0 +1,105 @@
name: Publish Release
on:
push:
branches:
- develop
paths:
- src/**
- .gitea/workflows/release-beta.yml
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read Version from version.txt
id: read_version
run: |
VERSION=beta
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION"
- name: Extract Release Notes
id: release_notes
run: |
VERSION=beta
RELEASE_NOTES=$(awk -v ver="Unreleased" '
/^## \[/ {
if (found) exit
if (index($0, "[" ver "]") > 0) found = 1
next
}
found { print }
' RELEASENOTES.md)
# Write multi-line output properly
echo "body<<EOF" >> "$GITHUB_OUTPUT"
echo "$RELEASE_NOTES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Compile Spearhead
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-action@action/v0
with:
source-root: ./src
output-file: ./output/spearhead.lua
- name: Remove existing tag if it exists
id: check_tag
run: |
TAG="${{ steps.read_version.outputs.tag }}"
if git tag | grep -q "^$TAG$"; then
echo "Tag exists. Removing it..."
git push origin :refs/tags/"$TAG" || true
git tag -d "$TAG" || true
fi
echo "Ready to create fresh tag: $TAG"
- name: Apply Tag to Current Commit
run: |
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git tag ${{ steps.read_version.outputs.tag }}
git push origin ${{ steps.read_version.outputs.tag }}
- name: Copy Release to Versioned File
run: |
cp ./output/spearhead.lua ./output/spearhead.${{ steps.read_version.outputs.tag }}.lua
echo "Versioned file created: spearhead.${{ steps.read_version.outputs.tag }}.lua"
- name: Create Release and Upload Assets
uses: akkuman/gitea-release-action@v1
with:
tag_name: ${{ steps.read_version.outputs.tag }}
name: Release ${{ steps.read_version.outputs.tag }}
body: ${{ steps.release_notes.outputs.body }}
prerelease: true
files: |-
./output/spearhead.lua
./output/spearhead.${{ steps.read_version.outputs.tag }}.lua
#token: ${{ secrets.GITHUB_TOKEN }}
env:
NODE_OPTIONS: '--experimental-fetch'
- name: Announce Release
uses: tsickert/discord-webhook@v7.0.0
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
content: "New Beta Release"
username: "Spearhead Release Bot"
#TODO: avatar-url spearhead avatar
thread-id: 1538520488442331227
embed-title: "Spearhead Release Beta"
embed-color: 3093247
embed-description:
${{ steps.release_notes.outputs.body }}
embed-url:
https://git.dutchie031.com/Spearhead/spearhead/releases/tag/${{ steps.read_version.outputs.tag }}
+15
View File
@@ -84,3 +84,18 @@ jobs:
env:
NODE_OPTIONS: '--experimental-fetch'
- name: Announce Release
uses: tsickert/discord-webhook@v7.0.0
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
content: "New Beta Release"
username: "Spearhead Release Bot"
#TODO: avatar-url spearhead avatar
thread-id: 1538520596802175037
embed-title: "Spearhead Release"
embed-color: 3093247
embed-description:
${{ steps.release_notes.outputs.body }}
embed-url:
https://git.dutchie031.com/Spearhead/spearhead/releases/tag/${{ steps.read_version.outputs.tag }}
+2 -1
View File
@@ -3,4 +3,5 @@
/dist
.vscode/settings.json
.vscode/settings.json
**\settings.json
+3
View File
@@ -15,6 +15,8 @@ spec:
metadata:
labels:
app: {{ .Values.app }}
annotations:
deployment-timestamp: "{{ now.Unix }}"
spec:
securityContext:
runAsNonRoot: true
@@ -24,6 +26,7 @@ spec:
containers:
- name: {{ .Values.app }}
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
imagePullPolicy: Always
ports:
- containerPort: {{ .Values.ports.containerPort }}
volumeMounts:
-25
View File
@@ -1,25 +0,0 @@
{
"cSpell.words": [
"farp",
"Sams",
"SEAD"
],
"Lua.diagnostics.globals": [
"lfs"
],
"maptz.regionfolder": {
"[lua]" : {
"foldEnd": "--endregion",
"foldEndRegex": "[\\s]*--endregion",
"foldStart": "--region [NAME]",
"foldStartRegex": "[\\s]*--region[\\s]*(.*)",
"disableFolding": false
}
},
"livePreview.serverRoot": "/_docs",
"Lua.workspace.library": [
"/Users/ex61wi/Developer/personal/DcsMissionScriptingTools/dutchies-dcs-scripting-tools/lua-addons",
"c:\\Users\\Tim\\.vscode\\extensions\\dutchie031.dutchies-dcs-scripting-tools-0.0.3\\lua-addons"
],
"dutchies-dcs-scripting-tools.compileAt": "onSave",
}
+27 -2
View File
@@ -7,16 +7,41 @@
### Bug Fixes
## [0.13.0] 2026-08
## [0.12.1] 2026-07
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>
### Breaking Changes
### New Features
- Issue #26
Possibility to have Stage Overview briefings (which include current missions sorted by distance) to be shown on spawning of a player.
PR #31
### Bug Fixes
- Fixed stage drawing to only be checked when a stage completed. Now done on stage number changed.
- Issue #11
Supply crate spawning now checks for free space and will not spawn if the area is too crowded.
Additionally different units will spawn in different areas depending on loading side.
PR #17
- Fixed command wiring for supply hubs for better and more accurate detection of units spawning and entering/exiting zone.
PR #15
- Fixed custom drawings not being drawn correctly.
PR #18
- Fixed CAP Callbacks not working since the change to a transpiled script. Now a global callback circumvents this issue.
PR #18
- Fixed #9
Changed order of checking mission briefings
PR #19
- Fixed Configuration defaulting to true for all booleans in StageConfig
PR #28
- Fixed Pre-Activated stages not always drawing or pre-activating correctly.
PR #28
- Addressed #24
CAP max commit range is now configurable in the config.lua file.
Issue remains open in order to apply further fine grained tuning.
PR #29
## [0.12.0] 2026-06
+10 -1
View File
@@ -6,7 +6,7 @@ SpearheadConfig = {
--- 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
briefingMessageDuration = 30, --default 30
briefingMessageDuration = 60, --default 60
CapConfig = {
--quickly enable of disable the entire CAP Logic
@@ -29,6 +29,12 @@ SpearheadConfig = {
-- unit: feet
maxAlt = 28000, -- default 28000
--The "maxDistance" that's set on the CAP tasking for the aircraft to commit to a target.
--This is the distance from the aircraft to the target that the aircraft will commit to engaging
--This is shared for CAP, Intercept and Sweep missions for now.
-- unit: nautical miles
maxCommitRange = 35, -- default 35
-- DELAYS.
-- Delays work as follow.
-- When an aircraft lands alive and well it will be rearmed and ready to go.
@@ -67,6 +73,9 @@ SpearheadConfig = {
--The location will continously update for the last killed unit.
markLastContact = false, -- default false
--If enabled, the stage briefing including the current missions will be shown to players on spawn.
briefingOnSpawn = true, -- default 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.
--[[
+5 -5
View File
@@ -220,7 +220,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
action = {
id = "Script",
params = {
command = "pcall(Spearhead.Events.PublishOnStation, \"" .. groupName .. "\")"
command = "pcall(GlobalCapCallBacks.PublishOnStation, \"" .. groupName .. "\")"
}
}
}
@@ -260,7 +260,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
},
stopCondition = {
duration = durationBefore10,
condition = "return Spearhead.DcsUtil.NeedsRTBInTen(\"" .. groupName .. "\", 0.10)",
condition = "return GlobalCapCallBacks.NeedsRTBInTen(\"" .. groupName .. "\", 0.10)",
}
}
},
@@ -273,7 +273,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
action = {
id = "Script",
params = {
command = "pcall(Spearhead.Events.PublishRTBInTen, \"" .. groupName .. "\")"
command = "pcall(GlobalCapCallBacks.PublishRTBInTen, \"" .. groupName .. "\")"
}
}
}
@@ -299,7 +299,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
},
stopCondition = {
duration = durationAfter10,
condition = "return Spearhead.DcsUtil.IsBingoFuel(\"" .. groupName .. "\")",
condition = "return GlobalCapCallBacks.IsBingoFuel(\"" .. groupName .. "\", 0.10)",
}
}
},
@@ -312,7 +312,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
action = {
id = "Script",
params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")"
command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
}
}
}
@@ -0,0 +1,26 @@
local DcsUtil = require("classes.util.DcsUtil")
local Events = require("classes.spearhead_events")
GlobalCapCallBacks = {}
function GlobalCapCallBacks.IsBingoFuel(groupName, fuelPercent)
return DcsUtil.IsBingoFuel(groupName, fuelPercent)
end
function GlobalCapCallBacks.NeedsRTBInTen(groupName, fuelOffset)
return DcsUtil.NeedsRTBInTen(groupName, fuelOffset)
end
function GlobalCapCallBacks.PublishRTBInTen(groupName)
return Events.PublishRTBInTen(groupName)
end
function GlobalCapCallBacks.PublishRTB(groupName)
return Events.PublishRTB(groupName)
end
function GlobalCapCallBacks.PublishOnStation(groupName)
return Events.PublishOnStation(groupName)
end
@@ -247,7 +247,7 @@ function INTERCEPT.getInterceptTaskPoint(groupName, currentPoint, targetPoint, a
action = {
id = "Script",
params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")"
command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
}
}
}
@@ -379,7 +379,7 @@ function INTERCEPT.getUnitInterceptTaskPoint(groupName, currentPoint, targetPosi
action = {
id = "Script",
params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")"
command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
}
}
}
+2 -2
View File
@@ -189,7 +189,7 @@ function SWEEP.getAsTasking(groupName, airbase, capZone, capConfig)
action = {
id = "Script",
params = {
command = "pcall(Spearhead.Events.PublishOnStation, \"" .. groupName .. "\")"
command = "pcall(GlobalCapCallBacks.PublishOnStation, \"" .. groupName .. "\")"
}
}
}
@@ -270,7 +270,7 @@ function SWEEP.getAsTasking(groupName, airbase, capZone, capConfig)
action = {
id = "Script",
params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")"
command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
}
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ function CapConfig.new()
self._minDurationOnStation = 1200
self._maxDurationOnStation = 2700
self._maxDeviationRange = 35 * 1852 -- in meters
self._maxDeviationRange = (tonumber(SpearheadConfig.CapConfig.maxCommitRange) or 35) * 1852 -- in meters
self._rearmDelay = tonumber(SpearheadConfig.CapConfig.rearmDelay) or 600
self._repairDelay = tonumber(SpearheadConfig.CapConfig.repairDelay) or 600
self._deathDelay = tonumber(SpearheadConfig.CapConfig.deathDelay) or 1800
+14 -2
View File
@@ -4,7 +4,8 @@
local briefingMessageTime = nil
---@class GlobalConfig
---@field private _briefingTime number ;
---@field private _briefingTime number
---@field private _debugEnabled boolean
local GlobalConfig = {}
GlobalConfig.__index = GlobalConfig;
@@ -19,13 +20,24 @@ function GlobalConfig.New()
if SpearheadConfig.briefingMessageDuration then
self._briefingTime = SpearheadConfig.briefingMessageDuration
end
if SpearheadConfig.debugEnabled ~= nil then
self._debugEnabled = SpearheadConfig.debugEnabled
else
self._debugEnabled = false
end
end
return self
end
function GlobalConfig:getBriefingTime()
return self._briefingTime or 30
return self._briefingTime or 60
end
function GlobalConfig:isDebugEnabled()
return self._debugEnabled or false
end
+34 -16
View File
@@ -7,29 +7,47 @@
--- @field startingStage integer
--- @field maxMissionsPerStage integer
--- @field AmountPreactivateStage integer
--- @field briefingOnSpawnEnabled boolean
local StageConfig = {};
StageConfig.__index = StageConfig
local Logger = require("classes.util.Logger")
local _logger = Logger.new("StageConfig", Logger.LogLevel)
---comment
---@return StageConfig
function StageConfig:new()
local function new()
if SpearheadConfig == nil then SpearheadConfig = {} end
if SpearheadConfig.StageConfig == nil then SpearheadConfig.StageConfig = {} end
if SpearheadConfig == nil then
_logger:warn("SpearheadConfig is nil, creating default SpearheadConfig")
SpearheadConfig = {}
end
---@type StageConfig
local o = {
isEnabled = SpearheadConfig.StageConfig.enabled or true,
isDrawStagesEnabled = SpearheadConfig.StageConfig.drawStages or true,
isAutoStages = SpearheadConfig.StageConfig.autoStages or true,
startingStage = SpearheadConfig.StageConfig.startingStage or 1,
maxMissionsPerStage = SpearheadConfig.StageConfig.maxMissionStage or 10,
isDrawPreActivatedEnabled = SpearheadConfig.StageConfig.drawPreActivated or true,
AmountPreactivateStage = SpearheadConfig.StageConfig.preactivateStage or 1,
}
setmetatable(o, { __index = self })
if SpearheadConfig.StageConfig == nil then
_logger:warn("SpearheadConfig.StageConfig is nil, creating default StageConfig")
SpearheadConfig.StageConfig = {}
end
return o;
local self = setmetatable({}, StageConfig)
self.isEnabled = SpearheadConfig.StageConfig.enabled ~= false
self.isDrawStagesEnabled = SpearheadConfig.StageConfig.drawStages ~= false
self.isAutoStages = SpearheadConfig.StageConfig.autoStages ~= false
self.startingStage = SpearheadConfig.StageConfig.startingStage or 1
self.maxMissionsPerStage = SpearheadConfig.StageConfig.maxMissionStage or 10
self.isDrawPreActivatedEnabled = SpearheadConfig.StageConfig.drawPreActivated ~= false
self.AmountPreactivateStage = SpearheadConfig.StageConfig.preactivateStage or 1
self.briefingOnSpawnEnabled = SpearheadConfig.StageConfig.briefingOnSpawn ~= false
_logger:info("Successfully created StageConfig Object")
return self;
end
local config = new();
---@return StageConfig
function StageConfig:getInstance()
return config
end
return StageConfig
+15 -12
View File
@@ -350,18 +350,6 @@ function Database.New(Logger)
end
end
for _, missionZone in pairs(self._tables.MissionZones) do
if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then
MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing")
end
end
for _, missionZone in pairs(self._tables.RandomMissionZones) do
if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then
MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing")
end
end
for _, farpZoneName in pairs(self._tables.AllFarpZones) do
for _, airbase in pairs(world.getAirbases()) do
if airbase:getDesc().category == Airbase.Category.HELIPAD then
@@ -447,6 +435,21 @@ function Database.New(Logger)
end
end
-- Checks for missing briefings in mission zones and random mission zones
do
for _, missionZone in pairs(self._tables.MissionZones) do
if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then
MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing")
end
end
for _, missionZone in pairs(self._tables.RandomMissionZones) do
if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then
MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing")
end
end
end
if missions == 0 then missions = 1 end
self._logger:info("initiated the database with amount of zones: ")
+23
View File
@@ -50,6 +50,19 @@ do
table.insert(OnStageNumberChangedHandlers, handler)
end
---@class OnStageNumberChangeCompleteListener
---@field OnStageNumberChangeComplete fun(self:OnStageNumberChangeCompleteListener, number:integer)
local OnStageNumberChangeCompleteListeners = {}
---@param listener OnStageNumberChangeCompleteListener
SpearheadEvents.AddStageNumberChangeCompleteListener = function(listener)
if type(listener) ~= "table" or type(listener.OnStageNumberChangeComplete) ~= "function" then
warn("Event handler not of type table/object with function OnStageNumberChangeComplete(self, number)")
return
end
table.insert(OnStageNumberChangeCompleteListeners, listener)
end
---@param newStageNumber number
SpearheadEvents.PublishStageNumberChanged = function(newStageNumber)
pcall(function ()
@@ -71,6 +84,16 @@ do
logError(err)
end
end
for _, callable in pairs(OnStageNumberChangeCompleteListeners) do
local succ, err = pcall(function()
callable:OnStageNumberChangeComplete(newStageNumber)
end)
if err then
logError(err)
end
end
Logger.new("Events", "INFO"):info("Published stage number changed to: " .. tostring(newStageNumber))
end
end
+6 -6
View File
@@ -92,7 +92,7 @@ do --setup route util
action = {
id = "Script",
params = {
command = "pcall(Spearhead.Events.PublishOnStation, \"" .. groupName .. "\")"
command = "pcall(GlobalCapCallBacks.PublishOnStation, \"" .. groupName .. "\")"
}
}
}
@@ -122,7 +122,7 @@ do --setup route util
},
stopCondition = {
duration = durationBefore10,
condition = "return Spearhead.DcsUtil.IsBingoFuel(\"" .. groupName .. "\", 0.10)",
condition = "return GlobalCapCallBacks.IsBingoFuel(\"" .. groupName .. "\", 0.10)",
}
}
},
@@ -135,7 +135,7 @@ do --setup route util
action = {
id = "Script",
params = {
command = "pcall(Spearhead.Events.PublishRTBInTen, \"" .. groupName .. "\")"
command = "pcall(GlobalCapCallBacks.PublishRTBInTen, \"" .. groupName .. "\")"
}
}
}
@@ -156,7 +156,7 @@ do --setup route util
},
stopCondition = {
duration = durationAfter10,
condition = "return Spearhead.DcsUtil.IsBingoFuel(\"" .. groupName .. "\")",
condition = "return GlobalCapCallBacks.IsBingoFuel(\"" .. groupName .. "\", 0.10)",
}
}
},
@@ -169,7 +169,7 @@ do --setup route util
action = {
id = "Script",
params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")"
command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
}
}
}
@@ -375,7 +375,7 @@ do --setup route util
action = {
id = "Script",
params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" ..
command = "pcall(GlobalCapCallBacks.PublishRTB, \"" ..
groupName .. "\")"
}
}
@@ -1,11 +1,13 @@
local Events = require("classes.spearhead_events")
local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
local Logger = require("classes.util.Logger")
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
local ExtraStage = require("classes.stageClasses.Stages.ExtraStage")
local PrimaryStage = require("classes.stageClasses.Stages.PrimaryStage")
local WaitingStage = require("classes.stageClasses.Stages.WaitingStage")
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
local StagesByName = {}
@@ -20,10 +22,11 @@ local WaitingStagesByIndex = {}
local currentStage = -99
---@class GlobalStageManager : StageCompleteListener, OnStageChangedListener
---@class GlobalStageManager : StageCompleteListener, OnStageChangedListener, OnStageNumberChangeCompleteListener
---@field private database Database
---@field private logger Logger
---@field private stageConfig StageConfig
---@field private _missionCommandsHelper MissionCommandsHelper
local GlobalStageManager = {}
GlobalStageManager.__index = GlobalStageManager
@@ -41,6 +44,7 @@ function GlobalStageManager.NewAndStart(database, stageConfig, logLevel, spawnMa
local self = setmetatable({}, GlobalStageManager)
self.database = database
self.stageConfig = stageConfig
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
self.logger = logger
if stageConfig.isAutoStages ~= true then
@@ -214,14 +218,34 @@ function GlobalStageManager:OnStageNumberChanged(stageNumber)
self:UpdateDrawings(stageNumber)
end
function GlobalStageManager:OnStageNumberChangeComplete(stageNumber)
self.logger:debug("Stage number change complete to: " .. tostring(stageNumber))
local groups = {}
for _, player in pairs(DcsUtil.getAllPlayerUnits()) do
local group = player:getGroup()
if group then
groups[group:getID()] = group
end
end
for _, group in pairs(groups) do
self._missionCommandsHelper:OverviewToGroup(group:getID())
end
end
---@private
function GlobalStageManager:UpdateDrawings(stageNumber)
self.logger:debug("Updating custom drawings for stage number: " .. tostring(stageNumber))
local drawings = self.database:getCustomDrawings()
for _, drawing in pairs(drawings) do
local startStage, stopStage = drawing:GetStartAndStop()
if stageNumber >= startStage and stageNumber < stopStage then
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
@@ -2,6 +2,8 @@ local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTracker")
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
---@class SupplyHub
---@field private _database Database
@@ -12,7 +14,7 @@ local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionComma
---@field private _isCommmandAdded table<string, boolean>
---@field private _missionCommandsHelper MissionCommandsHelper
---@field private _inZone table<string, boolean>
---@field private _drawID number
---@field private _customDrawing CustomDrawing?
---@field private _cargoInUnits table<table<string, number>>
---@field private _activeAtStart boolean
---@field private _active boolean
@@ -30,6 +32,7 @@ function SupplyHub.new(database, logger, zoneName)
self._database = database
self._logger = logger
self._zoneName = zoneName
self._customDrawing = nil
local split = Util.split_string(zoneName, "_")
if string.lower(split[2]) == "a" then
@@ -40,9 +43,9 @@ function SupplyHub.new(database, logger, zoneName)
self._zone = DcsUtil.getZoneByName(zoneName)
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate(logger.LogLevel)
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate()
self._inZone = {}
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(logger.LogLevel)
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
self._logger:debug("Creating Supply Hub zone: " .. self._zoneName)
@@ -72,13 +75,13 @@ function SupplyHub:Activate()
self._logger:debug("Activating Supply Hub zone: " .. self._zoneName)
local zone = DcsUtil.getZoneByName(self._zoneName)
if zone and self._drawID == nil then
---@type DrawColor
local fillColor = { r=0, g=1, b=0, a=0.2 }
---@type DrawColor
local lineColor = { r=0, g=1, b=0, a=1}
if zone and self._customDrawing == nil then
local fillColor = DrawingHelper.ColorTableToColorString({ 0, 1, 0, 0.2 })
local lineColor = DrawingHelper.ColorTableToColorString({ 0, 1, 0, 1})
local lineStyle = 1
self._drawID = DcsUtil.DrawZone(zone, lineColor, fillColor, lineStyle)
self._customDrawing = CustomDrawing.FromZone(zone, lineColor, fillColor, lineStyle, 6)
self._customDrawing:Draw()
end
self._supplyUnitsTracker:RegisterHub(self)
@@ -11,6 +11,8 @@ local StageBase = require("classes.stageClasses.SpecialZones.StageBase")
local BlueSam = require("classes.stageClasses.SpecialZones.BlueSam")
local Events = require("classes.spearhead_events")
local GlobalCapManager = require("classes.capClasses.GlobalCapManager")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
---@alias StageColor
---| "RED"
@@ -43,17 +45,15 @@ local GlobalCapManager = require("classes.capClasses.GlobalCapManager")
--- @field zoneName string
--- @field stageName string?
--- @field stageNumber number
--- @field protected _currentStageState CurrentStageState
--- @field protected _missionCommandsHelper MissionCommandsHelper
--- @field protected _isActive boolean
--- @field protected _isComplete boolean
--- @field protected _missionPriority MissionPriority
--- @field protected _database Database
--- @field protected _db StageData
--- @field protected _logger Logger
--- @field protected _preActivated boolean
--- @field protected _activeStage integer
--- @field protected _stageConfig StageConfig
--- @field protected _stageDrawingId integer
--- @field protected _customDrawing CustomDrawing
--- @field protected _spawnedGroups Array<string>
--- @field protected _stageCompleteListeners Array<StageCompleteListener>
--- @field protected CheckContinuousAsync fun(self:Stage, time:number) : number?
@@ -63,13 +63,20 @@ local Stage = {}
Stage.__index = Stage
---@enum CurrentStageState
Stage.CurrentStageState = {
INACTIVE = 0,
PREACTIVATED = 1,
ACTIVE = 2,
BLUE = 3
}
Stage.StageColors = {
INVISIBLE = { r=0, g=0, b=0, a=0 },
RED_ACTIVE = { r=1, g=0, b=0, a=0.15 },
RED_PREACTIVE = { r=1, g=0, b=0, a=0.10},
BLUE = { r=0, g=0, b=1, a=0.10},
GRAY = { r=80/255, g=80/255, b=80/255, a=0.10 }
INVISIBLE = { 0, 0, 0, 0 },
RED_ACTIVE = { 1, 0, 0, 0.20 },
RED_PREACTIVE = { 1, 0, 0, 0.05},
BLUE = { 0, 0, 1, 0.10},
GRAY = { 80/255, 80/255, 80/255, 0.10 }
}
---comment
@@ -83,12 +90,11 @@ Stage.StageColors = {
function Stage:superNew(database, stageConfig, logger, initData, missionPriority, spawnManager)
logger:debug("[BaseStage] Initiating stage with name: " .. initData.stageZoneName)
self._currentStageState = Stage.CurrentStageState.INACTIVE
self.zoneName = initData.stageZoneName
self.stageNumber = initData.stageNumber
self._isActive = false
self._isComplete = false
self.stageName = initData.stageDisplayName
self._currentStageState = Stage.CurrentStageState.INACTIVE
self.OnPostStageComplete = nil
self.OnPostBlueActivated = nil
@@ -110,13 +116,19 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
}
self._activeStage = -99
self._preActivated = false
self._stageConfig = stageConfig or {}
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(logger.LogLevel)
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
local zone = DcsUtil.getZoneByName(self.zoneName)
if zone then
self._stageDrawingId = DcsUtil.DrawZone(zone, Stage.StageColors.INVISIBLE, Stage.StageColors.INVISIBLE, 4)
local colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE)
local fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE)
local customDrawing = CustomDrawing.FromZone(zone, colorString, fillColorString, 1, 5)
if customDrawing then
self._customDrawing = customDrawing
end
end
self._spawnedGroups = {}
@@ -253,6 +265,7 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
end
end
Events.AddStageNumberChangedListener(self)
return self
@@ -260,7 +273,7 @@ end
---@return boolean
function Stage:IsComplete()
if self._isComplete == true then return true end
if self._currentStageState == Stage.CurrentStageState.BLUE then return true end
for i, mission in pairs(self._db.sams) do
local state = mission:getState()
@@ -276,13 +289,13 @@ function Stage:IsComplete()
end
end
self._isComplete = true
self._currentStageState = Stage.CurrentStageState.BLUE
return true
end
---@return boolean
function Stage:IsActive()
return self._isActive == true
return self._currentStageState == Stage.CurrentStageState.ACTIVE
end
---comment
@@ -367,10 +380,9 @@ function Stage:AddStageCompleteListener(listener)
end
---Activates all SAMS, Airbase units etc all at once.
---@param draw boolean
function Stage:PreActivate(draw)
if self._preActivated == false then
self._preActivated = true
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()
@@ -382,40 +394,60 @@ function Stage:PreActivate(draw)
end
end
if draw == true then
self:MarkStage(Stage.StageColors.RED_PREACTIVE)
end
self:MarkStage()
end
---@param stageColor DrawColor
function Stage:MarkStage(stageColor)
local lineColor = { r=stageColor.r, g=stageColor.g, b=stageColor.b, a=stageColor.a }
local fillColor = { r=stageColor.r, g=stageColor.g, b=stageColor.b, a=stageColor.a }
function Stage:MarkStage()
if stageColor.a > 0 then
lineColor.a = 1
self._logger:debug("Marking stage '" .. Util.toString(self.zoneName) .. "' with state: " .. self._currentStageState)
if self._customDrawing then
self._customDrawing:Remove()
end
if stageColor == Stage.StageColors.RED_PREACTIVE then
lineColor.a = 0
if self._stageConfig.isDrawStagesEnabled == false then return end
if self._stageConfig.isDrawPreActivatedEnabled == false and self._currentStageState == Stage.CurrentStageState.PREACTIVATED then
return
end
if self._stageDrawingId and self._stageConfig.isDrawStagesEnabled == true then
DcsUtil.SetLineColor(self._stageDrawingId, lineColor)
DcsUtil.SetFillColor(self._stageDrawingId, fillColor)
if self._customDrawing then
self._customDrawing:UpdateDrawingObject(function(drawingObject)
local drawing = drawingObject --[[@as Polygon]]
if self._currentStageState == Stage.CurrentStageState.ACTIVE then
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_ACTIVE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_ACTIVE)
drawing.style = "dot dash"
elseif self._currentStageState == Stage.CurrentStageState.PREACTIVATED then
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_PREACTIVE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_PREACTIVE)
drawing.style = "no line"
elseif self._currentStageState == Stage.CurrentStageState.BLUE then
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.BLUE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.BLUE)
drawing.style = "two dash"
else
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE)
drawing.style = "no line"
end
return drawing
end)
self._customDrawing:Draw()
end
end
function Stage:ActivateStage()
self._isActive = true;
self._currentStageState = Stage.CurrentStageState.ACTIVE
pcall(function()
self:MarkStage(Stage.StageColors.RED_ACTIVE)
self:PreActivate()
pcall(function()
self:MarkStage()
end)
self:PreActivate(false)
self._logger:debug("Activating Misc groups for zone. Count: " .. Util.tableLength(self._db.miscGroups))
for _, miscGroup in pairs(self._db.miscGroups) do
miscGroup:Spawn()
@@ -462,9 +494,9 @@ function Stage:OnStageNumberChanged(number)
if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then
self._logger:debug("Pre-activating stage: " .. self.zoneName .. " with number: " .. number)
self:PreActivate(true)
self:PreActivate()
elseif GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
self:PreActivate(false)
self:PreActivate()
end
if number == self.stageNumber then
@@ -559,7 +591,7 @@ end
function Stage:ActivateBlueStage()
self._logger:debug("Setting stage '" .. Util.toString(self.zoneName) .. "' to blue")
self._currentStageState = Stage.CurrentStageState.BLUE
for _, mission in pairs(self._db.missions) do
mission:SpawnPersistedState()
end
@@ -575,7 +607,7 @@ function Stage:ActivateBlueStage()
---@param self Stage
local ActivateBlueAsync = function(self)
pcall(function()
self:MarkStage(Stage.StageColors.BLUE)
self:MarkStage()
end)
self:ActivateBlueGroups()
@@ -23,7 +23,7 @@ function ExtraStage.New(database, stageConfig, logger, initData, spawnManager)
self.OnPostBlueActivated = function (selfStage)
selfStage:MarkStage(Stage.StageColors.GRAY)
selfStage:MarkStage()
end
self.OnPostStageComplete = function (selfStage)
@@ -34,7 +34,7 @@ function ExtraStage.New(database, stageConfig, logger, initData, spawnManager)
end
---comment
---@param self Stage
---@param self ExtraStage
---@param number integer
function ExtraStage:OnStageNumberChanged(number)
@@ -47,16 +47,16 @@ function ExtraStage:OnStageNumberChanged(number)
if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then
self._logger:debug("Pre-activating stage: " .. self.zoneName .. " with number: " .. number)
self:PreActivate(true)
self:PreActivate()
elseif GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
self:PreActivate(false)
self:PreActivate()
end
if number == self.stageNumber then
self:ActivateStage()
end
if self._isComplete == true then
if self._currentStageState == Stage.CurrentStageState.BLUE then
self:ActivateBlueStage()
end
@@ -1,23 +1,28 @@
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
local Util = require("classes.util.Util")
local Logger = require("classes.util.Logger")
local drawingLogger = Logger.new("CustomDrawing")
---@class CustomDrawing
---@field private _id integer?
---@field private _drawingObject DrawingObject
---@field private _startingStage number
---@field private _removeAtStage number
---@field private _logger Logger
local CustomDrawing = {}
CustomDrawing.__index = CustomDrawing
---@param drawingObject DrawingObject
---@param id integer?
---@return CustomDrawing
function CustomDrawing.New(drawingObject, id)
function CustomDrawing.New(drawingObject)
local self = setmetatable({}, CustomDrawing)
self._drawingObject = drawingObject
self._id = id
self._id = nil -- initiate ID as nil, when drawn it will be set to the ID returned by DrawingHelper.Draw
self._logger = drawingLogger
local name = drawingObject.name
local split = Util.split_string(name or "", "_")
@@ -28,6 +33,63 @@ function CustomDrawing.New(drawingObject, id)
return self
end
---@param zone SpearheadTriggerZone
---@param colorString string
---@param fillColorString string
---@param lineStyle LineType
---@param lineThickness number
---@return CustomDrawing?
function CustomDrawing.FromZone(zone, colorString, fillColorString, lineStyle, lineThickness)
if zone == nil then
drawingLogger:warn("CustomDrawing.FromZone called with nil zone")
return nil
end
if zone.zone_type == "Cilinder" then
---@type Circle
local drawingObject = {
mapX = zone.location.x,
mapY = zone.location.y,
radius = zone.radius,
name = zone.name .. "_drawing",
primitiveType = "Polygon",
polygonMode = "circle",
visible = true,
style = DrawingHelper.ToLineStyleString(lineStyle),
colorString = colorString,
fillColorString = fillColorString,
thickness = lineThickness,
}
return CustomDrawing.New(drawingObject)
end
if zone.zone_type == "Polygon" then
---@type Free
local drawingObject = {
mapX = 0,
mapY = 0,
points = zone.verts,
name = zone.name .. "_drawing",
primitiveType = "Polygon",
polygonMode = "free",
visible = true,
style = DrawingHelper.ToLineStyleString(lineStyle),
colorString = colorString,
fillColorString = fillColorString,
thickness = lineThickness,
}
return CustomDrawing.New(drawingObject)
end
end
---@return string
function CustomDrawing:GetName()
return self._drawingObject.name
end
---@return number start
---@return number stop
function CustomDrawing:GetStartAndStop()
@@ -35,14 +97,26 @@ function CustomDrawing:GetStartAndStop()
end
function CustomDrawing:Draw()
self._id = DrawingHelper.Draw(self._drawingObject)
self._logger:debug("Drawing custom drawing with ID " .. tostring(self._id))
if self._id ~= nil then
DrawingHelper.Remove(self._id)
end
self._id = DrawingHelper.Draw(self._drawingObject, self._logger)
end
function CustomDrawing:Remove()
if self._id ~= nil then
self._logger:debug("Removing custom drawing with ID " .. tostring(self._id))
DrawingHelper.Remove(self._id)
self._id = nil
end
end
---@param updateFunc fun(drawingObject:DrawingObject):DrawingObject
function CustomDrawing:UpdateDrawingObject(updateFunc)
self._drawingObject = updateFunc(self._drawingObject)
end
return CustomDrawing
@@ -1,25 +1,49 @@
local Util = require("classes.util.Util")
local Logger = require("classes.util.Logger")
local GlobalConfig = require("classes.configuration.GlobalConfig")
---@type LogLevel
local level = "INFO"
if GlobalConfig:isDebugEnabled() then
level = "DEBUG"
end
local drawingLogger = Logger.new("DrawingHelper", level)
---@class DrawingHelper
local DrawingHelper = {}
DrawingHelper.__index = DrawingHelper
local customDrawingIdIncrementer = 4210
---@param object DrawingObject
---@param logger Logger?
---@return integer? id
function DrawingHelper.Draw(object)
function DrawingHelper.Draw(object, logger)
if object == nil then
if logger then
logger:warn("DrawingHelper.Draw called with nil object")
end
return nil
end
local id = DrawingHelper.GetAndAddId()
if logger then
logger:debug("Drawing object with ID " .. tostring(id) .. " and primitive type: " .. tostring(object.primitiveType))
end
if(object.primitiveType == "Polygon") then
DrawingHelper.DrawPolygon(object--[[@as Polygon]], id)
DrawingHelper.DrawPolygon(object--[[@as Polygon]], id, logger)
elseif(object.primitiveType == "Line") then
DrawingHelper.DrawLine(object--[[@as Line]], id)
DrawingHelper.DrawLine(object--[[@as Line]], id, logger)
elseif(object.primitiveType == "TextBox") then
DrawingHelper.DrawTextBox(object--[[@as TextBox]], id)
else
if logger then
logger:warn("Unknown primitive type: " .. tostring(object.primitiveType))
end
end
return id
@@ -36,16 +60,25 @@ end
---@param points Array<Vec3>
---@param fillColor table
---@param lineColor table
local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineStyle)
---@param lineStyle LineType
---@param lineThickness number
---@param logger Logger?
local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineStyle, lineThickness, logger)
if lineThickness == nil or lineThickness <= 0 then
lineStyle = 0
end
local functionString = "trigger.action.markupToAll(" .. shapeID .. ", -1, " .. drawID .. ","
for _, point in ipairs(points) do
for _, point in pairs(points) do
functionString = functionString .. " { x=" .. point.x .. ", y=0,z=" .. point.z .. "},"
end
functionString = functionString ..
"{ " .. lineColor[1] .. "," .. lineColor[2] .. "," .. lineColor[3] .. "," .. lineColor[4] .. "}, " ..
"{ " .. fillColor[1] .. "," .. fillColor[2] .. "," .. fillColor[3] .. "," .. fillColor[4] .. "}, " ..
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)
end
---@diagnostic disable-next-line: deprecated
local f, err = loadstring(functionString)
@@ -55,12 +88,21 @@ local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineS
env.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
trigger.action.setMarkupColorFill(drawID, fillColor)
trigger.action.setMarkupColor(drawID, lineColor)
trigger.action.setMarkupTypeLine(drawID, lineStyle)
end
---@private
---@param object Polygon
---@param id integer
function DrawingHelper.DrawPolygon(object, id)
---@param logger Logger?
function DrawingHelper.DrawPolygon(object, id, logger)
if object == nil then
return
end
@@ -75,11 +117,12 @@ function DrawingHelper.DrawPolygon(object, id)
end
---@param oval Oval
local function DrawOval(oval)
---@param logger Logger?
local function DrawOval(oval, logger)
---@type Array<Vec3>
local points = {}
local pointsNo = 30
local angleStep = (2 * math.pi) / points
local angleStep = (2 * math.pi) / pointsNo
local fillColor = DrawingHelper.ColorToColorTable(oval.fillColorString)
local color = DrawingHelper.ColorToColorTable(oval.colorString)
@@ -91,20 +134,36 @@ function DrawingHelper.DrawPolygon(object, id)
local y = oval.mapY + (oval.r2 * math.sin(angle))
table.insert(points, { x = x, y = 0, z = y } )
end
MarkupToAll(7, id, points, fillColor, color, lineStyle)
MarkupToAll(7, id, points, fillColor, color, lineStyle, oval.thickness, logger)
end
---@param free Free
local function DrawFree(free)
---@param logger Logger?
local function DrawFree(free, logger)
local fillColor = DrawingHelper.ColorToColorTable(free.fillColorString)
local color = DrawingHelper.ColorToColorTable(free.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(free.style)
local points = {}
for _, point in ipairs(free.points) do
table.insert(points, { x = point.x, y = 0, z = point.y } )
local keys = {}
for k, _ in pairs(free.points) do
table.insert(keys, k)
end
MarkupToAll(7, id, points, fillColor, color, lineStyle)
table.sort(keys, function(a, b) return a < b end)
local points = {}
for _, k in ipairs(keys) do
local point = free.points[k]
local newPoint = { x = free.mapX + point.x, y = 0, z = free.mapY + point.y }
local firstPoint = points[1]
if firstPoint == nil or newPoint.x ~= firstPoint.x or newPoint.z ~= firstPoint.z then
table.insert(points, newPoint)
end
end
MarkupToAll(7, id, points, fillColor, color, lineStyle, free.thickness, logger)
end
---@param rect Rect
@@ -116,38 +175,49 @@ function DrawingHelper.DrawPolygon(object, id)
local pointA = { x = rect.mapX, y = 0, z = rect.mapY }
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)
end
---@param arrow Arrow
local function DrawArrow(arrow)
---@param logger Logger?
local function DrawArrow(arrow, logger)
local fillColor = DrawingHelper.ColorToColorTable(arrow.fillColorString)
local color = DrawingHelper.ColorToColorTable(arrow.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(arrow.style)
local startPoint = { x = arrow.mapX, y = 0, z = arrow.mapY }
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))
end
local endPoint = { x = arrow.mapX, y = 0, z = arrow.mapY }
local rad = math.rad(arrow.angle or 0)
local length = arrow.length or 100
local endPoint = { x = arrow.mapX + length * math.cos(rad), y = 0, z = arrow.mapY + length * math.sin(rad) }
local startPoint = { x = arrow.mapX - length * math.sin(rad), y = 0, z = arrow.mapY + length * math.cos(rad) }
trigger.action.arrowToAll(-1, id, startPoint, endPoint, color, fillColor, lineStyle, true)
end
if logger then
logger:debug("Drawing polygon with ID " .. tostring(id) .. " and polygon mode: " .. tostring(object.polygonMode))
end
if object.polygonMode == "circle" then
DrawCircle(object--[[@as Circle]])
elseif object.polygonMode == "oval" then
DrawOval(object--[[@as Oval]])
DrawOval(object--[[@as Oval]], logger)
elseif object.polygonMode == "free" then
DrawFree(object--[[@as Free]])
DrawFree(object--[[@as Free]], logger)
elseif object.polygonMode == "rect" then
DrawRect(object--[[@as Rect]])
elseif object.polygonMode == "arrow" then
DrawArrow(object--[[@as Arrow]])
DrawArrow(object--[[@as Arrow]], logger)
end
end
---@private
---@param object Line
---@param id integer
function DrawingHelper.DrawLine(object, id)
---@param logger Logger?
function DrawingHelper.DrawLine(object, id, logger)
---@type Array<Vec3>
local points = {}
@@ -158,7 +228,7 @@ function DrawingHelper.DrawLine(object, id)
local color = DrawingHelper.ColorToColorTable(object.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(object.style)
MarkupToAll(1, id, points, color, color, lineStyle)
MarkupToAll(1, id, points, color, color, lineStyle, object.thickness, logger)
end
---@private
@@ -177,20 +247,41 @@ function DrawingHelper.Remove(id)
trigger.action.removeMark(id)
end
---@param hexStr string
---@return table
function DrawingHelper.ColorToColorTable(hexStr)
if hexStr == nil then
drawingLogger:warn("ColorToColorTable called with nil hexStr, returning default color {0, 0, 0, 0}")
return { 0, 0, 0, 0 }
end
hexStr = hexStr:gsub("0x", "")
local a = tonumber(hexStr:sub(1, 2), 16) / 255
local r = tonumber(hexStr:sub(3, 4), 16) / 255
local g = tonumber(hexStr:sub(5, 6), 16) / 255
local b = tonumber(hexStr:sub(7, 8), 16) / 255
local r = tonumber(hexStr:sub(1, 2), 16) / 255
local g = tonumber(hexStr:sub(3, 4), 16) / 255
local b = tonumber(hexStr:sub(5, 6), 16) / 255
local a = tonumber(hexStr:sub(7, 8), 16) / 255
return { r, g , b , a }
end
---@param rgba table
---@return string
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
drawingLogger:warn("ColorTableToColorString called with invalid rgba table, returning default color string '0x00000000'")
return "0x00000000"
end
local r = string.format("%02X", math.floor(rgba[1] * 255))
local g = string.format("%02X", math.floor(rgba[2] * 255))
local b = string.format("%02X", math.floor(rgba[3] * 255))
local a = string.format("%02X", math.floor(rgba[4] * 255))
return "0x" .. r .. g .. b .. a
end
---@param lineStyle string
function DrawingHelper.ToLineStyleInteger(lineStyle)
lineStyle = lineStyle:lower()
@@ -213,6 +304,26 @@ function DrawingHelper.ToLineStyleInteger(lineStyle)
end
end
function DrawingHelper.ToLineStyleString(lineStyle)
if lineStyle == 0 then
return "no line"
elseif lineStyle == 1 then
return "solid"
elseif lineStyle == 2 then
return "dashed"
elseif lineStyle == 3 then
return "dotted"
elseif lineStyle == 4 then
return "dot dash"
elseif lineStyle == 5 then
return "long dash"
elseif lineStyle == 6 then
return "two dash"
else
return "no line"
end
end
---@class ARGB
---@field public a number
---@field public r number
@@ -137,10 +137,6 @@ function BattleManager:LetUnitsShoot(groups, targetGroups)
}
}
if debugDrawing == true then
self:DrawDebugLine(point, unit)
end
local controller = unit:getController()
if controller then
controller:setTask(shootTask)
@@ -245,42 +241,8 @@ function BattleManager:GetRandomPoint(origin, groupHulls)
if not hull then return nil end
local shootPoints = Util.GetTangentHullPointsFromOrigin(hull, origin)
if debugDrawing == true then
self:DrawDebugZone({ hull })
end
return Util.randomFromList(shootPoints) --[[@as Vec2]]
end
do --DEBUG
---@param unit Unit
---@param target Vec2
function BattleManager:DrawDebugLine(target, unit)
local color = {r = 1, g = 0, b = 0, a = 1}
if unit:getCoalition() == 2 then
color = {r = 0, g = 0, b = 1, a = 1}
end
DcsUtil.DrawLine(unit:getPoint(), {x = target.x, y = 0, z = target.y}, color, 1)
end
---@param hulls Array<Array<Vec2>>
function BattleManager:DrawDebugZone(hulls)
for _, drawHull in pairs(hulls) do
---@type SpearheadTriggerZone
local zone = {
name = "temp",
zone_type = "Polygon",
radius = 0,
verts = drawHull,
location = { x=drawHull[1].x, y=drawHull[1].y },
}
DcsUtil.DrawZone(zone, {r =0, g=0, b =1, a = 0.5} ,{r =0, g= 0, b =1, a = 0}, 1)
end
end
end --DEBUG
return BattleManager
@@ -1,20 +0,0 @@
---@class MaxLoadConfig
---@field maxInternalLoad number
---@type table<string, MaxLoadConfig>
local MaxLoadConfig = {
["Mi-8MT"] = {
maxInternalLoad = 4000,
},
["CH-47Fbl1"] = {
maxInternalLoad = 10000
},
["Mi-24P"] = {
maxInternalLoad = 2000
},
["UH-1H"] = {
maxInternalLoad = 2000
}
}
return MaxLoadConfig
@@ -4,6 +4,7 @@ local Logger = require("classes.util.Logger")
local SpearheadEvents = require("classes.spearhead_events")
local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTracker")
local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper")
local StageConfig = require("classes.configuration.StageConfig")
---@class MissionCommandsHelper
@@ -14,9 +15,9 @@ local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHel
---@field updateContinuous fun(self: MissionCommandsHelper, time: number): number @function to update commands continuously
---@field pinnedByGroup table<string, Mission> @table of pinned missions by group ID
---@field private _stageBriefings table<string, string> @table of stage briefings by stage name
---@field private _supplyHubGroups table<string, boolean> @table of supply hub groups by their ID
---@field private _logger Logger @logger instance for logging
---@field private _supplyUnitsTracker SupplyUnitsTracker @supply units tracker instance
---@field private _stageConfig StageConfig
local MissionCommandsHelper = {}
MissionCommandsHelper.__index = MissionCommandsHelper
@@ -24,8 +25,8 @@ MissionCommandsHelper.__index = MissionCommandsHelper
---@param groupPos Vec2
local function sortMissions(list, groupPos)
table.sort(list, function(a, b)
local distA = Util.VectorDistance2d(groupPos, a.location or {x=0, y=0})
local distB = Util.VectorDistance2d(groupPos, b.location or {x=0, y=0})
local distA = Util.VectorDistance2d(groupPos, a.location or { x = 0, y = 0 })
local distB = Util.VectorDistance2d(groupPos, b.location or { x = 0, y = 0 })
return distA < distB;
end)
end
@@ -35,12 +36,11 @@ local id = 0
local instance = nil
---@return MissionCommandsHelper
---@param logLevel string @log level for the logger
function MissionCommandsHelper.getOrCreate(logLevel)
function MissionCommandsHelper.getOrCreate()
if instance == nil then
instance = setmetatable({}, MissionCommandsHelper)
instance._logger = Logger.new("MissionCommandsHelper", logLevel)
instance._logger = Logger.new("MissionCommandsHelper")
instance._logger:info("Creating MissionCommandsHelper instance")
@@ -49,10 +49,29 @@ function MissionCommandsHelper.getOrCreate(logLevel)
instance.updateNeeded = false
instance.pinnedByGroup = {}
instance.lastUpdate = 0
instance._supplyHubGroups = {}
instance._stageBriefings = {}
instance._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate(logLevel)
instance._stageConfig = StageConfig:getInstance()
instance._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate()
instance._supplyUnitsTracker:AddOnSupplyUnitEventListener(
{
enteredSupplyHub = function(self, unit)
if unit == nil then return end
instance.updateNeeded = true
instance:updateCommandsForGroup(unit:getGroup():getID())
end,
exitedSupplyHub = function(self, unit)
instance.updateNeeded = true
instance:updateCommandsForGroup(unit:getGroup():getID())
end,
supplyUnitSpawned = function(self, unit)
instance.updateNeeded = true
instance:updateCommandsForGroup(unit:getGroup():getID())
end
}
)
---comment
---@param selfA MissionCommandsHelper
@@ -79,7 +98,6 @@ function MissionCommandsHelper.getOrCreate(logLevel)
timer.scheduleFunction(instance.updateContinuous, instance, timer.getTime() + 5)
SpearheadEvents.AddOnPlayerEnterUnitListener(instance)
end
return instance
@@ -108,38 +126,16 @@ function MissionCommandsHelper:RemoveMissionToCommands(mission)
self.updateNeeded = true
end
---@param groupID number
function MissionCommandsHelper:MarkUnitInSupplyHub(groupID)
self._logger:debug("Marking unit in supply hub: " .. tostring(groupID))
local updateNeeded = false
if self._supplyHubGroups[tostring(groupID)] ~= true then
updateNeeded = true
end
self._supplyHubGroups[tostring(groupID)] = true
if updateNeeded == true then self:updateCommandsForGroup(groupID) end
end
---@param groupID number
function MissionCommandsHelper:MarkUnitOutsideSupplyHub(groupID)
self._logger:debug("Marking unit outide supply hub: " .. tostring(groupID))
local updateNeeded = false
if self._supplyHubGroups[tostring(groupID)] == true then
updateNeeded = true
end
self._supplyHubGroups[tostring(groupID)] = false
if updateNeeded == true then self:updateCommandsForGroup(groupID) end
end
---@param unit Unit
function MissionCommandsHelper:OnPlayerEntersUnit(unit)
if unit then
local group = unit:getGroup()
if group then self:updateCommandsForGroup(group:getID()) end
if group then
self:updateCommandsForGroup(group:getID())
if self._stageConfig.briefingOnSpawnEnabled == true then
self:OverviewToGroup(group:getID())
end
end
end
end
@@ -174,111 +170,116 @@ local pinMissionCommand = function(args)
end
end
---@param groupID integer
function MissionCommandsHelper:OverviewToGroup(groupID)
local text = "Missions Overview\n\n"
local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
---@type Vec2
local groupPos = { x = 0, y = 0 }
if group then
local pos = group:getUnit(1):getPosition().p
groupPos = { x = pos.x, y = pos.z }
end
---@param mission Mission
---@return string
local function formatLine(mission)
local distanceText = "?"
if group then
local lead = group:getUnit(1)
if lead and lead:isExist() == true then
local pos = lead:getPoint()
local Vec2Pos = { x = pos.x, y = pos.z }
local distance = Util.VectorDistance2d(Vec2Pos, mission.location) / 1852
distanceText = string.format("~%d", math.floor(distance))
end
end
return string.format("[%s]\t%s \t%s \t%s %% \t%s nM\n", mission.code, mission.missionTypeDisplay, mission.name,
mission:PercentageComplete(), distanceText)
end
for _, briefing in pairs(self._stageBriefings) do
text = text .. briefing .. "\n\n"
end
---Primary missions
text = text .. "Primary Missions\n"
---@type Array<Mission>
local primaryMissions = {}
for code, enabled in pairs(self.enabledByCode) do
if enabled == true then
local mission = self.missionsByCode[code]
if mission and mission:getState() == "ACTIVE" and mission.priority == "primary" then
table.insert(primaryMissions, mission)
end
end
end
sortMissions(primaryMissions, groupPos)
for _, mission in pairs(primaryMissions) do
text = text .. formatLine(mission)
end
---Secondary missions
text = text .. "\nSecondary Missions\n"
---@type Array<Mission>
local secondaryMissions = {}
for code, enabled in pairs(self.enabledByCode) do
if enabled == true then
local mission = self.missionsByCode[code]
if mission and mission:getState() == "ACTIVE" and mission.priority == "secondary" then
table.insert(secondaryMissions, mission)
end
end
end
sortMissions(secondaryMissions, groupPos)
for _, mission in pairs(secondaryMissions) do
text = text .. formatLine(mission)
end
trigger.action.outTextForGroup(groupID, text, 20, true)
end
---@private
function MissionCommandsHelper:AddOverviewCommand(groupID)
local MissionsOverviewToGroup = function (id)
local text = "Missions Overview\n\n"
---@class OverviewToGroupCommandArgs
---@field self MissionCommandsHelper @the MissionCommandsHelper instance
---@field groupId integer @the group ID of the player requesting the overview
local group = DcsUtil.GetPlayerGroupByGroupID(id)
---@type Vec2
local groupPos = { x=0, y=0 }
if group then
local pos = group:getUnit(1):getPosition().p
groupPos = { x= pos.x, y=pos.z }
end
---comment
---@param mission Mission
---@return string
local function formatLine(mission)
local distanceText = "?"
if group then
local lead = group:getUnit(1)
if lead and lead:isExist() == true then
local pos = lead:getPoint()
local Vec2Pos = { x= pos.x, y=pos.z }
local distance = Util.VectorDistance2d(Vec2Pos, mission.location) / 1852
distanceText = string.format("~%d", math.floor(distance))
end
end
return string.format("[%s]\t%s \t%s \t%s %% \t%s nM\n", mission.code, mission.missionTypeDisplay, mission.name, mission:PercentageComplete(), distanceText)
end
for _, briefing in pairs(self._stageBriefings) do
text = text .. briefing .. "\n\n"
end
---Primary missions
text = text .. "Primary Missions\n"
---@type Array<Mission>
local primaryMissions = {}
for code, enabled in pairs(self.enabledByCode) do
if enabled == true then
local mission = self.missionsByCode[code]
if mission and mission:getState() == "ACTIVE" and mission.priority == "primary" then
table.insert(primaryMissions, mission)
end
end
end
sortMissions(primaryMissions, groupPos)
for _, mission in pairs(primaryMissions) do
text = text .. formatLine(mission)
end
---Secondary missions
text = text .. "\nSecondary Missions\n"
---@type Array<Mission>
local secondaryMissions = {}
for code, enabled in pairs(self.enabledByCode) do
if enabled == true then
local mission = self.missionsByCode[code]
if mission and mission:getState() == "ACTIVE" and mission.priority == "secondary" then
table.insert(secondaryMissions, mission)
end
end
end
sortMissions(secondaryMissions, groupPos)
for _, mission in pairs(secondaryMissions) do
text = text .. formatLine(mission)
end
trigger.action.outTextForGroup(id, text, 20, true)
---@param args OverviewToGroupCommandArgs
local MissionOverViewToGroup = function(args)
args.self:OverviewToGroup(args.groupId)
end
missionCommands.removeItemForGroup(groupID, { "Overview" } )
missionCommands.addCommandForGroup(groupID, "Overview", nil, MissionsOverviewToGroup, groupID)
---@type OverviewToGroupCommandArgs
local overviewToGroupCommandArgs = { self = self, groupId = groupID }
missionCommands.removeItemForGroup(groupID, { "Overview" })
missionCommands.addCommandForGroup(groupID, "Overview", nil, MissionOverViewToGroup, overviewToGroupCommandArgs)
end
---@private
---@param groupID number
function MissionCommandsHelper:AddPinnedMission(groupID)
local pinndedMission = self.pinnedByGroup[tostring(groupID)]
missionCommands.removeItemForGroup(groupID, { "Pinned Mission" })
if pinndedMission and self.enabledByCode[tostring(pinndedMission.code)] == true then
missionCommands.addCommandForGroup(groupID, "Pinned Mission", nil, missionBriefingRequested, { groupId = groupID, mission = pinndedMission })
missionCommands.addCommandForGroup(groupID, "Pinned Mission", nil, missionBriefingRequested,
{ groupId = groupID, mission = pinndedMission })
end
end
---@param groupID number
function MissionCommandsHelper:updateCommandsForGroup(groupID)
self._logger:debug("Updating commands for group: " .. tostring(groupID))
self:AddPinnedMission(groupID)
@@ -296,15 +297,14 @@ function MissionCommandsHelper:updateCommandsForGroup(groupID)
trigger.action.outTextForGroup(id, "clearing...", 1, true)
end
missionCommands.removeItemForGroup(groupID, { "Clear View" } )
missionCommands.removeItemForGroup(groupID, { "Clear View" })
missionCommands.addCommandForGroup(groupID, "Clear View", nil, clearView, groupID)
missionCommands.removeItemForGroup(groupID, { "Refresh Missions" } )
missionCommands.removeItemForGroup(groupID, { "Refresh Missions" })
missionCommands.addCommandForGroup(groupID, "Refresh Missions", nil, function(refresh_mission_id)
self._logger:debug("Manual refresh of missions for group: " .. tostring(refresh_mission_id))
self:updateCommandsForGroup(refresh_mission_id)
end, groupID)
end
local folderNames = {
@@ -327,17 +327,16 @@ function MissionCommandsHelper:PinMission(mission, groupID)
end
function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID)
local perFolder = 9
local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
---@type Vec2
local groupPos = { x=0, y=0 }
local groupPos = { x = 0, y = 0 }
if group then
local pos = group:getUnit(1):getPosition().p
groupPos = { x= pos.x, y=pos.z }
groupPos = { x = pos.x, y = pos.z }
end
do --- primary missions
local count = 0
local path = { [1] = folderNames.primary }
@@ -363,7 +362,7 @@ function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID)
else
local name = "Next Menu ..."
missionCommands.addSubMenuForGroup(groupID, name, path)
path[#path+1] = name
path[#path + 1] = name
count = 0
end
end
@@ -392,7 +391,7 @@ function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID)
else
local name = "Next Menu ..."
missionCommands.addSubMenuForGroup(groupID, name, path)
path[#path+1] = name
path[#path + 1] = name
count = 0
end
end
@@ -405,28 +404,28 @@ end
---@param path Array<string>
---@param mission Mission
function MissionCommandsHelper:addMissionCommands(groupId, path, mission)
if path then
local group = DcsUtil.GetPlayerGroupByGroupID(groupId)
local distance = "[?]"
if group then
local lead = group:getUnit(1)
if lead and lead:isExist() == true then
local pos = lead:getPoint()
local Vec2Pos = { x= pos.x, y=pos.z }
local Vec2Pos = { x = pos.x, y = pos.z }
local dist = Util.VectorDistance2d(Vec2Pos, mission.location) / 1852
distance = "[" .. string.format("~%dnM", math.floor(dist)) .. "]"
end
end
local missionFolderName = "[" .. mission.code .. "]" .. distance .. mission.name .. "( " .. mission.missionTypeDisplay .. " )"
local missionFolderName = "[" ..
mission.code .. "]" .. distance .. mission.name .. "( " .. mission.missionTypeDisplay .. " )"
missionCommands.addSubMenuForGroup(groupId, missionFolderName, path)
table.insert(path, missionFolderName)
---@type MissionBriefingRequestedArgs
local missionBriefingRequestedArgs = { groupId = groupId, mission = mission }
missionCommands.addCommandForGroup(groupId, "Briefing", path, missionBriefingRequested,missionBriefingRequestedArgs)
missionCommands.addCommandForGroup(groupId, "Briefing", path, missionBriefingRequested,
missionBriefingRequestedArgs)
---@type PinMissionCommandArgs
local pinMissionCommandArgs = { self = self, groupId = groupId, mission = mission }
@@ -437,8 +436,7 @@ end
---@private
---@param groupID integer
function MissionCommandsHelper:AddSupplyHubCommandsIfApplicable(groupID)
if self._supplyHubGroups[tostring(groupID)] ~= true then return end
if self._supplyUnitsTracker:IsGroupLeadInSupplyHub(groupID) ~= true then return end
self._logger:debug("Adding supply hub commands for group: " .. tostring(groupID))
@@ -468,28 +466,32 @@ function MissionCommandsHelper:AddSupplyHubCommandsIfApplicable(groupID)
local path = { [1] = folderNames.supplyHub }
---@type LoadCargoCommandParams
local farpParams1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_1000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self }
local farpParams1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_1000", supplyUnitsTracker =
self._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Load FARP Crate (1000)", path, loadCargoCommand, farpParams1000)
---@type LoadCargoCommandParams
local farpParams2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self }
local farpParams2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_2000", supplyUnitsTracker =
self._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Load FARP Crate (2000)", path, loadCargoCommand, farpParams2000)
---@type LoadCargoCommandParams
local samParms1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self }
local samParms1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker =
self._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Load SAM Crate (1000)", path, loadCargoCommand, samParms1000)
---@type LoadCargoCommandParams
local samParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self }
local samParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker =
self._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Load SAM Crate (2000)", path, loadCargoCommand, samParms2000)
---@type LoadCargoCommandParams
local airbaseParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "AIRBASE_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self }
local airbaseParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "AIRBASE_CRATE_2000", supplyUnitsTracker =
self._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Airbase Crate (2000)", path, loadCargoCommand, airbaseParms2000)
end
function MissionCommandsHelper:AddCargoCommands(groupID)
local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
if group == nil then return end
@@ -519,25 +521,23 @@ function MissionCommandsHelper:AddCargoCommands(groupID)
for i = 1, amount do
local path = { [1] = folderNames.cargo }
---@type UnloadCargoCommandParams
local params = { unitID = unit:getID(), crateType = cargoType, supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Unload " .. cargoConfig.displayName, path, unloadCargoCommand, params)
local params = { unitID = unit:getID(), crateType = cargoType, supplyUnitsTracker = self
._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Unload " .. cargoConfig.displayName, path,
unloadCargoCommand, params)
end
end
end
end
end
---@private
---@param groupId integer
function MissionCommandsHelper:addMissionFolders(groupId)
missionCommands.addSubMenuForGroup(groupId, folderNames.primary)
missionCommands.addSubMenuForGroup(groupId, folderNames.secondary)
if self._supplyHubGroups[tostring(groupId)] == true then
self._logger:debug("Adding supply hub commands folder for group: " .. tostring(groupId))
if self._supplyUnitsTracker:IsGroupLeadInSupplyHub(groupId) == true then
missionCommands.addSubMenuForGroup(groupId, folderNames.supplyHub)
end
@@ -0,0 +1,42 @@
---@class DropZoneSlice
---@field centerAngle number Angle in degrees (0=forward, 90=right, 180=rear, 270=left)
---@field angleWidth number Total width of the slice in degrees (e.g. 60 = ±30°)
---@field minRadius number Minimum search radius in meters (safe distance from helicopter)
---@field maxRadius number Maximum search radius in meters
---@field spacing number Distance increment when searching outward in meters
---@class SupplyLoadConfig
---@field maxInternalLoad number
---@field dropZones Array<DropZoneSlice>
---@type table<string, SupplyLoadConfig>
local SupplyLoadConfig = {
["Mi-8MT"] = {
maxInternalLoad = 4000,
dropZones = {
{ centerAngle = 180, angleWidth = 60, minRadius = 20, maxRadius = 50, spacing = 5 },
}
},
["CH-47Fbl1"] = {
maxInternalLoad = 10000,
dropZones = {
{ centerAngle = 180, angleWidth = 30, minRadius = 20, maxRadius = 75, spacing = 5 },
}
},
["Mi-24P"] = {
maxInternalLoad = 2000,
dropZones = {
{ centerAngle = 270, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
{ centerAngle = 90, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
}
},
["UH-1H"] = {
maxInternalLoad = 2000,
dropZones = {
{ centerAngle = 270, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
{ centerAngle = 90, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
}
}
}
return SupplyLoadConfig
@@ -3,7 +3,7 @@ local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
local SpearheadEvents = require("classes.spearhead_events")
local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper")
local MaxLoadConfig = require("classes.stageClasses.helpers.MaxLoadConfig")
local SupplyLoadConfig = require("classes.stageClasses.helpers.SupplyLoadConfig")
---@class SupplyUnitEventListener
---@field supplyUnitSpawned fun(self:SupplyUnitEventListener, unit:Unit) | nil
@@ -26,13 +26,12 @@ SupplyUnitsTracker.__index = SupplyUnitsTracker
local singleton = nil
---comment
---@param logLevel LogLevel
---@return SupplyUnitsTracker
function SupplyUnitsTracker.getOrCreate(logLevel)
function SupplyUnitsTracker.getOrCreate()
if singleton == nil then
singleton = setmetatable({}, SupplyUnitsTracker)
singleton._logger = Logger.new("SupplyUnitsTracker", logLevel)
singleton._logger = Logger.new("SupplyUnitsTracker")
singleton._unitPositions = {}
singleton._cargoInUnits = {}
singleton._supplyUnitsByName = {}
@@ -94,7 +93,7 @@ end
---@param listener SupplyUnitEventListener
function SupplyUnitsTracker:AddOnSupplyUnitSpawnedListener(listener)
function SupplyUnitsTracker:AddOnSupplyUnitEventListener(listener)
if listener == nil then return end
if self._supplyUnitEventsListeners == nil then
@@ -104,6 +103,26 @@ function SupplyUnitsTracker:AddOnSupplyUnitSpawnedListener(listener)
table.insert(self._supplyUnitEventsListeners, listener)
end
---@param unit Unit
---@return boolean
function SupplyUnitsTracker:IsUnitInSupplyHub(unit)
if unit == nil then return false end
local unitIDStr = tostring(unit:getID())
return self._unitInSupplyHub[unitIDStr] == true
end
---@param groupID number
---@return boolean
function SupplyUnitsTracker:IsGroupLeadInSupplyHub(groupID)
local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
if group == nil then return false end
local unit = group:getUnit(1)
if unit == nil then return false end
return self:IsUnitInSupplyHub(unit)
end
function SupplyUnitsTracker:Update()
local players = DcsUtil.getAllPlayerUnits()
for _, player in pairs(players) do
@@ -211,7 +230,6 @@ function SupplyUnitsTracker:CheckUnitsInZones()
if Util.is3dPointInZone(pos, zone) then
if self._unitInSupplyHub[tostring(unit:getID())] ~= true then
self._unitInSupplyHub[tostring(unit:getID())] = true
for _, listener in pairs(self._supplyUnitEventsListeners) do
pcall(function()
if listener.enteredSupplyHub then
@@ -278,14 +296,15 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe
self._logger:debug("Unload requested for unit: " .. unitID .. " crateType: " .. crateType)
local unit = DcsUtil.GetPlayerUnitByID(unitID)
if unit == nil or unit:isExist() == false then return end
if unit == nil or unit:isExist() == false then
self._logger:warn("Unload requested for non-existent unit: " .. unitID)
return
end
local group = unit:getGroup()
if group == nil then
self._logger:warn("Unload requested for unit with no group: " .. unit:getName())
return
end
self:RemoveCargoFromUnit(unitID, crateType)
self:UpdateWeightForUnit(unit)
local cargoConfig = SupplyConfigHelper.getSupplyConfig(crateType)
@@ -294,7 +313,16 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe
return
end
local cargoPos = self:GetCargoPlacePosition(unit)
local cargoPos = self:GetCargoPlacePosition(unit, cargoConfig.staticType)
if cargoPos == nil then
self._logger:warn("No valid position found to drop cargo for unit: " .. unit:getName())
trigger.action.outTextForUnit(unit:getID(), "No valid position to drop cargo. Unloading area is too crowded.", 10)
return
end
self:RemoveCargoFromUnit(unitID, crateType)
self:UpdateWeightForUnit(unit)
cargoCount = cargoCount + 1
local cargoSpawnObject = {
@@ -304,9 +332,11 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe
y = cargoPos.z,
}
self._logger:debug("Spawning crate #" .. cargoCount .. " at (" .. string.format("%.2f", cargoPos.x) .. ", " .. string.format("%.2f", cargoPos.y) .. ", " .. string.format("%.2f", cargoPos.z) .. ")")
local spawned = coalition.addStaticObject(unit:getCoalition(), cargoSpawnObject)
self._droppedCrates[cargoSpawnObject.name] = spawned
missionCommandsHelper:updateCommandsForGroup(group:getID())
self._logger:debug("Cargo dropped for unit: " .. unit:getName() .. " crateType: " .. crateType)
end
---@return table<string,StaticObject>
@@ -394,7 +424,7 @@ function SupplyUnitsTracker:TryLoadCrateInUnit(unit, crateType, commandHelper)
end
end
local unitConfig = MaxLoadConfig[unit:getTypeName()]
local unitConfig = SupplyLoadConfig[unit:getTypeName()]
if unitConfig == nil then
trigger.action.outTextForUnit(unit:getID(), "Your unit type is not configured for logistics: " .. crateType, 5)
self._logger:error("Invalid unit type: " .. unit:getTypeName())
@@ -437,53 +467,234 @@ function SupplyUnitsTracker:UnitRequestCrateSpawn(groupID, crateType)
end
end
---@class SupplyUnitsBoundingBox
---@field min Vec3 World space minimum
---@field max Vec3 World space maximum
---@field heading number? Object heading in radians
---@param foundObject Object
---@return SupplyUnitsBoundingBox?
function SupplyUnitsTracker:GetBoundingBoxes(foundObject)
local desc = foundObject:getDesc()
if foundObject:getCategory() == Object.Category.SCENERY then
desc = SceneryObject.getDescByName(foundObject:getTypeName())
end
if desc == nil or desc.box == nil then
return nil
end
local objPos = foundObject:getPoint()
local box = desc.box
local heading = 0
-- Try to get object heading from position vector's forward direction
-- This works for units and other objects that support getPosition
pcall(function()
local objPosition = foundObject:getPosition()
if objPosition and objPosition.x then
heading = math.atan2(objPosition.x.z, objPosition.x.x)
end
end)
-- For rotated objects, we need to rotate the bounding box
local minX = box.min.x
local maxX = box.max.x
local minZ = box.min.z
local maxZ = box.max.z
-- If object has significant rotation, apply rotation to bbox corners
if math.abs(heading) > 0.1 then
-- Get all 4 corners of bbox in local space
local corners = {
{minX, minZ},
{minX, maxZ},
{maxX, minZ},
{maxX, maxZ}
}
-- Rotate corners and find new min/max
minX, maxX = math.huge, -math.huge
minZ, maxZ = math.huge, -math.huge
for _, corner in ipairs(corners) do
local rotX = corner[1] * math.cos(heading) - corner[2] * math.sin(heading)
local rotZ = corner[1] * math.sin(heading) + corner[2] * math.cos(heading)
minX = math.min(minX, rotX)
maxX = math.max(maxX, rotX)
minZ = math.min(minZ, rotZ)
maxZ = math.max(maxZ, rotZ)
end
end
-- Convert relative bbox to world space by adding object position
---@type SupplyUnitsBoundingBox
return {
min = {
x = objPos.x + minX,
y = objPos.y + box.min.y,
z = objPos.z + minZ
},
max = {
x = objPos.x + maxX,
y = objPos.y + box.max.y,
z = objPos.z + maxZ
},
heading = heading
}
end
---Check if two axis-aligned bounding boxes collide with safety margin
---@param crateBBox SupplyUnitsBoundingBox The crate's bbox in world space
---@param objBBox SupplyUnitsBoundingBox The existing object's bbox in world space
---@param safetyMargin number Safety margin around objects
---@return boolean True if collision detected
function SupplyUnitsTracker:CheckBBoxCollision(crateBBox, objBBox, safetyMargin)
-- Apply safety margin to object bbox
local objMin = {
x = objBBox.min.x - safetyMargin,
y = objBBox.min.y - safetyMargin,
z = objBBox.min.z - safetyMargin
}
local objMax = {
x = objBBox.max.x + safetyMargin,
y = objBBox.max.y + safetyMargin,
z = objBBox.max.z + safetyMargin
}
-- AABB collision detection
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.z <= objMax.z and crateBBox.max.z >= objMin.z
end
---@private
---@param unit Unit
---@return Vec3
function SupplyUnitsTracker:GetCargoPlacePosition(unit)
---@param crateTypeName string
---@return Vec3?
function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName)
local pos = unit:getPosition()
local preferredPos = {
x = pos.p.x - 10 * pos.x.x,
y = pos.p.y - 10 * pos.x.y,
z = pos.p.z - 10 * pos.x.z
local unitPos = unit:getPosition()
-- Get unit's heading from the forward vector (x component)
-- Heading is calculated as: atan2(forward.z, forward.x)
local unitHeading = math.atan2(unitPos.x.z, unitPos.x.x)
-- Get crate bbox - relative to placement position
local crateDesc = StaticObject.getDescByName(crateTypeName) --[[@as table]]
if crateDesc == nil or crateDesc.box == nil then
self._logger:error("Could not get bbox for crate type: " .. crateTypeName)
return nil
end
local crateRelativeBBox = crateDesc.box
-- Get drop zone config for this unit
local dropZones = {
{ centerAngle = 180, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }
}
if SupplyLoadConfig[unit:getTypeName()] ~= nil then
dropZones = SupplyLoadConfig[unit:getTypeName()].dropZones
end
-- Get occupied objects with their bboxes
local searchVolume = {
id = world.VolumeType.SPHERE,
params = {
point = {
x = unitPos.p.x,
y = unitPos.p.y,
z = unitPos.p.z
},
radius = 100 -- Search a large area
}
}
return preferredPos
local occupiedObjects = {}
local found = function(foundItem, val)
local bbox = self:GetBoundingBoxes(foundItem)
if bbox then
self._logger:debug("Found object: " .. foundItem:getTypeName() .. " at (" .. foundItem:getPoint().x .. ", " .. foundItem:getPoint().z .. ")")
table.insert(occupiedObjects, {
pos = foundItem:getPoint(),
bbox = bbox
})
else
self._logger:debug("Found object without bbox: " .. foundItem:getTypeName())
end
end
local searchCategories = {}
for key, value in pairs(Object.Category) do
self._logger:debug("Adding category to search: " .. tostring(value) .. " (" .. tostring(key) .. ")")
table.insert(searchCategories, value)
end
-- local volume = {
-- id = world.VolumeType.SPHERE,
-- params = {
-- point = preferredPos,
-- radius = 10
-- }
-- }
---@diagnostic disable-next-line: param-type-mismatch
world.searchObjects(searchCategories, searchVolume, found)
-- local occupiedPosX = {}
-- local occupiedPosZ = {}
local safetyMargin = 3 -- Safety margin around objects
-- ---@param foundItem Object
-- local found = function(foundItem, val)
-- Search through each slice
for _, zone in ipairs(dropZones) do
-- Calculate angle range for this slice
local minAngle = zone.centerAngle - (zone.angleWidth / 2)
local maxAngle = zone.centerAngle + (zone.angleWidth / 2)
-- local foundPos = foundItem:getPoint()
-- Search outward in rings starting from minRadius
for distance = zone.minRadius, zone.maxRadius, zone.spacing do
-- Check multiple positions within the angular slice
local angleStep = math.min(15, zone.angleWidth / 3) -- Divide slice into sections
for angle = minAngle, maxAngle, angleStep do
local radians = math.rad(angle)
-- Calculate position at this angle and distance, relative to unit's heading
-- Angle 0 = forward, 90 = right, 180 = rear, 270 = left
-- Apply unit heading to make angles relative to unit orientation
local worldAngle = radians + unitHeading
local candidateX = unitPos.p.x + distance * math.sin(worldAngle)
local candidateZ = unitPos.p.z + distance * math.cos(worldAngle)
local candidateY = land.getHeight({ x = candidateX, y = candidateZ })
-- local z = math.floor(foundPos.z)
-- for i = z - 3 , z + 3 do
-- occupiedPosZ[i] = true
-- end
-- Convert crate's relative bbox to world space at this position
local crateBBoxWorldSpace = {
min = {
x = candidateX + crateRelativeBBox.min.x,
y = candidateY + crateRelativeBBox.min.y,
z = candidateZ + crateRelativeBBox.min.z
},
max = {
x = candidateX + crateRelativeBBox.max.x,
y = candidateY + crateRelativeBBox.max.y,
z = candidateZ + crateRelativeBBox.max.z
}
}
-- local x = math.floor(foundPos.x)
-- for i = x - 3 , x + 3 do
-- occupiedPosX[i] = true
-- end
-- end
-- Check if crate bbox collides with any existing objects
local collides = false
for _, obj in ipairs(occupiedObjects) do
if self:CheckBBoxCollision(crateBBoxWorldSpace, obj.bbox, safetyMargin) then
collides = true
self._logger:debug("Collision at angle=" .. angle .. ", distance=" .. distance)
break
end
end
-- world.searchObjects(volume.id, volume.params, found)
if not collides then
self._logger:debug("Valid position found at angle=" .. angle .. ", distance=" .. distance .. ", pos=(" .. string.format("%.2f", candidateX) .. ", " .. string.format("%.2f", candidateY) .. ", " .. string.format("%.2f", candidateZ) .. ")")
return { x = candidateX, y = candidateY, z = candidateZ }
end
end
end
end
-- No free spot found
return nil
end
return SupplyUnitsTracker
@@ -5,6 +5,8 @@ local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTrac
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
local GlobalConfig = require("classes.configuration.GlobalConfig")
local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
---@class BuildableMission : Mission, SupplyUnitEventListener
---@field private _requiredKilos number
@@ -17,8 +19,8 @@ local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHel
---@field private _supplyUnitsTracker SupplyUnitsTracker
---@field private _noLandingZone SpearheadTriggerZone?
---@field private _dropOffZone SpearheadTriggerZone?
---@field private _noLandingZoneId number
---@field private _dropOffZoneId number
---@field private _noLandingZoneDrawing CustomDrawing?
---@field private _dropOffZoneDrawing CustomDrawing?
local BuildableMission = {}
BuildableMission.__index = BuildableMission
@@ -76,7 +78,7 @@ function BuildableMission.new(database, logger, targetZone, noLandingZone, requi
self._onCrateDroppedOfListeners = {}
self._completeListeners = {}
self._markIDsPerGroup = {}
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate(logger.LogLevel)
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate()
self._state = "NEW"
@@ -87,7 +89,7 @@ function BuildableMission.new(database, logger, targetZone, noLandingZone, requi
self.priority = "secondary"
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(self._logger.LogLevel)
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
self._crateType = requiredCrateType
@@ -164,20 +166,20 @@ function BuildableMission:SpawnActive()
return
end
---@type DrawColor
local lineColor = { r=230/255, g=93/255, b=49/255, a=1}
---@type DrawColor
local fillColor = { r=230/255, g=93/255, b=49/255, a=0.2}
self._noLandingZoneId = DcsUtil.DrawZone(self._noLandingZone, lineColor, fillColor, 6)
local lineColor = DrawingHelper.ColorTableToColorString({ 230/255, 93/255, 49/255, 1})
local fillColor = DrawingHelper.ColorTableToColorString({ 230/255, 93/255, 49/255, 0.2})
self._noLandingZoneDrawing = CustomDrawing.FromZone(self._noLandingZone, lineColor, fillColor, 2, 6)
self._noLandingZoneDrawing:Draw()
if self._dropOffZone == nil then
self._logger:error("No drop off zone found for mission: " .. self.code)
return
end
local lineColor2 = { r=0, g=0, b=1, a=1}
local fillColor2 = { r=0, g=0, b=1, a=0}
self._dropOffZoneId = DcsUtil.DrawZone(self._dropOffZone, lineColor2, fillColor2, 6)
local lineColor2 = DrawingHelper.ColorTableToColorString({ 0, 0, 1, 1})
local fillColor2 = DrawingHelper.ColorTableToColorString({ 0, 0, 1, 0})
self._dropOffZoneDrawing = CustomDrawing.FromZone(self._dropOffZone, lineColor2, fillColor2, 2, 6)
self._dropOffZoneDrawing:Draw()
---@param selfA BuildableMission
---@param time number
@@ -197,7 +199,7 @@ function BuildableMission:SpawnActive()
self._state = "ACTIVE"
self._missionCommandsHelper:AddMissionToCommands(self)
self._supplyUnitsTracker:AddOnSupplyUnitSpawnedListener(self)
self._supplyUnitsTracker:AddOnSupplyUnitEventListener(self)
local units = self._supplyUnitsTracker:GetUnits()
if units then
@@ -254,8 +256,8 @@ function BuildableMission:CheckCratesInZone()
end
if self._droppedKilos >= self._requiredKilos then
DcsUtil.RemoveMark(self._noLandingZoneId)
DcsUtil.RemoveMark(self._dropOffZoneId)
self._dropOffZoneDrawing:Remove()
self._noLandingZoneDrawing:Remove()
self:NotifyMissionComplete()
self._state = "COMPLETED"
end
@@ -1,6 +1,7 @@
local Mission = require("classes.stageClasses.missions.baseMissions.Mission")
local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
---@class RunwayStrikeMission : Mission
---@field runwayBombingTracker RunwayBombingTracker
@@ -153,10 +154,23 @@ function RunwayStrikeMission:Draw()
if runwaySection.drawID == nil then
local zone = self:SectionToSpearheadZone(runwaySection)
---@type Free
local drawObject = {
primitiveType = "Polygon",
polygonMode = "free",
mapX = 0,
mapY = 0,
points = zone.verts,
name = zone.name,
fillColorString = DrawingHelper.ColorTableToColorString(fillColor),
colorString = DrawingHelper.ColorTableToColorString(lineColor),
style = "solid",
thickness = 1,
visible = true,
}
local color = { r=0, g=1, b=0, a=0.5 }
runwaySection.drawID = DcsUtil.DrawZone(zone, color, color, 5)
runwaySection.drawID = DrawingHelper.Draw(drawObject)
else
DcsUtil.SetFillColor(runwaySection.drawID, fillColor)
DcsUtil.SetLineColor(runwaySection.drawID, lineColor)
@@ -350,7 +350,7 @@ function ZoneMission:SpawnPersistedState()
end
end
---spawns the mission, but doesn't add
---spawns the mission, but doesn't add it to the mission commands.
function ZoneMission:SpawnInactive()
self._logger:info("PreActivating " .. self.name)
@@ -52,7 +52,7 @@ function Mission.newSuper(self, zoneName, missionName, missionType, missionBrief
self.location = database:GetLocationForMissionZone(zoneName)
self.missionTypeDisplay = self.missionType
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(logger.LogLevel)
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
return true, "success"
end
-69
View File
@@ -173,10 +173,6 @@ do -- INIT DCS_UTIL
verts = enlargedPoints
}
if SpearheadConfig and SpearheadConfig.debugEnabled == true then
DCS_UTIL.DrawZone(triggerZone, { r = 0, g = 1, b = 0, a = 1 }, { a = 0, r = 0, g = 1, b = 0 }, 1)
end
DCS_UTIL.__airbaseZonesByName[name] = triggerZone
end
end
@@ -636,71 +632,6 @@ do -- INIT DCS_UTIL
---@field b number
---@field a number
local drawID = 400
---@param zone SpearheadTriggerZone
---@param lineColor DrawColor
---@param fillColor DrawColor
---@param lineStyle LineType
---@return number drawID
function DCS_UTIL.DrawZone(zone, lineColor, fillColor, lineStyle)
if lineStyle == nil then lineStyle = 4 end
drawID = drawID + 1
if zone.zone_type == "Cilinder" then
trigger.action.circleToAll(-1, drawID, { x = zone.location.x, y = 0, z = zone.location.y }, zone.radius,
{ 0, 0, 0, 0 }, { 0, 0, 0, 0 }, lineStyle, true)
else
local functionString = "trigger.action.markupToAll(7, -1, " .. drawID .. ","
for _, vecpoint in pairs(zone.verts) do
functionString = functionString .. " { x=" .. vecpoint.x .. ", y=0,z=" .. vecpoint.y .. "},"
end
functionString = functionString .. "{0,1,0,1}, {0,1,0,1}, " .. lineStyle .. ")"
---@diagnostic disable-next-line: deprecated
local f, err = loadstring(functionString)
if f then
f()
else
env.error("Something failed when drawing complex drawing" .. err)
end
end
local fillColorMapped = {
fillColor.r or 0,
fillColor.g or 0,
fillColor.b or 0,
fillColor.a or 0.5
}
local lineColorMapped = {
lineColor.r or 0,
lineColor.g or 0,
lineColor.b or 0,
lineColor.a or 1
}
trigger.action.setMarkupColorFill(drawID, fillColorMapped)
trigger.action.setMarkupColor(drawID, lineColorMapped)
return drawID
end
---@param start Vec3
---@param finish Vec3
---@param lineColor DrawColor
---@param lineStyle LineType
function DCS_UTIL.DrawLine(start, finish, lineColor, lineStyle)
if lineStyle == nil then lineStyle = 4 end
drawID = drawID + 1
local lineColorMapped = {
lineColor.r or 0,
lineColor.g or 0,
lineColor.b or 0,
lineColor.a or 1
}
trigger.action.lineToAll(-1, drawID, start, finish, lineColorMapped, lineStyle)
return drawID
end
---@param groupID number
---@param text string
+14 -2
View File
@@ -1,5 +1,17 @@
local Util = require("classes.util.Util")
local SpearheadConfig = require("classes.configuration.GlobalConfig")
---@type LogLevel
local defaultLogLevel = "INFO"
if SpearheadConfig then
if SpearheadConfig:isDebugEnabled() then
defaultLogLevel = "DEBUG"
end
end
--- @class Logger
--- @field LoggerName string the name of the logger
@@ -10,13 +22,13 @@ do
---comment
---@param logger_name any
---@param logLevel LogLevel
---@param logLevel LogLevel? override the default log level
---@return Logger
function LOGGER.new(logger_name, logLevel)
LOGGER.__index = LOGGER
local self = setmetatable({}, LOGGER)
self.LoggerName = logger_name or "(loggername not set)"
self.LogLevel = logLevel or "INFO"
self.LogLevel = logLevel or defaultLogLevel
return self
end
+43
View File
@@ -297,6 +297,49 @@ do -- INIT UTIL
return false
end
---@param points Array<Vec3>
---@return Array<Vec3>
function UTIL.getConvexHull3d(points)
if #points == 0 then
return {}
end
---comment
---@param a Vec3
---@param b Vec3
---@param c Vec3
---@return boolean
local function ccw(a, b, c)
return (b.z - a.z) * (c.x - a.x) > (b.x - a.x) * (c.z - a.z)
end
table.sort(points, function(left, right)
return left.z < right.z
end)
local hull = {}
-- lower hull
for _, point in pairs(points) do
while #hull >= 2 and not ccw(hull[#hull - 1], hull[#hull], point) do
table.remove(hull, #hull)
end
table.insert(hull, point)
end
-- upper hull
local t = #hull + 1
for i = #points, 1, -1 do
local point = points[i]
while #hull >= t and not ccw(hull[#hull - 1], hull[#hull], point) do
table.remove(hull, #hull)
end
table.insert(hull, point)
end
table.remove(hull, #hull)
return hull
end
---comment
---@param points Array<Vec2> points
---@return Array<Vec2> hullPoints
+2 -24
View File
@@ -28,10 +28,10 @@ SpearheadEvents.Init(defaultLogLevel)
local dbLogger = Logger.new("database", defaultLogLevel)
local standardLogger = Logger.new("", defaultLogLevel)
local databaseManager = Database.New(dbLogger)
MissionCommandsHelper.getOrCreate(defaultLogLevel) -- initiate
MissionCommandsHelper.getOrCreate() -- initiate
local capConfig = CapConfig:new();
local stageConfig = StageConfig:new();
local stageConfig = StageConfig:getInstance();
local startingStage = stageConfig.startingStage or 1
if SpearheadConfig and SpearheadConfig.Persistence and SpearheadConfig.Persistence.enabled == true then
@@ -72,25 +72,3 @@ local missionEditorWarningsLogger = Logger.new("MissionEditorWarnings", defaultL
MissionEditorWarnings.WriteAll(missionEditorWarningsLogger)
GlobalStageManager:printFullOverview()
--Check lines of code in directory per file:
-- Get-ChildItem . -Include *.lua -Recurse | foreach {""+(Get-Content $_).Count + " => " + $_.name }; GCI . -Include *.lua* -Recurse | foreach{(GC $_).Count} | measure-object -sum | % Sum
-- find . -name '*.lua' | xargs wc -l
--- ==================== DEBUG ORDER OR ZONE VEC ===========================
-- local zone = Spearhead.DcsUtil.getZoneByName("MISSIONSTAGE_99")
-- local count = Spearhead.Util.tableLength(zone.verts)
-- for i = 1, count - 1 do
-- local a = zone.verts[i]
-- local b = zone.verts[i+1]
-- local color = {0,0,0,1}
-- color[i] = 1
-- trigger.action.textToAll(-1, 46+i , { x= a.x, y = 0, z = a.z } , color, {0,0,0}, 24 , true , "" .. i )
-- trigger.action.lineToAll(-1 , 56+i , { x= a.x, y = 0, z = a.z } , { x = b.x, y = 0, z = b.z } , color , 1, true)
-- end
+1 -1
View File
@@ -1 +1 @@
0.12.1
0.13.0