Feature/scenery targets (#60)

* Added Scenery targets
* Documentation updates
This commit is contained in:
2025-07-17 19:50:57 +02:00
committed by GitHub
parent b6a1285bde
commit 1d4c0a8313
27 changed files with 833 additions and 182 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

+4 -2
View File
@@ -6,9 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead</title>
<link rel="stylesheet" href="style/style.css">
<link rel="stylesheet" href="/style/prism.css">
<link rel="stylesheet" href="/style/style.css">
<script src="js/site.js"></script>
<script src="/js/prism.js"></script>
<script src="/js/site.js"></script>
<script type="module" src="js/components.js"></script>
</head>
+3
View File
@@ -1,2 +1,5 @@
import * as header from "./components/header.js";
import * as sidebar from "./components/sidebar.js";
import './components/code-block.js';
import './components/code-inline.js';
import './components/note.js';
+29
View File
@@ -0,0 +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);
+32
View File
@@ -0,0 +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);
+3
View File
@@ -9,6 +9,7 @@ class Header extends HTMLElement {
this.innerHTML = `
<div class="header-box">
<a class="logo" href="/index.html">Spearhead</a>
<span class="subTitle">mission making, made easy</span>
<div class="header-right">
<nav>
<a href="/index.html">Home</a>
@@ -18,6 +19,7 @@ class Header extends HTMLElement {
<div class="dropdown-content">
<a href="/pages/tutorials.html">Quick Starts</a>
<a href="/pages/advanced/CAP.html">Advanced: CAP</a>
<a href="/pages/advanced/missions.html">Advanced: Missions</a>
</div>
</div>
@@ -25,6 +27,7 @@ class Header extends HTMLElement {
<a href="/pages/persistence.html">Persistence</a>
<a href="/pages/reference.html">Reference</a>
<a href="/pages/spearheadapi.html">API</a>
<a href="/pages/about_us.html">About Us</a>
</nav>
<button id="theme-toggle" class="theme-toggle" title="Toggle light/dark theme">
<svg id="moon-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+35
View File
@@ -0,0 +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);
+23 -3
View File
@@ -15,11 +15,11 @@ class Sidebar extends HTMLElement {
// Clear existing nav items
this.navItems = [];
// Find all h2 and h3 elements with IDs in the content area
// 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]');
const headers = contentWrapper.querySelectorAll('h2[id], h3[id], h4[id]');
headers.forEach(header => {
const id = header.getAttribute('id');
@@ -45,6 +45,7 @@ class Sidebar extends HTMLElement {
ul.innerHTML = '';
let currentH2Li = null;
let currentH3Li = null;
this.navItems.forEach(item => {
if (item.level === 'h2') {
@@ -59,6 +60,7 @@ class Sidebar extends HTMLElement {
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');
@@ -74,6 +76,24 @@ class Sidebar extends HTMLElement {
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);
}
@@ -139,7 +159,7 @@ class Sidebar extends HTMLElement {
link.classList.remove('active');
});
if (activeId) {
if (activeId !== nil && activeId !== '') {
const activeLink = this.querySelector(`a[href="#${activeId}"]`);
if (activeLink) {
activeLink.classList.add('active');
+10
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -1,3 +1,4 @@
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.getElementById('theme-toggle');
const sunIcon = document.getElementById('sun-icon');
+67
View File
@@ -0,0 +1,67 @@
<!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>About Us</title> <!-- Google Fonts -->
<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>
<style>
.side-nav a.active {
font-weight: bold;
color: #4fc3f7;
}
</style>
<script type="module" src="/js/components.js"></script>
</head>
<body>
<header>
<app-header></app-header>
</header>
<main>
<div class="reference-container">
<app-sidebar></app-sidebar>
<div class="content-wrapper">
<h1>About Spearhead</h1>
<h2>Created by Mission makers, for mission makers</h2>
<p>
I Dutchie, started Spearhead as an idea of making mission making easier for myself and others.<br>
On the way I've had a lot of help, in the form of feedback, ideas and most importantly testing from a lot of people. <br>
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.
</p>
<h2>Hope you enjoy it!</h2>
<p>
All I can say is that I hope that some of you out there will take the step and make a mission! <br>
Or, those that are experienced in mission making, will find more joy in making the mission they always wanted to make!<br>
If you do, dont hesitate let me and TDCS know! <br>
Don't worry, no obligation to let us use it, but any feedback is always welcome! <br>
All I really want is to see the DCS community enjoy the Multiplayer environment as much as I do!<br>
~ Dutchie
</p>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Spearhead Project</p>
</footer>
</body>
</html>
```
+186 -5
View File
@@ -4,8 +4,12 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead Persistence</title>
<title>Spearhead: Advanced CAP</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>
<style>
.side-nav a.active {
@@ -25,14 +29,191 @@
<app-sidebar></app-sidebar>
<div class="content-wrapper">
<note-box type="warning" title="Work in Progress">
This page is not complete and documentation will be changed as updates are made.
</note-box>
<h1>Advanced Tutorial: CAP</h1>
<p style="font-size: 24px;">
<b>You're a little early! <br>
We're still working on this tutorial, but it will be available soon.</b>
<p>
Setting up CAP can be a challenge. Creating a dynamic environment for your players where enemy
aircraft prove a challenge, but is still fun is something that requires some thought. <br>
With Spearhead, we tried to make it as easy as possible, but to give you as much control as well.
<br>
Air combat is so much more than just aircraft flying around. There's intercepts, combat air patrol
or maybe fighter sweeps. <br>
Sometimes one of these is enough, sometimes creating a network of all three can make it feel like a
very dynamic environment. <br>
In this tutorial we'll explain how to set up each different type of CAP and we'll finish with some
considerations on how to combine them and create an environment we think is fun to play in. <br>
</p>
<h2 id="quick-reference">Quick Reference</h2>
<p>
Quickly noting down all the names, trigger zone conventions and other things worth mentioning before
we get into the details. <br>
<h3 id="cap-groups-ref">Cap Group Prefix</h3>
<table class="table-no-style">
<tbody>
<tr>
<td><code-inline>CAP_A</code-inline></td>
<td>=></td>
<td>Primary CAP group. First to go out and also important in defining how many active units
can in a zone.</td>
</tr>
<tr>
<td><code-inline>CAP_B</code-inline></td>
<td>=></td>
<td>Secondary CAP group. Will be reinforcing the primary CAP group.</td>
</tr>
<tr>
<td><code-inline>CAP_S</code-inline></td>
<td>=></td>
<td>Sweep groups. These group will be used for fighter sweeps.</td>
</tr>
<tr>
<td><code-inline>CAP_I</code-inline></td>
<td>=></td>
<td>Intercept groups. These groups will be used for intercepts.</td>
</tr>
</tbody>
</table>
<note-box type="info" title="Combining Tasks">
Currently a group can do 1 thing. Either be Primary, Seconday, Sweep or Intercept. <br>
If you want to have different types during different stages, it's best to copy paste groups and
rename them accordingly. <br>
</note-box>
<note-box type="info" title="Airfield Defenses">
</note-box>
<h3 id="cap-zones-ref">Zones</h3>
<table class="table-no-style">
<tbody>
<tr>
<td><code-inline>CAPROUTE_[ZoneID]_[NAME]</code-inline></td>
<td>=></td>
<td>A Cap ROUTE. The two most outer points are used to define the fly-over points.</td>
</tr>
<tr>
<td><code-inline>INTERCEPTZONE_[ZoneID]_[NAME]</code-inline></td>
<td>=></td>
<td>An Intercept zone. This is where, when detected, Intercept flights will try and
intercept an enemy units.</td>
</tr>
</tbody>
</table>
</p>
<h2 id="setup">Setting up groups</h2>
<h3 id="types-explained">Types explained</h3>
<p>
Before we go into depth on each type it's best to first look at the types briefly and what we
mean.<br>
All groups that fly CAP, intercept or sweep missions are prefixed with
<code-inline>CAP_</code-inline> and then a letter to indicate the type of group. <br>
<br>
<code-inline>CAP_A</code-inline> and <code-inline>CAP_B</code-inline> are CAP groups as most often
seen in missions. <br>
They will fly out, patrol a certain route and engage any enemy fighter that gets too close. <br>
<br>
<code-inline>CAP_S</code-inline> groups are Sweep groups. <br>
These will fly out, fly a pre-designated route and engage enemy fighters they come across. <br>
They, however, will not stick around and orbit, but will fly back home once their sweep is complete.
<br>
These are very nice for older scenarios or where Air Superiority is not maintained by either side.
<br>
<br>
<code-inline>CAP_I</code-inline> groups are Intercept groups. <br>
These groups will remain on high alert and will be triggered when enemy aircraft are detected in a
certain zone. <br>
<br>
NOT IMPLMENTED YET: <br>
<code-inline>CAP_E</code-inline> are escort groups. <br>
These units will be dedicated to escorting CAS, SEAD and Strike flights. <br>
The ground attack units mentioned are not in yet and thus escort groups are not either. <br>
<note-box type="warning" title="Overriding Taskings">
Each tasking has very specific waypoints, both for departure, arrical and ofcourse all point in between. <br>
Overriding the "tasking" will break part of the cap flow as "RTBInTen" and RTB events are triggered by waypoints. <br>
Overriding these waypoints and taskings will break the management by Spearhead.
</note-box>
<h3 id="combat-air-patrol">Combat Air Patrol</h3>
<h4 id="zone-setup-cap">Zone Setup</h4>
<p>
Combat Air Patrol groups use <code-inline>CAPROUTE_</code-inline> zones to know where to go.<br>
These are trigger zones that need to be <code-inline>quad zones</code-inline>.
The way the trigger zone is mapped to a route is as follows: <br>
<br>
The 2 points that are furthest apart from each other are chosen as the "leg" of the route. <br>
The other 2 points are discarded. <br>
<br>
The Orbit route is then created with an "Anchored" task. With the closest point being the starting
point with an outbound leg to the furthest point. <br>
</p>
<h4 id="group-setup-cap">Group Setup</h4>
<p>
Now you've defined where the CAP group will fly, it's time to set up the group itself. <br>
The groups are used directly and will not be renamed, duplicated or anything else. <br>
However, please note, again, that if you manually take over controll of the groups by giving them waypoints it will break the management by Spearhead. <br>
They will ofcourse be despawned, respawned and tracked by Spearhead as needed. <br>
Add an aircraft unit and set it to "spawn from parking cold". <br>
Name the group (unit names are not regarded) <br>
<note-box type="info" title="Cloning">
Adding the lead first, then editing the loadout, livery etc. and only then adding flight members will automatically copy the leads loadout and other settings.
</note-box>
</p>
<h4 id="what-it-does-cap">What it does</h4>
<p>
</p>
<h3 id="sweep">Sweep</h3>
<p>
</p>
<h3 id="intercept">Intercept</h3>
<p>
</p>
<h2 id="balancing">Balancing & Scaling</h2>
<p>
We are fully aware that balancing and scaling of the Air Threats is something you need to do. <br>
We are still designing how to make this as easy as possible for you, the mission creators. <br>
The idea is to have thresholds. (If x number of players are in the mission, then add certain groups.) <br/>
Most importantly it should be easy to use and easy to estimate before play testing. <br>
<br>
Things we need to think about are (but not exclusively) whether or not we only want to scale based on CAP capable aircraft. <br>
eg. if you have 10 apaches, should it bring up twice as much CAP? <br>
Ofcourse we also need to think about the downscaling and about how later it can integrate with supplies. <br>
Please bear with us, as we are definitely working on this and making sure that you can scale for to 100 players without much trouble. <br>
</p>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Spearhead Project</p>
+68
View File
@@ -0,0 +1,68 @@
<!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 Missions</title>
<link rel="stylesheet" href="/style/style.css">
<script src="/js/site.js"></script>
<style>
.side-nav a.active {
font-weight: bold;
color: #4fc3f7;
}
</style>
<script type="module" src="/js/components.js"></script>
</head>
<body>
<header>
<app-header></app-header>
</header>
<main>
<div class="reference-container">
<app-sidebar></app-sidebar>
<div class="content-wrapper">
<h1>Advanced Tutorial: Missions</h1>
<h2 id="strike">Strike</h2>
<p>
Naming: <span class="inline-lua"><span class="lua-variable">MISSION_STRIKE_[Name]</span></span> <br>
</p>
<h3 id="specific_targets">Specific Targets</h3>
<p>TBD</p>
<h3 id="scenery_targets">Scenery Targets</h3>
<p>
Bridges, buildings or other type of targets that are baked into the map are awesome targets.<br>
We wanted to make sure they were easily integrated in Spearhead.
All you will need to do is right click on the scenery object you want added. <br>
You will then get the option to "Assign as...", click this and you'll see a new trigger zone created. <br>
It's best not to edit the triggerzone, but you can rename it however you like. <br>
Scenery objects will always be a specific "target" in the mission.
</p>
<note-box type="warning" title="Changing IDs">
Be aware that Object IDs can change when maps are updated and edited and the "Assign as..." action will have to be performed again.
</note-box>
<p>Here's two images showing how a bridge is added to a mission.</p>
<img src="/img/bridge.png" style="max-height: 35remrem;"/>
<img src="/img/bridge_added.png" style="max-height: 35remrem;"/>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Spearhead Project</p>
</footer>
</body>
</html>
```
+3
View File
@@ -6,7 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead Persistence</title> <!-- Google Fonts -->
<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>
<style>
.side-nav a.active {
+5 -2
View File
@@ -6,8 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead Reference</title> <!-- Google Fonts -->
<link rel="stylesheet" href="../style/style.css">
<script src="../js/site.js"></script>
<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>
<style>
.side-nav a.active {
font-weight: bold;
+5 -2
View File
@@ -6,8 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead API</title>
<link rel="stylesheet" href="../style/style.css">
<script src="../js/site.js"></script>
<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>
+17 -27
View File
@@ -6,8 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead Tutorials</title>
<link rel="stylesheet" href="../style/style.css">
<script src="../js/site.js"></script>
<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>
<style>
.side-nav a.active {
font-weight: bold;
@@ -49,13 +52,11 @@
<img style="width: 100%;" src="../img/script_install.png"></img>
</div>
<div class="note">
<p>
Spearhead does not require any dependencies (eg. MIST or MOOSE). Compatibility with other frameworks is
not tested at this time, so cannot be guaranteed, but there should be no conflicts if they are not
controlling the same units.
</p>
</div>
<note-box type="info" title="Note">
Spearhead does not require any dependencies (eg. MIST or MOOSE). Compatibility with other frameworks is
not tested at this time, so cannot be guaranteed, but there should be no conflicts if they are not
controlling the same units.
</note-box>
<h2 id="stages">Stages</h2>
@@ -114,13 +115,9 @@
will trigger a backup unit to startup and fly out to take over. <br />
</p>
<div class="note">
<p>
TIP ! <br />
You can start a mission, speed up the simulation and make the CAP fly out to see what happens
</p>
</div>
<note-box type="info" title="Tip">
You can start a mission, speed up the simulation and make the CAP fly out to see what happens
</note-box>
<h3 id="creating-cap-routes">Creating CAP Routes</h3>
<p>
@@ -153,11 +150,10 @@
Missions are managed and monitored by Spearhead. <br/>
Statics, groups and single units all alike. <br/>
</p>
<div class="note">
<p>
<strong>NOTE:</strong> While static are the same as groups in this context, they are not within DCS, please refrain from using static groups. A <span class="inline-ref">static</span> in DCS has a 1:1 relation for group:unit.
</p>
</div>
<note-box type="info" title="Note">
While static are the same as groups in this context, they are not within DCS, please refrain from artificially created static groups. A <code-inline>static</code-inline> in DCS has a 1:1 relation for group:unit. <br>
By default DCS will always keep 1 static object in 1 groups.
</note-box>
<p>
For this example I'll set up two missions. The first one is <span class="inline-lua"><span class="lua-variable">DEAD</span></span> mission and will consist of an SA-2 site with an additional "control center".
</p>
@@ -284,12 +280,6 @@
<img src="../img/briefing_mission.png"/>
</div>
</div>
<div class="note">
<p>
<strong>TIP:</strong> You can make both the <span class="inline-ref">Color</span> and <span class="inline-ref">Fill</span> have a <span class="inline-ref">0</span> value for <span class="inline-ref">a</span>. This will make the weird box be invisible. Make sure to add the name of the mission to the text box name to easily find it back.
</p>
</div>
<p>
We are now done creating a first mission. Hit fly and test it. <br/>
Check all references for way more features and keep up to date with the latest changes as they come along! <br/>
+3
View File
@@ -0,0 +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}
+157 -83
View File
@@ -1,14 +1,11 @@
:root {
/* Dark Theme (Default) - Slightly lighter */
--color-bg-primary: #1b1d27; /* Much darker primary background */
--color-bg-secondary: #2d303f; /* Darker secondary background */
--color-bg-tertiary: #2c3041; /* Darker tertiary */
--color-bg-header: #10121b; /* Darker header */
--color-bg-code: #10121a; /* Almost black code block background */
--color-bg-note: rgba(30, 32, 40, 0.85); /* Slightly darker note background */
--color-bg-hover: rgba(120, 140, 255, 0.12); /* More visible hover */
--color-bg-inline-lua: #181a22; /* Match code block background */
--color-bg-note-dark: rgba(40, 35, 30, 0.85); /* Slightly darker note box */
--color-text-primary: #a5a5a5; /* Brighter text */
@@ -22,14 +19,6 @@
--color-link: #7ecbff;
--color-link-hover: #ffffff;
/* Lua syntax highlighting colors with high contrast */
--color-lua-variable: #4fc3f7;
--color-lua-comment: #b0b8d0; /* Lighter, more visible comment */
--color-lua-operator: #ffffff;
--color-lua-keyword: #ffd166; /* Orange for keywords */
--color-lua-string: #7fffde; /* Aqua for strings */
--color-lua-function: #ffe066; /* Gold for functions */
/* Shadow and transparency values - unchanged */
--shadow-small: 0 2px 5px rgba(0, 0, 0, 0.3);
--shadow-medium: 0 3px 8px rgba(0, 0, 0, 0.5);
@@ -59,11 +48,14 @@
--gradient-header: linear-gradient(135deg, var(--color-bg-header) 0%, var(--color-bg-secondary) 70%, var(--color-bg-header-accent) 100%);
--color-bg-header-accent: #2a2620;
--color-logo-gradient: linear-gradient(120deg, #5caaff, #a0e8ff, #ffffff);
--color-text-subTitle: #4a576f; /* Lighter subtitle text */
--color-nav-underline: #b0a890;
--color-nav-underline-glow: rgba(176, 168, 144, 0.6); --color-nav-hover-bg: var(--color-bg-hover);
--color-nav-hover-text: #ffffff;
--color-nav-hover-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
--color-inline-code: #3498db;
/* Heading colors */
--color-heading-h1: #8fc6ff;
--color-heading-h2: #b3e0ff;
@@ -77,10 +69,8 @@
--color-bg-secondary: #b6b6b6;
--color-bg-tertiary: #b6b6b6;
--color-bg-header: #d3d3d3;
--color-bg-code: #e3e6eb; /* Medium-light gray for code block background */
--color-bg-note: rgba(215, 228, 245, 0.75);
--color-bg-hover: rgba(100, 120, 200, 0.1);
--color-bg-inline-lua: #e3e6eb;
--color-bg-note-dark: rgba(235, 240, 250, 0.7);
--color-text-primary: #151720;
@@ -94,15 +84,6 @@
--color-link: #3050b0;
--color-link-hover: #4060c0;
/* Lua syntax highlighting colors for light theme */
--color-lua-variable: #1a237e;
--color-lua-comment: #374151;
--color-lua-operator: #23272e;
--color-lua-keyword: #b45309;
--color-lua-string: #065f46;
--color-lua-function: #6d28d9;
/* Shadow values adjusted for light theme */
--shadow-small: 0 2px 5px rgba(0, 0, 0, 0.15);
--shadow-medium: 0 3px 8px rgba(0, 0, 0, 0.2);
@@ -188,7 +169,7 @@ header {
}
.header-right {
width: 50%;
width: auto;
margin-left: 10em;
display: flex;
justify-content: space-between;
@@ -205,6 +186,13 @@ header {
text-shadow: var(--shadow-small);
transition: all 0.3s ease;
}
.subTitle {
position: relative;
left: -5.5rem;
top: 1.7rem;
color: var(--color-text-subTitle);
text-wrap: nowrap;;
}
.logo:hover {
transform: scale(1.03);
@@ -363,10 +351,6 @@ main {
will-change: transform;
}
.side-nav h4 {
margin-top: 0;
margin-bottom: 0.5em;
}
.side-nav ul {
list-style-type: none;
@@ -414,6 +398,12 @@ main {
font-size: 0.9em;
}
.side-nav-h4 {
margin-top: 0;
margin-bottom: 0.5em;
font-size: 0.75em;
}
.content-wrapper {
flex: 1;
max-width: 70%;
@@ -503,62 +493,6 @@ th {
font-weight: bold;
}
/* Code and syntax highlighting with improved contrast */
.inline-lua {
background-color: var(--color-bg-inline-lua);
border-radius: 3px;
font-family: var(--font-family-code);
white-space: nowrap;
padding: 0.1em 0.3em;
color: #23272e;
}
/* Code block styling with improved contrast */
pre {
background-color: var(--color-bg-code);
border: 1px solid var(--color-border-primary);
border-radius: 8px;
padding: 1.2em;
margin: 1.5em 0;
overflow-x: auto;
font-family: var(--font-family-code);
line-height: 1.4;
box-shadow: inset 0 1px 8px rgba(0, 0, 0, 0.08);
color: #23272e;
}
.lua-variable {
color: var(--color-lua-variable); /* Brighter variable color */
}
.lua-comment {
color: var(--color-lua-comment); /* Brighter comment color */
font-style: italic;
}
.lua-operator {
color: var(--color-lua-operator); /* Bright white for better contrast */
}
.lua-keyword {
color: var(--color-lua-keyword); /* Brighter keyword color */
font-weight: bold;
}
.lua-string {
color: var(--color-lua-string); /* Brighter string color */
}
.lua-function {
color: var(--color-lua-function); /* Brighter function color */
font-weight: bold;
}
.inline-ref {
color: var(--color-text-accent);
font-weight: bold;
padding: 0 2px;
}
/* Note boxes */
.note {
@@ -574,6 +508,58 @@ pre {
margin-bottom: 0;
}
/* Enhanced note component styles */
.note-title {
margin-top: 0;
margin-bottom: 0.8em;
font-size: 1.1em;
font-weight: 600;
color: var(--color-text-accent);
border-bottom: 1px solid rgba(126, 203, 255, 0.3);
padding-bottom: 0.3em;
}
/* Note type variations */
.note-warning {
background: linear-gradient(to right, rgba(80, 60, 30, 0.7), rgba(70, 55, 30, 0.55));
border-left-color: #f39c12;
}
.note-warning .note-title {
color: #f39c12;
border-bottom-color: rgba(243, 156, 18, 0.3);
}
.note-info {
background: linear-gradient(to right, rgba(30, 60, 80, 0.7), rgba(25, 55, 75, 0.55));
border-left-color: #3498db;
}
.note-info .note-title {
color: #3498db;
border-bottom-color: rgba(52, 152, 219, 0.3);
}
.note-success {
background: linear-gradient(to right, rgba(30, 80, 40, 0.7), rgba(25, 75, 35, 0.55));
border-left-color: #27ae60;
}
.note-success .note-title {
color: #27ae60;
border-bottom-color: rgba(39, 174, 96, 0.3);
}
.note-danger {
background: linear-gradient(to right, rgba(80, 30, 30, 0.7), rgba(75, 25, 25, 0.55));
border-left-color: #e74c3c;
}
.note-danger .note-title {
color: #e74c3c;
border-bottom-color: rgba(231, 76, 60, 0.3);
}
/* Image styling */
img {
max-width: 100%;
@@ -582,6 +568,81 @@ img {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
/* Code styling */
code, pre {
font-family: var(--font-family-code);
font-size: 0.9em;
}
/* Block code styling */
.custom-code-block {
background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%);
border: 1px solid var(--color-border-primary);
border-radius: 8px;
padding: 1.2em;
margin: 1.5em 0;
overflow-x: auto;
box-shadow: var(--shadow-small);
position: relative;
}
.custom-code-block code {
background: none;
padding: 0;
border: none;
border-radius: 0;
font-size: 0.85em;
line-height: 1.5;
color: var(--color-text-secondary);
}
/* Inline code styling */
code.custom-inline-code {
background: var(--color-bg-secondary) !important;
border: 1px solid var(--color-border-primary);
border-radius: 4px;
padding: 0.2em 0.4em;
font-size: 0.85em;
color: var(--color-inline-code) !important;
font-weight: 500;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: inline;
white-space: nowrap;
font-family: var(--font-family-code) !important;
}
/* Variable highlighting within inline code */
.inline-var {
color: var(--color-text-accent);
font-weight: 600;
background: rgba(126, 203, 255, 0.1);
padding: 0.1em 0.2em;
border-radius: 2px;
border: 1px solid rgba(126, 203, 255, 0.2);
}
/* Prism.js compatibility - override default styles */
.custom-code-block .token.variable {
color: var(--color-text-accent) !important;
}
.custom-code-block .token.string {
color: #a3d977 !important;
}
.custom-code-block .token.keyword {
color: #ff7aa3 !important;
}
.custom-code-block .token.function {
color: #78dce8 !important;
}
.custom-code-block .token.comment {
color: #727072 !important;
font-style: italic;
}
/* Custom Scrollbar - Enhanced version */
::-webkit-scrollbar {
width: 12px;
@@ -694,6 +755,19 @@ img {
}
}
.table-no-style {
border-collapse: collapse;
gap: 0px;
}
.table-no-style > tr,td {
border: none;
padding: 0px;
padding-left: 1rem;
margin: none;
}
/* Theme Toggle Button */
.theme-toggle {
background: none;
Binary file not shown.
+3
View File
@@ -14,3 +14,6 @@ do -- mission aliases
end
---@class KeyValuePair
---@field key string
---@field value any
+12 -1
View File
@@ -539,6 +539,7 @@ do -- INIT DCS_UTIL
---@field radius number
---@field verts Array<Vec2>
---@field zone_type SpearheadTriggerZoneType
---@field properties Array<KeyValuePair>?
---@type Array<SpearheadTriggerZone>
DCS_UTIL.__trigger_zones = {}
@@ -598,9 +599,18 @@ do -- INIT DCS_UTIL
zone_type = zoneType,
location = { x = trigger_zone.x, y = trigger_zone.y },
radius = trigger_zone.radius,
verts = verts
verts = verts,
properties = {}
}
if trigger_zone.properties then
for _, kvPair in pairs(trigger_zone.properties) do
local key = kvPair["key"]
local value = kvPair["value"]
zone.properties[#zone.properties + 1] = { key = key, value = value }
end
end
DCS_UTIL.__trigger_zones[zone.name] = zone
end
end
@@ -978,6 +988,7 @@ do -- INIT DCS_UTIL
end
end
---comment Get all units that are players
---@return Array<Unit> units
function DCS_UTIL.getAllPlayerUnits()
+55 -52
View File
@@ -7,6 +7,7 @@
---@field MissionZonesLocations table<string, Vec2> All Mission Zone Locations
---@field StageZonesByNumber table<string, Array<string>> Stage zones grouped by index number
---@field AllFarpZones Array<string>
---@field AllSceneryObjects Array<SpearheadSceneryObject> All scenery objects
---@field AllCapRoutes Array<string> All Cap route zone names
---@field AllInterceptZones Array<string> All intercept zones
---@field capZonesByCapZoneID table<string, CapRoute>
@@ -58,11 +59,9 @@
---@class MissionZoneData
---@field RedGroups Array<string>
---@field SceneryTargets Array<SpearheadSceneryObject>
---@field BlueGroups Array<string>
---@class MissionData
---@field Groups Array<string>
---@class FarpZoneData
---@field groups Array<string>
---@field padNames Array<string>
@@ -85,6 +84,7 @@ function Database.New(Logger)
AllCapRoutes = {},
capZonesByCapZoneID = {},
AllInterceptZones = {},
AllSceneryObjects = {},
interceptZonesByZoneID = {},
CarrierRouteZones = {},
MissionZones = {},
@@ -186,6 +186,19 @@ function Database.New(Logger)
if lowered == "supplyhub" then
table.insert(self._tables.SupplyHubZones, zone_name)
end
if zone_data.properties then
for _, kvPair in pairs(zone_data.properties) do
if kvPair.key and kvPair.key == "OBJECT ID" then
local objectID = tonumber(kvPair.value)
if objectID then
local sceneryObject = Spearhead.classes.stageClasses.Groups.SpearheadSceneryObject.New(objectID)
table.insert(self._tables.AllSceneryObjects, sceneryObject)
end
end
end
end
end
end
@@ -649,67 +662,57 @@ function Database:loadBlueSamUnits()
end
---@private
function Database:loadMissionzoneUnits()
function Database:LoadZoneData(missionZoneName)
local all_groups = getAvailableGroups()
for _, missionZoneName in pairs(self._tables.MissionZones) do
self._tables.MissionZoneData[missionZoneName] = {
RedGroups = {},
BlueGroups = {},
}
self._tables.MissionZoneData[missionZoneName] = {
RedGroups = {},
BlueGroups = {},
SceneryTargets = {}
}
local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, missionZoneName)
for _, groupName in pairs(groups) do
if Spearhead.classes.helpers.MizGroupsManager.IsGroupStatic(groupName) == true then
local object = StaticObject.getByName(groupName)
local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, missionZoneName)
for _, groupName in pairs(groups) do
if Spearhead.classes.helpers.MizGroupsManager.IsGroupStatic(groupName) == true then
local object = StaticObject.getByName(groupName)
if object and object:getCoalition() == coalition.side.RED then
table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName)
elseif object then
table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName)
end
if object and object:getCoalition() == coalition.side.RED then
table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName)
elseif object then
table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName)
end
else
local group = Group.getByName(groupName)
if group and group:getCoalition() == coalition.side.RED then
table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName)
elseif group then
table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName)
end
else
local group = Group.getByName(groupName)
if group and group:getCoalition() == coalition.side.RED then
table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName)
elseif group then
table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName)
end
end
is_group_taken[groupName] = true
end
for _, sceneryObject in pairs(self._tables.AllSceneryObjects) do
local point = sceneryObject:GetPoint()
if point then
if Spearhead.DcsUtil.isPositionInZone(point.x, point.z, missionZoneName) == true then
table.insert(self._tables.MissionZoneData[missionZoneName].SceneryTargets, sceneryObject)
end
is_group_taken[groupName] = true
end
end
end
---@private
function Database:loadMissionzoneUnits()
for _, missionZoneName in pairs(self._tables.MissionZones) do
self:LoadZoneData(missionZoneName)
end
end
---@private
function Database:loadRandomMissionzoneUnits()
local all_groups = getAvailableGroups()
for _, missionZoneName in pairs(self._tables.RandomMissionZones) do
self._tables.MissionZoneData[missionZoneName] = {
RedGroups = {},
BlueGroups = {},
}
local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, missionZoneName)
for _, groupName in pairs(groups) do
if Spearhead.classes.helpers.MizGroupsManager.IsGroupStatic(groupName) == true then
local object = StaticObject.getByName(groupName)
if object and object:getCoalition() == coalition.side.RED then
table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName)
elseif object then
table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName)
end
else
local group = Group.getByName(groupName)
if group and group:getCoalition() == coalition.side.RED then
table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName)
elseif group then
table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName)
end
end
is_group_taken[groupName] = true
end
self:LoadZoneData(missionZoneName)
end
end
@@ -0,0 +1,77 @@
---@class SpearheadSceneryObject
---@field private persistentName string The persistent name of the scenery object
---@field private objectID number The ID of the scenery object
---@field private internalObj table
---@field private isDead boolean Indicates if the scenery object is dead
local SpearheadSceneryObject = {}
SpearheadSceneryObject.__index = SpearheadSceneryObject
---comment
---@param objectID number
---@return SpearheadSceneryObject?
function SpearheadSceneryObject.New(objectID)
local self = setmetatable({}, SpearheadSceneryObject)
if objectID == nil then
return nil
end
self.persistentName = "SpearheadSceneryObject_" .. objectID
self.objectID = objectID
self.isDead = false
self.internalObj = {
["id_"] = objectID
}
return self
end
---@return boolean
function SpearheadSceneryObject:IsAlive()
if Object.isExist(self.internalObj) == false then
self:MarkDead()
return false
end
if SceneryObject.getLife(self.internalObj) <= 0.10 then
self:MarkDead()
return false
end
return true
end
---@private
function SpearheadSceneryObject:MarkDead()
if self.isDead == true then return end
self.isDead = true
Spearhead.classes.persistence.Persistence.UnitKilled(self.persistentName, self:GetPoint(), 0, "Scenery")
end
function SpearheadSceneryObject:UpdateStatePersistently()
if self.isDead == true then
return
end
local state = Spearhead.classes.persistence.Persistence.UnitState(self.persistentName)
if state and state.isDead == true then
trigger.action.explosion(self:GetPoint(), 1000)
self.isDead = true
end
end
function SpearheadSceneryObject:GetPersistentName()
return self.persistentName
end
function SpearheadSceneryObject:GetPoint()
return Object.getPoint(self.internalObj)
end
if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end
if not Spearhead.classes.stageClasses.Groups then Spearhead.classes.stageClasses.Groups = {} end
Spearhead.classes.stageClasses.Groups.SpearheadSceneryObject = SpearheadSceneryObject
+29 -1
View File
@@ -15,6 +15,7 @@ local ZoneMission = {}
--- @field blueGroups Array<SpearheadGroup>
--- @field unitsAlive table<string, table<string, boolean>>
--- @field targetsAlive table<string, table<string, boolean>>
--- @field sceneryTargets Array<SpearheadSceneryObject>
--- @field groupNamesPerunit table<string,string>
---@class ParsedMissionName
@@ -101,7 +102,8 @@ function ZoneMission.new(zoneName, priority, database, logger, parentStage, spaw
unitsAlive = {},
targetsAlive = {},
hasTargets = false,
groupNamesPerunit = {}
groupNamesPerunit = {},
sceneryTargets = {}
}
self._parentStage = parentStage
@@ -127,6 +129,10 @@ function ZoneMission.new(zoneName, priority, database, logger, parentStage, spaw
local missionData = database:getMissionDataForZone(zoneName)
if not missionData then return end
self._missionGroups.sceneryTargets = missionData.SceneryTargets or {}
if Spearhead.Util.tableLength(self._missionGroups.sceneryTargets) > 0 then
self._missionGroups.hasTargets = true
end
for _, groupName in pairs(missionData.BlueGroups) do
local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true)
@@ -288,6 +294,13 @@ function ZoneMission:UpdateState(checkHealth, messageIfDone)
end
end
for _, sceneryObject in pairs(self._missionGroups.sceneryTargets) do
total = total + 1
if sceneryObject:IsAlive() == true then
alive = alive + 1
end
end
local deadRatio = (total - alive) / total
if deadRatio >= self._completeAtIndex then
self._logger:debug("Dead ratio " .. self.zoneName .. deadRatio .. " >= " .. self._completeAtIndex)
@@ -327,6 +340,10 @@ function ZoneMission:SpawnPersistedState()
for _, group in pairs(self._missionGroups.redGroups) do
group:Spawn()
end
for _, object in pairs(self._missionGroups.sceneryTargets) do
object:UpdateStatePersistently()
end
end
---spawns the mission, but doesn't add
@@ -361,6 +378,10 @@ function ZoneMission:SpawnActive()
group:Spawn()
end
for _, sceneryObject in pairs(self._missionGroups.sceneryTargets) do
sceneryObject:UpdateStatePersistently()
end
if self._battleManager then
self._battleManager:Start()
end
@@ -404,6 +425,13 @@ function ZoneMission:PercentageComplete()
end
end
for _, sceneryObject in pairs(self._missionGroups.sceneryTargets) do
total = total + 1
if sceneryObject:IsAlive() == false then
dead = dead + 1
end
end
if total > 0 then
return math.floor((dead / total) * 100)
end
+2
View File
@@ -36,6 +36,8 @@ assert(loadfile(classPath .. "stageClasses\\Stages\\WaitingStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Groups\\SpearheadGroup.lua"))()
assert(loadfile(classPath .. "stageClasses\\Groups\\SpearheadSceneryObject.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\MissionCommandsHelper.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\SupplyConfig.lua"))()