Merge branch 'release-candidate' into manager-wizard

This commit is contained in:
Pax1601 2024-01-04 13:03:44 +01:00
commit 73b1714191
30 changed files with 1316 additions and 176 deletions

View File

@ -33,6 +33,6 @@ jobs:
- name: Upload a Build Artifact
uses: actions/upload-artifact@v3.1.3
with:
name: latest
name: development_build_not_a_release
path: ./package

View File

@ -23,7 +23,7 @@ jobs:
uses: actions/setup-node@v2
- name: Install dependencies
run: npm ci
run: npm install
working-directory: ./client
- name: Create the docs directory locally in CI

View File

@ -23,7 +23,7 @@ Even better it requires no client mods be installed if used on a server
The full feature list is simply too long to enumerate in a short summary but needless to say Olympus offers up a lot of unique gameplay that has previously not existed, and enhances many other elements of DCS in exciting ways
### Installing DCS Olympus
A prebuilt installer will soon be released and available here
Check the [Wiki](https://github.com/Pax1601/DCSOlympus/wiki) for installation instructions
# Frequently Asked Questions
### Can I join up and help out with the project? ###

18
client/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM node:20-alpine AS appbuild
WORKDIR /usr/src/app
COPY package.json ./
COPY package-lock.json ./
RUN npm install
COPY . .
RUN npm run build-release-linux
FROM node:20-alpine
WORKDIR /usr/src/app
COPY package.json ./
COPY package-lock.json ./
RUN npm install --omit=dev
COPY . .
COPY --from=appbuild /usr/src/app/public ./public
EXPOSE 3000
CMD npm start

194
client/configurator.js Normal file
View File

@ -0,0 +1,194 @@
const fs = require('fs')
const path = require('path')
const yargs = require('yargs');
const prompt = require('prompt-sync')({sigint: true});
const sha256 = require('sha256');
var jsonPath = path.join('..', 'olympus.json');
var regedit = require('regedit')
const shellFoldersKey = 'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders'
const saveGamesKey = '{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}'
/* Set the acceptable values */
yargs.alias('a', 'address').describe('a', 'Backend address').string('a');
yargs.alias('b', 'backendPort').describe('b', 'Backend port').number('b');
yargs.alias('c', 'clientPort').describe('c', 'Client port').number('c');
yargs.alias('p', 'gameMasterPassword').describe('p', 'Game Master password').string('p');
yargs.alias('bp', 'blueCommanderPassword').describe('bp', 'Blue Commander password').string('bp');
yargs.alias('rp', 'redCommanderPassword').describe('rp', 'Red Commander password').string('rp');
yargs.alias('d', 'directory').describe('d', 'Directory where the DCS Olympus configurator is located').string('rp');
args = yargs.argv;
async function run() {
/* Check that we can read the json */
if (fs.existsSync(jsonPath)) {
var json = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
var address = args.address ?? json["server"]["address"];
var clientPort = args.clientPort ?? json["client"]["port"];
var backendPort = args.backendPort ?? json["server"]["port"];
var gameMasterPassword = args.gameMasterPassword? sha256(args.gameMasterPassword): json["authentication"]["gameMasterPassword"];
var blueCommanderPassword = args.blueCommanderPassword? sha256(args.blueCommanderPassword): json["authentication"]["blueCommanderPassword"];
var redCommanderPassword = args.redCommanderPassword? sha256(args.redCommanderPassword): json["authentication"]["redCommanderPassword"];
/* Run in interactive mode */
if (args.address === undefined && args.clientPort === undefined && args.backendPort === undefined &&
args.gameMasterPassword === undefined && args.blueCommanderPassword === undefined && args.redCommanderPassword === undefined) {
var newValue;
var result;
/* Get the new address */
newValue = prompt(`Insert an address or press Enter to keep current value ${address}. Use * for any address: `);
address = newValue !== ""? newValue: address;
/* Get the new client port */
while (true) {
newValue = prompt(`Insert a client port or press Enter to keep current value ${clientPort}. Integers between 1025 and 65535: `);
if (newValue === "")
break;
result = Number(newValue);
if (!isNaN(result) && Number.isInteger(result) && result > 1024 && result <= 65535)
break;
}
clientPort = newValue? result: clientPort;
/* Get the new backend port */
while (true) {
newValue = prompt(`Insert a backend port or press Enter to keep current value ${backendPort}. Integers between 1025 and 65535: `);
if (newValue === "")
break;
result = Number(newValue);
if (!isNaN(result) && Number.isInteger(result) && result > 1024 && result <= 65535 && result != clientPort)
break;
if (result === clientPort)
console.log("Client port and backend port must be different.");
}
backendPort = newValue? result: backendPort;
/* Get the new Game Master password */
while (true) {
newValue = prompt(`Insert a new Game Master password or press Enter to keep current value: `, {echo: "*"});
gameMasterPassword = newValue !== ""? sha256(newValue): gameMasterPassword;
// Check if Game Master password is unique
if (gameMasterPassword === blueCommanderPassword || gameMasterPassword === redCommanderPassword) {
console.log("Game Master password must be different from other passwords. Please try again.");
continue;
}
break;
}
/* Get the new Blue Commander password */
while (true) {
newValue = prompt(`Insert a new Blue Commander password or press Enter to keep current value: `, {echo: "*"});
blueCommanderPassword = newValue !== ""? sha256(newValue): blueCommanderPassword;
// Check if Blue Commander password is unique
if (blueCommanderPassword === gameMasterPassword || blueCommanderPassword === redCommanderPassword) {
console.log("Blue Commander password must be different from other passwords. Please try again.");
continue;
}
break;
}
/* Get the new Red Commander password */
while (true) {
newValue = prompt(`Insert a new Red Commander password or press Enter to keep current value: `, {echo: "*"});
redCommanderPassword = newValue !== ""? sha256(newValue): redCommanderPassword;
// Check if Red Commander password is unique
if (redCommanderPassword === gameMasterPassword || redCommanderPassword === blueCommanderPassword) {
console.log("Red Commander password must be different from other passwords. Please try again.");
continue;
}
break;
}
}
/* Apply the inputs */
json["server"]["address"] = address;
json["client"]["port"] = clientPort;
json["server"]["port"] = backendPort;
json["authentication"]["gameMasterPassword"] = gameMasterPassword;
json["authentication"]["blueCommanderPassword"] = blueCommanderPassword;
json["authentication"]["redCommanderPassword"] = redCommanderPassword;
/* Write the result to disk */
const serialized = JSON.stringify(json, null, 4);
fs.writeFileSync(jsonPath, serialized, 'utf8');
console.log("Olympus.json updated correctly, goodbye!");
}
else {
console.error("Error, could not read olympus.json file!")
}
/* Wait a bit before closing the window */
await new Promise(resolve => setTimeout(resolve, 3000));
}
console.log('\x1b[36m%s\x1b[0m', "*********************************************************************");
console.log('\x1b[36m%s\x1b[0m', "* _____ _____ _____ ____ _ *");
console.log('\x1b[36m%s\x1b[0m', "* | __ \\ / ____|/ ____| / __ \\| | *");
console.log('\x1b[36m%s\x1b[0m', "* | | | | | | (___ | | | | |_ _ _ __ ___ _ __ _ _ ___ *");
console.log('\x1b[36m%s\x1b[0m', "* | | | | | \\___ \\ | | | | | | | | '_ ` _ \\| '_ \\| | | / __| *");
console.log('\x1b[36m%s\x1b[0m', "* | |__| | |____ ____) | | |__| | | |_| | | | | | | |_) | |_| \\__ \\ *");
console.log('\x1b[36m%s\x1b[0m', "* |_____/ \\_____|_____/ \\____/|_|\\__, |_| |_| |_| .__/ \\__,_|___/ *");
console.log('\x1b[36m%s\x1b[0m', "* __/ | | | *");
console.log('\x1b[36m%s\x1b[0m', "* |___/ |_| *");
console.log('\x1b[36m%s\x1b[0m', "*********************************************************************");
console.log('\x1b[36m%s\x1b[0m', "");
console.log("DCS Olympus configurator {{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}}");
console.log("");
/* Run the configurator */
if (args.directory) {
jsonPath = path.join(args.directory, "olympus.json");
}
else {
/* Automatically detect possible DCS installation folders */
regedit.list(shellFoldersKey, function(err, result) {
if (err) {
console.log(err);
}
else {
if (result[shellFoldersKey] !== undefined && result[shellFoldersKey]["exists"] && result[shellFoldersKey]['values'][saveGamesKey] !== undefined && result[shellFoldersKey]['values'][saveGamesKey]['value'] !== undefined)
{
const searchpath = result[shellFoldersKey]['values'][saveGamesKey]['value'];
const folders = fs.readdirSync(searchpath);
var options = [];
folders.forEach((folder) => {
if (fs.existsSync(path.join(searchpath, folder, "Logs", "dcs.log"))) {
options.push(folder);
}
})
console.log("The following DCS Saved Games folders have been automatically detected.")
options.forEach((folder, index) => {
console.log(`(${index + 1}) ${folder}`)
});
while (true) {
var newValue = prompt(`Please choose a folder onto which the configurator shall operate by typing the associated number: `)
result = Number(newValue);
if (!isNaN(result) && Number.isInteger(result) && result > 0 && result <= options.length) {
jsonPath = path.join(searchpath, options[result - 1], "Config", "olympus.json");
break;
}
else {
console.log(`Please type a number between 1 and ${options.length}`);
}
}
} else {
console.error("An error occured while trying to fetch the location of the DCS folder. Please type the folder location manually.")
jsonPath = path.join(prompt(`DCS Saved Games folder location: `), "olympus.json");
}
console.log(`Configurator will run on ${jsonPath}, if this is incorrect please restart the configurator`)
run();
}
})
}

4
client/copy.sh Executable file
View File

@ -0,0 +1,4 @@
cp ./node_modules/leaflet/dist/leaflet.css ./public/stylesheets/leaflet/leaflet.css
cp ./node_modules/leaflet-gesture-handling/dist/leaflet-gesture-handling.css ./public/stylesheets/leaflet/leaflet-gesture-handling.css
cp ./node_modules/leaflet.nauticscale/dist/leaflet.nauticscale.js ./public/javascripts/leaflet.nauticscale.js
cp ./node_modules/leaflet-path-drag/dist/L.Path.Drag.js ./public/javascripts/L.Path.Drag.js

View File

@ -44,6 +44,7 @@
"@types/node": "^18.16.1",
"@types/sortablejs": "^1.15.0",
"@types/svg-injector": "^0.0.29",
"ajv": "^8.12.0",
"babelify": "^10.0.0",
"browserify": "^17.0.0",
"concurrently": "^7.6.0",

View File

@ -60,11 +60,6 @@
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
@ -651,7 +646,7 @@ svg.leaflet-image-layer.leaflet-interactive path {
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {

View File

@ -8,24 +8,20 @@
z-index: 9999;
}
#map-contextmenu>div:nth-child(2) {
align-items: center;
/* #map-contextmenu>div:nth-child(n+4)>div {
width: 100%;
} */
#map-contextmenu .spawn-mode {
display: flex;
flex-direction: row;
flex-direction: column;
justify-content: space-between;
padding-right: 0px;
row-gap: 5px;
}
#map-contextmenu>div:nth-child(3) {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
padding-right: 0px;
}
#map-contextmenu>div:nth-child(n+4) {
align-items: center;
.ol-context-menu-panel {
display: flex;
flex-direction: column;
justify-content: space-between;
@ -33,10 +29,6 @@
padding: 20px;
}
#map-contextmenu>div:nth-child(n+4)>div {
width: 100%;
}
.contextmenu-advanced-options,
.contextmenu-metadata {
align-items: center;
@ -143,20 +135,6 @@
padding: 2px 5px;
}
/*
.ol-tag-CA {
background-color: #FF000022;
}
.ol-tag-Radar {
background-color: #00FF0022;
}
.ol-tag-IR {
background-color: #0000FF22;
}
*/
.unit-loadout-list {
min-width: 0;
}
@ -187,7 +165,65 @@
content: " (" attr(data-points) " points)";
}
.upper-bar svg>* {
#spawn-mode-tabs {
align-items: center;
column-gap: 6px;
display: flex;
position: absolute;
right: 0;
top:0;
translate: -6px -100%;
z-index: 9998;
}
#spawn-mode-tabs button {
align-items: center;
border-bottom:2px solid transparent;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-top-left-radius: var(--border-radius-sm);
border-top-right-radius: var(--border-radius-sm);
display: flex;
height:32px;
justify-content: center;
margin:0;
width:38px;
}
#spawn-mode-tabs button:hover {
background-color: var(--background-steel);
}
[data-coalition="blue"] + #spawn-mode-tabs button {
border-bottom-color: var(--primary-blue);
}
[data-coalition="red"] + #spawn-mode-tabs button {
border-bottom-color: var(--primary-red);
}
[data-coalition="neutral"] + #spawn-mode-tabs button {
border-bottom-color: var(--primary-neutral);
}
#spawn-mode-tabs button svg {
height:24px;
margin:6px;
width:24px;
}
.upper-bar {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
padding-right: 0px;
}
.upper-bar svg>*,
#spawn-mode-tabs button svg * {
fill: white;
}
@ -200,24 +236,78 @@
margin-left: auto;
}
#spawn-history-menu {
align-items: center;
flex-direction: column;
max-height: 300px;
row-gap: 6px;
}
#spawn-history-menu button {
align-items: center;
column-gap: 6px;
display:flex;
height:32px;
text-align: left;
padding:0;
width:100%;
}
#spawn-history-menu button span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#spawn-history-menu button svg {
border-radius: var(--border-radius-sm);
height:24px;
padding:4px;
width:24px;
}
#spawn-history-menu button:hover {
background-color: transparent;
text-decoration: underline;
}
#spawn-history-menu button:hover svg * {
fill:white !important;
}
#spawn-history-menu button[data-spawned-coalition="blue"] svg {
background-color: var(--primary-blue);
}
#spawn-history-menu button[data-spawned-coalition="red"] svg {
background-color: var(--primary-red);
}
#spawn-history-menu button[data-spawned-coalition="neutral"] svg {
background-color: var(--primary-neutral);
}
[data-coalition="blue"]#active-coalition-label,
[data-coalition="blue"].deploy-unit-button,
[data-coalition="blue"]#spawn-airbase-aircraft-button,
[data-coalition="blue"].create-iads-button {
[data-coalition="blue"].create-iads-button,
[data-coalition="blue"] + #spawn-mode-tabs button.selected {
background-color: var(--primary-blue)
}
[data-coalition="red"]#active-coalition-label,
[data-coalition="red"].deploy-unit-button,
[data-coalition="red"]#spawn-airbase-aircraft-button,
[data-coalition="red"].create-iads-button {
[data-coalition="red"].create-iads-button,
[data-coalition="red"] + #spawn-mode-tabs button.selected {
background-color: var(--primary-red)
}
[data-coalition="neutral"]#active-coalition-label,
[data-coalition="neutral"].deploy-unit-button,
[data-coalition="neutral"]#spawn-airbase-aircraft-button,
[data-coalition="neutral"].create-iads-button {
[data-coalition="neutral"].create-iads-button,
[data-coalition="neutral"] + #spawn-mode-tabs button.selected {
background-color: var(--primary-neutral)
}

View File

@ -171,6 +171,10 @@ button svg.fill-coalition[data-coalition="red"] * {
position: relative;
}
.ol-select[disabled] {
color: var(--ol-dialog-disabled-text-color);
}
.ol-select>.ol-select-value {
align-content: center;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
@ -215,6 +219,10 @@ button svg.fill-coalition[data-coalition="red"] * {
background-size: 100% 100%;
}
.ol-select[disabled]:not(.ol-select-image)>.ol-select-value:after {
opacity: 15%;
}
.ol-select:not(.ol-select-image)>.ol-select-value.ol-select-warning:after {
background-image: url("/resources/theme/images/icons/chevron-down-warning.svg") !important;
}
@ -1312,6 +1320,11 @@ dl.ol-data-grid dd {
margin: 4px 0;
}
.ol-dialog label[disabled] {
color: var(--ol-dialog-disabled-text-color)
}
.ol-dialog-content table th {
background-color: var(--background-grey);
color:white;
@ -1383,6 +1396,10 @@ dl.ol-data-grid dd {
text-align: center;
}
.ol-text-input input[disabled] {
color:var(--ol-dialog-disabled-text-color);
}
input[type=number] {
-moz-appearance: textfield;
appearance: textfield;
@ -1422,6 +1439,11 @@ input[type=number]::-webkit-outer-spin-button {
border: 1px solid white;
}
.ol-button-apply[disabled] {
border-color: var(--ol-dialog-disabled-text-color);
color:var(--ol-dialog-disabled-text-color);
}
.ol-button-apply::before {
content: "\2713";
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="12" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M169.4 470.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 370.8 224 64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 306.7L54.6 265.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z"/></svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M75 75L41 41C25.9 25.9 0 36.6 0 57.9V168c0 13.3 10.7 24 24 24H134.1c21.4 0 32.1-25.9 17-41l-30.8-30.8C155 85.5 203 64 256 64c106 0 192 86 192 192s-86 192-192 192c-40.8 0-78.6-12.7-109.7-34.4c-14.5-10.1-34.4-6.6-44.6 7.9s-6.6 34.4 7.9 44.6C151.2 495 201.7 512 256 512c141.4 0 256-114.6 256-256S397.4 0 256 0C185.3 0 121.3 28.7 75 75zm181 53c-13.3 0-24 10.7-24 24V256c0 6.4 2.5 12.5 7 17l72 72c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-65-65V152c0-13.3-10.7-24-24-24z"/></svg>

After

Width:  |  Height:  |  Size: 718 B

View File

@ -48,6 +48,8 @@
--ol-switch-off:#686868;
--ol-switch-undefined:#383838;
--ol-dialog-disabled-text-color: #ffffff20;
/*** General border radii **/
--border-radius-xs: 2px;
--border-radius-sm: 5px;

View File

@ -26,6 +26,23 @@ export const ROEs: string[] = ["free", "designated", "", "return", "hold"];
export const reactionsToThreat: string[] = ["none", "manoeuvre", "passive", "evade"];
export const emissionsCountermeasures: string[] = ["silent", "attack", "defend", "free"];
export const ERAS = [{
"name": "Early Cold War",
"chronologicalOrder": 2
}, {
"name": "Late Cold War",
"chronologicalOrder": 4
}, {
"name": "Mid Cold War",
"chronologicalOrder": 3
}, {
"name": "Modern",
"chronologicalOrder": 5
}, {
"name": "WW2",
"chronologicalOrder": 1
}];
export const ROEDescriptions: string[] = [
"Free (Attack anyone)",
"Designated (Attack the designated target only) \nWARNING: Ground and Navy units don't respect this ROE, it will be equivalent to weapons FREE.",
@ -204,16 +221,19 @@ export const MAP_MARKER_CONTROLS: MapMarkerVisibilityControl[] = [{
"toggles": ["dcs"],
"tooltip": "Toggle DCS-controlled units' visibility"
}, {
"category": "Aircraft",
"image": "visibility/aircraft.svg",
"name": "Aircraft",
"toggles": ["aircraft"],
"tooltip": "Toggle aircraft's visibility"
}, {
"category": "Helicopter",
"image": "visibility/helicopter.svg",
"name": "Helicopter",
"toggles": ["helicopter"],
"tooltip": "Toggle helicopters' visibility"
}, {
"category": "AirDefence",
"image": "visibility/groundunit-sam.svg",
"name": "Air defence",
"toggles": ["groundunit-sam"],
@ -224,6 +244,7 @@ export const MAP_MARKER_CONTROLS: MapMarkerVisibilityControl[] = [{
"toggles": ["groundunit"],
"tooltip": "Toggle ground units' visibility"
}, {
"category": "GroundUnit",
"image": "visibility/navyunit.svg",
"name": "Naval",
"toggles": ["navyunit"],

View File

@ -5,8 +5,10 @@ import { Switch } from "../controls/switch";
import { GAME_MASTER } from "../constants/constants";
import { CoalitionArea } from "../map/coalitionarea/coalitionarea";
import { AirDefenceUnitSpawnMenu, AircraftSpawnMenu, GroundUnitSpawnMenu, HelicopterSpawnMenu, NavyUnitSpawnMenu } from "../controls/unitspawnmenu";
import { Airbase } from "../mission/airbase";
import { SmokeMarker } from "../map/markers/smokemarker";
import { UnitSpawnTable } from "../interfaces";
import { getCategoryBlueprintIconSVG, getUnitDatabaseByCategory } from "../other/utils";
import { SVGInjector } from "@tanem/svg-injector";
/** The MapContextMenu is the main contextmenu shown to the user whenever it rightclicks on the map. It is the primary interaction method for the user.
* It allows to spawn units, create explosions and smoke, and edit CoalitionAreas.
@ -96,6 +98,8 @@ export class MapContextMenu extends ContextMenu {
this.getContainer()?.addEventListener("hide", () => this.#airDefenceUnitSpawnMenu.clearCirclesPreviews());
this.getContainer()?.addEventListener("hide", () => this.#groundUnitSpawnMenu.clearCirclesPreviews());
this.getContainer()?.addEventListener("hide", () => this.#navyUnitSpawnMenu.clearCirclesPreviews());
this.#setupHistory();
}
/** Show the contextmenu on top of the map, usually at the location where the user has clicked on it.
@ -257,4 +261,91 @@ export class MapContextMenu extends ContextMenu {
this.#groundUnitSpawnMenu.setCountries();
this.#navyUnitSpawnMenu.setCountries();
}
/** Handles all of the logic for historal logging.
*
*/
#setupHistory() {
/* Set up the tab clicks */
const spawnModes = this.getContainer()?.querySelectorAll(".spawn-mode");
const activeCoalitionLabel = document.getElementById("active-coalition-label");
const tabs = this.getContainer()?.querySelectorAll(".spawn-mode-tab");
// Default selected tab to the "spawn now" option
if (tabs) tabs[tabs.length-1].classList.add("selected");
tabs?.forEach((btn:Element) => {
btn.addEventListener("click", (ev:MouseEventInit) => {
// Highlight tab
tabs.forEach(tab => tab.classList.remove("selected"));
btn.classList.add("selected");
// Hide/reset
spawnModes?.forEach(div => div.classList.add("hide"));
const prevSiblings = [];
let prev = btn.previousElementSibling;
/* Tabs and content windows are assumed to be in the same order */
// Count previous
while ( prev ) {
prevSiblings.push(prev);
prev = prev.previousElementSibling;
}
// Show content
if (spawnModes && spawnModes[prevSiblings.length]) {
spawnModes[prevSiblings.length].classList.remove("hide");
}
// We don't want to see the "Spawn [coalition] unit" label
if (activeCoalitionLabel) activeCoalitionLabel.classList.toggle("hide", !btn.hasAttribute("data-show-label"));
});
});
const history = <HTMLDivElement>document.getElementById("spawn-history-menu");
const maxEntries = 20;
/** Listen for unit spawned **/
document.addEventListener( "unitSpawned", (ev:CustomEventInit) => {
const buttons = history.querySelectorAll("button");
const detail:any = ev.detail;
if (buttons.length === 0) history.innerHTML = ""; // Take out any "no data" messages
const button = document.createElement("button");
button.title = "Click to spawn";
button.setAttribute("data-spawned-coalition", detail.coalition);
button.setAttribute("data-unit-type", detail.unitSpawnTable[0].unitType);
button.setAttribute("data-unit-qty", detail.unitSpawnTable.length);
const db = getUnitDatabaseByCategory(detail.category);
button.innerHTML = `<img src="${getCategoryBlueprintIconSVG(detail.category, detail.unitSpawnTable[0].unitType)}" /><span>${db?.getByName(detail.unitSpawnTable[0].unitType)?.label} (${detail.unitSpawnTable.length})</span>`;
// Remove a previous instance to save clogging up the list
const previous:any = [].slice.call(buttons).find( (button:Element) => (
detail.coalition === button.getAttribute("data-spawned-coalition") &&
detail.unitSpawnTable[0].unitType === button.getAttribute("data-unit-type") &&
detail.unitSpawnTable.length === parseInt(button.getAttribute("data-unit-qty") || "-1")));
if (previous instanceof HTMLElement) previous.remove();
/* Click to do the spawn */
button.addEventListener("click", (ev:MouseEventInit) => {
detail.unitSpawnTable.forEach((table:UnitSpawnTable, i:number) => {
table.location = this.getLatLng(); // Set to new menu location
table.location.lat += 0.00015 * i;
});
getApp().getUnitsManager().spawnUnits(detail.category, detail.unitSpawnTable, detail.coalition, detail.immediate, detail.airbase, detail.country);
this.hide();
});
/* Insert into DOM */
history.prepend(button);
SVGInjector(button.querySelectorAll("img"));
/* Trim down to max number of entries */
while (history.querySelectorAll("button").length > maxEntries) {
history.childNodes[maxEntries].remove();
}
});
}
}

View File

@ -26,7 +26,9 @@ export class Dropdown {
if (options != null) this.setOptions(options);
(this.#container.querySelector(".ol-select-value") as HTMLElement)?.addEventListener("click", (ev) => { this.#toggle(); });
(this.#container.querySelector(".ol-select-value") as HTMLElement)?.addEventListener("click", (ev) => {
if (!this.#container.hasAttribute("disabled") && !this.#container.closest("disabled")) this.#toggle();
});
document.addEventListener("click", (ev) => {
if (!(this.#value.contains(ev.target as Node) || this.#options.contains(ev.target as Node) || this.#container.contains(ev.target as Node))) {

View File

@ -38,6 +38,7 @@ require("../../public/javascripts/leaflet.nauticscale.js")
require("../../public/javascripts/L.Path.Drag.js")
export type MapMarkerVisibilityControl = {
"category"?: string;
"image": string;
"isProtected"?: boolean,
"name": string,

View File

@ -2,7 +2,7 @@ import { LatLng } from "leaflet";
import { getApp } from "..";
import { Airbase } from "./airbase";
import { Bullseye } from "./bullseye";
import { BLUE_COMMANDER, GAME_MASTER, NONE, RED_COMMANDER } from "../constants/constants";
import { BLUE_COMMANDER, ERAS, GAME_MASTER, NONE, RED_COMMANDER } from "../constants/constants";
import { Dropdown } from "../controls/dropdown";
import { groundUnitDatabase } from "../unit/databases/groundunitdatabase";
import { createCheckboxOption, getCheckboxOptions } from "../other/utils";
@ -26,13 +26,16 @@ export class MissionManager {
#coalitions: {red: string[], blue: string[]} = {red: [], blue: []};
constructor() {
document.addEventListener("showCommandModeDialog", () => this.showCommandModeDialog());
document.addEventListener("applycommandModeOptions", () => this.#applycommandModeOptions());
document.addEventListener("showCommandModeDialog", () => this.showCommandModeDialog());
document.addEventListener("toggleSpawnRestrictions", (ev:CustomEventInit) => {
this.#toggleSpawnRestrictions(ev.detail._element.checked)
});
/* command-mode settings dialog */
this.#commandModeDialog = document.querySelector("#command-mode-settings-dialog") as HTMLElement;
this.#commandModeErasDropdown = new Dropdown("command-mode-era-options", () => {});
}
/** Update location of bullseyes
@ -211,12 +214,18 @@ export class MissionManager {
}
showCommandModeDialog() {
const options = this.getCommandModeOptions()
const { restrictSpawns, restrictToCoalition, setupTime } = options;
this.#toggleSpawnRestrictions(restrictSpawns);
/* Create the checkboxes to select the unit eras */
var eras = aircraftDatabase.getEras().concat(helicopterDatabase.getEras()).concat(groundUnitDatabase.getEras()).concat(navyUnitDatabase.getEras());
eras = eras.filter((item: string, index: number) => eras.indexOf(item) === index).sort();
this.#commandModeErasDropdown.setOptionsElements(eras.map((era: string) => {
return createCheckboxOption(era, `Enable ${era} units spawns`, this.getCommandModeOptions().eras.includes(era));
}));
this.#commandModeErasDropdown.setOptionsElements(
ERAS.sort((eraA, eraB) => {
return ( eraA.chronologicalOrder > eraB.chronologicalOrder ) ? 1 : -1;
}).map((era) => {
return createCheckboxOption(era.name, `Enable ${era} units spawns`, this.getCommandModeOptions().eras.includes(era.name));
})
);
this.#commandModeDialog.classList.remove("hide");
@ -226,11 +235,11 @@ export class MissionManager {
const redSpawnPointsInput = this.#commandModeDialog.querySelector("#red-spawn-points")?.querySelector("input") as HTMLInputElement;
const setupTimeInput = this.#commandModeDialog.querySelector("#setup-time")?.querySelector("input") as HTMLInputElement;
restrictSpawnsCheckbox.checked = this.getCommandModeOptions().restrictSpawns;
restrictToCoalitionCheckbox.checked = this.getCommandModeOptions().restrictToCoalition;
blueSpawnPointsInput.value = String(this.getCommandModeOptions().spawnPoints.blue);
redSpawnPointsInput.value = String(this.getCommandModeOptions().spawnPoints.red);
setupTimeInput.value = String(Math.floor(this.getCommandModeOptions().setupTime / 60.0));
restrictSpawnsCheckbox.checked = restrictSpawns;
restrictToCoalitionCheckbox.checked = restrictToCoalition;
blueSpawnPointsInput.value = String(options.spawnPoints.blue);
redSpawnPointsInput.value = String(options.spawnPoints.red);
setupTimeInput.value = String(Math.floor(setupTime / 60.0));
}
#applycommandModeOptions() {
@ -309,4 +318,10 @@ export class MissionManager {
};
xhr.send();
}
#toggleSpawnRestrictions(restrictionsEnabled:boolean) {
this.#commandModeDialog.querySelectorAll("input, label, .ol-select").forEach( el => {
if (!el.closest("#restrict-spawns")) el.toggleAttribute("disabled", !restrictionsEnabled);
});
}
}

View File

@ -18,6 +18,7 @@ import { Manager } from "./other/manager";
import { SVGInjector } from "@tanem/svg-injector";
import { ServerManager } from "./server/servermanager";
import { sha256 } from 'js-sha256';
import Ajv from "ajv"
import { BLUE_COMMANDER, FILL_SELECTED_RING, GAME_MASTER, HIDE_UNITS_SHORT_RANGE_RINGS, RED_COMMANDER, SHOW_UNITS_ACQUISITION_RINGS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNIT_LABELS } from "./constants/constants";
import { aircraftDatabase } from "./unit/databases/aircraftdatabase";
@ -27,6 +28,8 @@ import { navyUnitDatabase } from "./unit/databases/navyunitdatabase";
import { UnitListPanel } from "./panels/unitlistpanel";
import { ContextManager } from "./context/contextmanager";
import { Context } from "./context/context";
import { AirDefenceUnitSpawnMenu } from "./controls/unitspawnmenu";
import { AirbasesJSONSchemaValidator } from "./schemas/schema";
var VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
@ -193,6 +196,9 @@ export class OlympusApp {
this.#unitsManager = new UnitsManager();
this.#weaponsManager = new WeaponsManager();
/* Validate data */
this.#validateData();
// Toolbars
this.getToolbarsManager().add("primaryToolbar", new PrimaryToolbar("primary-toolbar"))
.add("commandModeToolbar", new CommandModeToolbar("command-mode-toolbar"));
@ -447,4 +453,28 @@ export class OlympusApp {
img.addEventListener("load", () => { SVGInjector(img); });
})
}
#validateData() {
const airbasesValidator = new AirbasesJSONSchemaValidator();
/*
const validator = new Ajv();
const schema = {
type: "object",
properties: {
foo: {type: "integer"},
bar: {type: "string"},
},
required: ["foo"],
additionalProperties: false,
}
const data = this.#getRunwayData();
const validate = validator.compile(schema);
const valid = validate(data);
if (!valid) console.log(validate.errors);
//*/
}
}

View File

@ -5,7 +5,7 @@ import { aircraftDatabase } from "../unit/databases/aircraftdatabase";
import { helicopterDatabase } from "../unit/databases/helicopterdatabase";
import { groundUnitDatabase } from "../unit/databases/groundunitdatabase";
import { Buffer } from "buffer";
import { ROEs, emissionsCountermeasures, reactionsToThreat, states } from "../constants/constants";
import { GROUND_UNIT_AIR_DEFENCE_REGEX, ROEs, emissionsCountermeasures, reactionsToThreat, states } from "../constants/constants";
import { Dropdown } from "../controls/dropdown";
import { navyUnitDatabase } from "../unit/databases/navyunitdatabase";
import { DateAndTime, UnitBlueprint } from "../interfaces";
@ -379,6 +379,20 @@ export function getUnitDatabaseByCategory(category: string) {
return null;
}
export function getCategoryBlueprintIconSVG(category:string, unitName:string) {
const path = "/resources/theme/images/buttons/visibility/";
// We can just send these back okay
if (["Aircraft", "Helicopter", "NavyUnit"].includes(category)) return `${path}${category.toLowerCase()}.svg`;
// Return if not a ground units as it's therefore something we don't recognise
if (category !== "GroundUnit") return false;
/** We need to get the unit detail for ground units so we can work out if it's an air defence unit or not **/
return GROUND_UNIT_AIR_DEFENCE_REGEX.test(unitName) ? `${path}groundunit-sam.svg` : `${path}groundunit.svg`;
}
export function base64ToBytes(base64: string) {
return Buffer.from(base64, 'base64').buffer;
}

View File

@ -0,0 +1,67 @@
{
"type": "object",
"additionalProperties": false,
"properties": {
"airfields": {
"type": "object",
"minProperties": 1,
"patternProperties": {
".*": {
"type": "object",
"properties": {
"elevation": {
"type": "string",
"pattern": "^(0|([1-9][0-9]*))?$"
},
"ICAO": {
"type": "string"
},
"runways": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"headings": {
"type": "array",
"items": {
"type": "object",
"patternProperties": {
".*": {
"type": "object",
"properties": {
"ILS": {
"type": "string",
"pattern": "^(1[0-9]{1,2}\\.[0-9][05])?$"
},
"magHeading": {
"type": "string",
"pattern": "^([0-2][0-9]{2})|(3(([0-5][0-9])|(60)))?$"
}
},
"required": ["magHeading"]
}
}
},
"minItems": 1
},
"length": {
"type": "string",
"pattern": "^[1-9][0-9]{3,4}$"
}
},
"required": [ "headings", "length" ]
}
},
"TACAN": {
"type": "string",
"pattern": "^([1-9][0-9]{1,2}X)?$"
}
},
"required": [ "elevation", "runways" ]
}
}
}
},
"required": ["airfields"]
}

View File

@ -0,0 +1,425 @@
{
"$defs": {
"coalitionName": {
"enum": [
"blue",
"neutral",
"red"
],
"type": "string"
},
"lat": {
"maximum": 90,
"minimum": -90,
"type": "number"
},
"lng": {
"maximum": 180,
"minimum": -180,
"type": "number"
},
"vec2": {
"additionalProperties": false,
"properties": {
"lat": {
"$ref": "#/$defs/lat"
},
"lng": {
"$ref": "#/$defs/lng"
}
},
"required": [
"lat",
"lng"
],
"type": "object"
},
"vec3": {
"additionalProperties": false,
"properties": {
"alt": {
"type": "number"
},
"lat": {
"$ref": "#/$defs/lat"
},
"lng": {
"$ref": "#/$defs/lng"
}
},
"required": [
"alt",
"lat",
"lng"
],
"type": "object"
}
},
"patternProperties": {
".*": {
"items": {
"additionalProperties": false,
"properties": {
"activePath": {
"items": {
"$ref": "#/$defs/vec3"
},
"type": "array"
},
"alive": {
"type": "boolean"
},
"ammo": {
"items": {
"additionalProperties": false,
"properties": {
"category": {
"minimum": 0,
"type": "number"
},
"guidance": {
"minimum": 0,
"type": "number"
},
"missileCategory": {
"minimum": 0,
"type": "number"
},
"name": {
"minLength": 3,
"type": "string"
},
"quantity": {
"minimum": 0,
"type": "number"
}
},
"required": [
"quantity",
"name",
"guidance",
"category",
"missileCategory"
],
"type": "object"
},
"type": "array"
},
"category": {
"type": "string"
},
"categoryDisplayName": {
"type": "string"
},
"coalition": {
"$ref": "#/$defs/coalitionName"
},
"contacts": {
"type": "array"
},
"controlled": {
"type": "boolean"
},
"country": {
"type": "number"
},
"desiredAltitude": {
"minimum": 0,
"type": "number"
},
"desiredAltitudeType": {
"enum": [
"AGL",
"ASL"
],
"type": "string"
},
"desiredSpeed": {
"minimum": 0,
"type": "number"
},
"desiredSpeedType": {
"enum": [
"CAS",
"GS"
],
"type": "string"
},
"emissionsCountermeasures": {
"enum": [
"attac",
"defend",
"free",
"silent"
],
"type": "string"
},
"followRoads": {
"type": "boolean"
},
"formationOffset": {
"additionalProperties": false,
"properties": {
"x": {
"minimum": 0,
"type": "number"
},
"y": {
"minimum": 0,
"type": "number"
},
"z": {
"minimum": 0,
"type": "number"
}
},
"required": [
"x",
"y",
"z"
],
"type": "object"
},
"fuel": {
"maximum": 100,
"minimum": 0,
"type": "number"
},
"generalSettings": {
"additionalProperties": false,
"properties": {
"prohibitAA": {
"type": "boolean"
},
"prohibitAfterburner": {
"type": "boolean"
},
"prohibitAG": {
"type": "boolean"
},
"prohibitAirWpn": {
"type": "boolean"
},
"prohibitJettison": {
"type": "boolean"
}
},
"required": [
"prohibitAA",
"prohibitAfterburner",
"prohibitAG",
"prohibitAirWpn",
"prohibitJettison"
],
"type": "object"
},
"groupName": {
"type": "string"
},
"hasTask": {
"type": "boolean"
},
"heading": {
"type": "number"
},
"health": {
"maximum": 100,
"minimum": 0,
"type": "number"
},
"horizontalVelocity": {
"minimum": 0,
"type": "number"
},
"human": {
"type": "boolean"
},
"ID": {
"type": "number"
},
"isActiveAWACS": {
"type": "boolean"
},
"isActiveTanker": {
"type": "boolean"
},
"isLeader": {
"type": "boolean"
},
"leaderID": {
"minimum": 0,
"type": "number"
},
"name": {
"type": "string"
},
"onOff": {
"type": "boolean"
},
"operateAs": {
"$ref": "#/$defs/coalitionName"
},
"position": {
"$ref": "#/$defs/vec3"
},
"radio": {
"additionalProperties": false,
"properties": {
"callsign": {
"type": "number"
},
"callsignNumber": {
"type": "number"
},
"frequency": {
"type": "number"
}
},
"required": [
"frequency",
"callsign",
"callsignNumber"
],
"type": "object"
},
"reactionToThreat": {
"enum": [
"evade",
"maneouvre",
"none",
"passive"
],
"type": "string"
},
"ROE": {
"enum": [
"designated",
"free",
"hold",
"return"
],
"type": "string"
},
"shotsIntensity": {
"maximum": 3,
"minimum": 1,
"type": "number"
},
"shotsScatter": {
"maximum": 3,
"minimum": 1,
"type": "number"
},
"speed": {
"minimum": 0,
"type": "number"
},
"state": {
"type": "string"
},
"TACAN": {
"properties": {
"callsign": {
"type": "string"
},
"channel": {
"minimum": 0,
"type": "number"
},
"isOn": {
"type": "boolean"
},
"XY": {
"enum": [
"X",
"Y"
],
"type": "string"
}
},
"required": [
"callsign",
"channel",
"isOn",
"XY"
],
"type": "object"
},
"targetID": {
"minimum": 0,
"type": "number"
},
"targetPosition": {
"$ref": "#/$defs/vec2"
},
"task": {
"type": "string"
},
"track": {
"type": "number"
},
"unitName": {
"type": "string"
},
"verticalVelocity": {
"minimum": 0,
"type": "number"
}
},
"type": "object",
"required": [
"activePath",
"alive",
"ammo",
"category",
"categoryDisplayName",
"coalition",
"contacts",
"controlled",
"country",
"desiredAltitude",
"desiredAltitudeType",
"desiredSpeed",
"desiredSpeedType",
"emissionsCountermeasures",
"followRoads",
"formationOffset",
"fuel",
"generalSettings",
"groupName",
"hasTask",
"heading",
"health",
"horizontalVelocity",
"human",
"ID",
"isActiveAWACS",
"isActiveTanker",
"isLeader",
"leaderID",
"name",
"onOff",
"operateAs",
"position",
"radio",
"reactionToThreat",
"ROE",
"shotsIntensity",
"shotsScatter",
"speed",
"state",
"TACAN",
"targetID",
"targetPosition",
"task",
"track",
"unitName",
"verticalVelocity"
]
},
"minItems": 1,
"type": "array"
}
},
"type": "object"
}

View File

@ -0,0 +1,82 @@
import Ajv from "ajv";
import { AnySchemaObject } from "ajv/dist/core";
// For future extension
abstract class JSONSchemaValidator {
#ajv:Ajv;
#compiledValidator:any;
#schema!:AnySchemaObject;
constructor( schema:AnySchemaObject ) {
this.#schema = schema;
this.#ajv = new Ajv({
"allErrors": true
});
this.#compiledValidator = this.getAjv().compile(this.getSchema());
}
getAjv() {
return this.#ajv;
}
getCompiledValidator() {
return this.#compiledValidator;
}
getErrors() {
return this.getCompiledValidator().errors;
}
getSchema() {
return this.#schema;
}
validate(data:any) {
return (this.getCompiledValidator())(data);
}
}
export class AirbasesJSONSchemaValidator extends JSONSchemaValidator {
constructor() {
const schema = require("../schemas/airbases.schema.json");
super( schema );
[
require( "../../public/databases/airbases/caucasus.json" ),
require( "../../public/databases/airbases/falklands.json" ),
require( "../../public/databases/airbases/marianas.json" ),
require( "../../public/databases/airbases/nevada.json" ),
require( "../../public/databases/airbases/normandy.json" ),
require( "../../public/databases/airbases/persiangulf.json" ),
require( "../../public/databases/airbases/sinaimap.json" ),
require( "../../public/databases/airbases/syria.json" ),
require( "../../public/databases/airbases/thechannel.json" )
].forEach( data => {
const validate = this.getAjv().compile(this.getSchema());
const valid = validate(data);
if (!valid) console.error(validate.errors);
});
}
}
export class ImportFileJSONSchemaValidator extends JSONSchemaValidator {
constructor() {
const schema = require("../schemas/importdata.schema.json");
super( schema );
}
}

View File

@ -25,6 +25,10 @@ export class UnitDataFileExport extends UnitDataFile {
* Show the form to start the export journey
*/
showForm(units: Unit[]) {
this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => {
el.classList.toggle("hide", el.getAttribute("data-on-error") === "show");
});
const data: any = {};
const unitCanBeExported = (unit: Unit) => !["Aircraft", "Helicopter"].includes(unit.getCategory());

View File

@ -1,6 +1,7 @@
import { getApp } from "../..";
import { Dialog } from "../../dialog/dialog";
import { UnitData } from "../../interfaces";
import { ImportFileJSONSchemaValidator } from "../../schemas/schema";
import { UnitDataFile } from "./unitdatafile";
export class UnitDataFileImport extends UnitDataFile {
@ -48,21 +49,65 @@ export class UnitDataFileImport extends UnitDataFile {
unitsManager.spawnUnits(category, unitsToSpawn, coalition, false);
}
}
/*
for (let groupName in groups) {
if (groupName !== "" && groups[groupName].length > 0 && (groups[groupName].every((unit: UnitData) => { return unit.category == "GroundUnit"; }) || groups[groupName].every((unit: any) => { return unit.category == "NavyUnit"; }))) {
var aliveUnits = groups[groupName].filter((unit: UnitData) => { return unit.alive });
var units = aliveUnits.map((unit: UnitData) => {
return { unitType: unit.name, location: unit.position, liveryID: "" }
});
getApp().getUnitsManager().spawnUnits(groups[groupName][0].category, units, groups[groupName][0].coalition, true);
selectFile() {
var input = document.createElement("input");
input.type = "file";
input.addEventListener("change", (e: any) => {
var file = e.target.files[0];
if (!file) {
return;
}
}
//*/
var reader = new FileReader();
reader.onload = (e: any) => {
try {
this.#fileData = JSON.parse(e.target.result);
const validator = new ImportFileJSONSchemaValidator();
if (!validator.validate(this.#fileData)) {
const errors = validator.getErrors().reduce((acc:any, error:any) => {
let errorString = error.instancePath.substring(1) + ": " + error.message;
if (error.params) {
const {allowedValues} = error.params;
if (allowedValues)
errorString += ": " + allowedValues.join(', ');
}
acc.push(errorString);
return acc;
}, [] as string[]);
this.#showFileDataErrors(errors);
} else {
this.#showForm();
}
} catch(e:any) {
this.#showFileDataErrors([e]);
}
};
reader.readAsText(file);
})
input.click();
}
#showFileDataErrors( reasons:string[]) {
this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => {
el.classList.toggle("hide", el.getAttribute("data-on-error") === "hide");
});
const reasonsList = this.dialog.getElement().querySelector(".import-error-reasons");
if (reasonsList instanceof HTMLElement)
reasonsList.innerHTML = `<li>${reasons.join("</li><li>")}</li>`;
this.dialog.show();
}
#showForm() {
this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => {
el.classList.toggle("hide", el.getAttribute("data-on-error") === "show");
});
const data: any = {};
for (const [group, units] of Object.entries(this.#fileData)) {
@ -87,44 +132,11 @@ export class UnitDataFileImport extends UnitDataFile {
}
/*
groups.filter((unit:Unit) => unitCanBeImported(unit)).forEach((unit:Unit) => {
const category = unit.getCategoryLabel();
const coalition = unit.getCoalition();
if (!data.hasOwnProperty(category)) {
data[category] = {};
}
if (!data[category].hasOwnProperty(coalition))
data[category][coalition] = [];
data[category][coalition].push(unit);
});
//*/
this.data = data;
this.buildCategoryCoalitionTable();
this.dialog.show();
}
selectFile() {
var input = document.createElement("input");
input.type = "file";
input.addEventListener("change", (e: any) => {
var file = e.target.files[0];
if (!file) {
return;
}
var reader = new FileReader();
reader.onload = (e: any) => {
this.#fileData = JSON.parse(e.target.result);
this.#showForm();
};
reader.readAsText(file);
})
input.click();
}
#unitDataCanBeImported(unitData: UnitData) {
return unitData.alive && this.#unitGroupDataCanBeImported([unitData]);
}

View File

@ -1427,6 +1427,16 @@ export class UnitsManager {
if (spawnPoints <= getApp().getMissionManager().getAvailableSpawnPoints()) {
getApp().getMissionManager().setSpentSpawnPoints(spawnPoints);
spawnFunction();
document.dispatchEvent( new CustomEvent( "unitSpawned", {
"detail": {
"airbase": airbase,
"category": category,
"coalition": coalition,
"country": country,
"immediate": immediate,
"unitSpawnTable": units
}
}));
return true;
} else {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Not enough spawn points available!");

View File

@ -1,58 +1,71 @@
<div id="map-contextmenu" class="ol-context-menu" oncontextmenu="return false;">
<div id="active-coalition-label" data-coalition="blue"></div>
<div class="upper-bar ol-panel">
<div class="switch-control coalition no-label"><div id="coalition-switch" class="ol-switch"></div></div>
<button data-coalition="blue" id="aircraft-spawn-button" title="Spawn aircraft" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "aircraft" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/aircraft.svg" inject-svg></button>
<button data-coalition="blue" id="helicopter-spawn-button" title="Spawn helicopter" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "helicopter" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/helicopter.svg" inject-svg></button>
<button data-coalition="blue" id="air-defence-spawn-button" title="Spawn air defence unit" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "air-defence" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/sam.svg" inject-svg></button>
<button data-coalition="blue" id="groundunit-spawn-button" title="Spawn ground unit" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "groundunit" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/groundunit.svg" inject-svg></button>
<button data-coalition="blue" id="coalition-area-button" title="Edit coalition area" data-on-click="editCoalitionArea"
class="ol-context-menu-button"><img src="/resources/theme/images/buttons/other/edit.svg" inject-svg></button>
<button data-coalition="blue" id="more-options-button" title="More options" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "more" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/more.svg" inject-svg></button>
<div id="spawn-mode-tabs">
<button id="spawn-tab-history" class="ol-context-menu-button spawn-mode-tab"><img src="resources/theme/images/buttons/other/clock-rotate-left-solid.svg" inject-svg></button>
<button id="spawn-tab-new" class="ol-context-menu-button spawn-mode-tab" data-show-label><img src="resources/theme/images/buttons/other/arrow-down-solid.svg" inject-svg></button>
</div>
<div id="more-options-button-bar" class="upper-bar ol-panel hide">
<button data-coalition="blue" id="navyunit-spawn-button" title="Spawn navy unit" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "navyunit" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/navyunit.svg" inject-svg></button>
<button data-coalition="blue" id="smoke-spawn-button" title="Spawn smoke" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "smoke" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/smoke.svg" inject-svg></button>
<button data-coalition="blue" id="explosion-spawn-button" title="Explosion" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "explosion" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/explosion.svg" inject-svg></button>
<button data-coalition="blue" id="polygon-draw-button" title="Enter polygon draw mode" data-on-click="toggleCoalitionAreaDraw"
data-on-click-params='{"type": "polygon"}' class="ol-context-menu-button"><img src="resources/theme/images/buttons/tools/draw-polygon-solid.svg" inject-svg></button>
<div class="spawn-mode ol-panel ol-context-menu-panel hide">
<h4>Spawn history</h4>
<div id="spawn-history-menu" class="ol-scrollable">
<p>You do not have any units to show.</p>
</div>
<div id="aircraft-spawn-menu" class="ol-context-menu-panel ol-panel hide">
<!-- Here the aircraft spawn menu will be shown -->
</div>
<div id="helicopter-spawn-menu" class="ol-context-menu-panel ol-panel hide">
<!-- Here the helicopter spawn menu will be shown -->
</div>
<div id="air-defence-spawn-menu" class="ol-panel ol-context-menu-panel hide">
<!-- Here the air defence units' spawn menu will be shown -->
</div>
<div id="groundunit-spawn-menu" class="ol-panel ol-context-menu-panel hide">
<!-- Here the ground units' spawn menu will be shown -->
</div>
<div id="navyunit-spawn-menu" class="ol-panel ol-context-menu-panel hide">
<!-- Here the navy units' spawn menu will be shown -->
</div>
<div id="smoke-spawn-menu" class="ol-panel ol-context-menu-panel hide">
<button class="smoke-button" title="" data-smoke-color="white" data-on-click="contextMenuDeploySmoke" data-on-click-params='{ "color": "white" }'>White smoke</button>
<button class="smoke-button" title="" data-smoke-color="blue" data-on-click="contextMenuDeploySmoke" data-on-click-params='{ "color": "blue" }'>Blue smoke</button>
<button class="smoke-button" title="" data-smoke-color="red" data-on-click="contextMenuDeploySmoke" data-on-click-params='{ "color": "red" }'>Red smoke</button>
<button class="smoke-button" title="" data-smoke-color="green" data-on-click="contextMenuDeploySmoke" data-on-click-params='{ "color": "green" }'>Green smoke</button>
<button class="smoke-button" title="" data-smoke-color="orange" data-on-click="contextMenuDeploySmoke" data-on-click-params='{ "color": "orange" }'>Orange smoke</button>
</div>
<div id="explosion-menu" class="ol-panel ol-context-menu-panel hide">
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "explosionType": "normal", "strength": 1 }'>Small explosion</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "explosionType": "normal", "strength": 10 }'>Big explosion</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "explosionType": "phosphorous"}'>White phosphorous</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "explosionType": "napalm"}'>Napalm</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "explosionType": "secondary"}'>Explosion with debries</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "explosionType": "fire"}'>Static fire</button>
<div class="spawn-mode">
<div class="upper-bar ol-panel">
<div class="switch-control coalition no-label"><div id="coalition-switch" class="ol-switch"></div></div>
<button data-coalition="blue" id="aircraft-spawn-button" title="Spawn aircraft" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "aircraft" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/aircraft.svg" inject-svg></button>
<button data-coalition="blue" id="helicopter-spawn-button" title="Spawn helicopter" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "helicopter" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/helicopter.svg" inject-svg></button>
<button data-coalition="blue" id="air-defence-spawn-button" title="Spawn air defence unit" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "air-defence" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/sam.svg" inject-svg></button>
<button data-coalition="blue" id="groundunit-spawn-button" title="Spawn ground unit" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "groundunit" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/groundunit.svg" inject-svg></button>
<button data-coalition="blue" id="coalition-area-button" title="Edit coalition area" data-on-click="editCoalitionArea"
class="ol-context-menu-button"><img src="/resources/theme/images/buttons/other/edit.svg" inject-svg></button>
<button data-coalition="blue" id="more-options-button" title="More options" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "more" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/more.svg" inject-svg></button>
</div>
<div id="more-options-button-bar" class="upper-bar ol-panel hide">
<button data-coalition="blue" id="navyunit-spawn-button" title="Spawn navy unit" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "navyunit" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/navyunit.svg" inject-svg></button>
<button data-coalition="blue" id="smoke-spawn-button" title="Spawn smoke" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "smoke" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/smoke.svg" inject-svg></button>
<button data-coalition="blue" id="explosion-spawn-button" title="Explosion" data-on-click="mapContextMenuShow"
data-on-click-params='{ "type": "explosion" }' class="ol-context-menu-button"><img src="/resources/theme/images/buttons/spawn/explosion.svg" inject-svg></button>
<button data-coalition="blue" id="polygon-draw-button" title="Enter polygon draw mode" data-on-click="toggleCoalitionAreaDraw"
data-on-click-params='{"type": "polygon"}' class="ol-context-menu-button"><img src="resources/theme/images/buttons/tools/draw-polygon-solid.svg" inject-svg></button>
</div>
<div id="aircraft-spawn-menu" class="ol-context-menu-panel ol-panel hide">
<!-- Here the aircraft spawn menu will be shown -->
</div>
<div id="helicopter-spawn-menu" class="ol-context-menu-panel ol-panel hide">
<!-- Here the helicopter spawn menu will be shown -->
</div>
<div id="air-defence-spawn-menu" class="ol-panel ol-context-menu-panel hide">
<!-- Here the air defence units' spawn menu will be shown -->
</div>
<div id="groundunit-spawn-menu" class="ol-panel ol-context-menu-panel hide">
<!-- Here the ground units' spawn menu will be shown -->
</div>
<div id="navyunit-spawn-menu" class="ol-panel ol-context-menu-panel hide">
<!-- Here the navy units' spawn menu will be shown -->
</div>
<div id="smoke-spawn-menu" class="ol-panel ol-context-menu-panel hide">
<button class="smoke-button" title="" data-smoke-color="white" data-on-click="contextMenuDeploySmoke" data-on-click-params='{ "color": "white" }'>White smoke</button>
<button class="smoke-button" title="" data-smoke-color="blue" data-on-click="contextMenuDeploySmoke" data-on-click-params='{ "color": "blue" }'>Blue smoke</button>
<button class="smoke-button" title="" data-smoke-color="red" data-on-click="contextMenuDeploySmoke" data-on-click-params='{ "color": "red" }'>Red smoke</button>
<button class="smoke-button" title="" data-smoke-color="green" data-on-click="contextMenuDeploySmoke" data-on-click-params='{ "color": "green" }'>Green smoke</button>
<button class="smoke-button" title="" data-smoke-color="orange" data-on-click="contextMenuDeploySmoke" data-on-click-params='{ "color": "orange" }'>Orange smoke</button>
</div>
<div id="explosion-menu" class="ol-panel ol-context-menu-panel hide">
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "explosionType": "normal", "strength": 1 }'>Small explosion</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "explosionType": "normal", "strength": 10 }'>Big explosion</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "explosionType": "phosphorous"}'>White phosphorous</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "explosionType": "napalm"}'>Napalm</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "explosionType": "secondary"}'>Explosion with debries</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "explosionType": "fire"}'>Static fire</button>
</div>
</div>
</div>

View File

@ -8,11 +8,13 @@
<div class="ol-dialog-content">
<div id="restrict-spawns" class="ol-checkbox">
<label title="If false, no spawn restrictions will be applied">
<input type="checkbox"/>
Restrict spawns
<input type="checkbox" data-on-click="toggleSpawnRestrictions"/>
Enable spawn restrictions
</label>
</div>
<hr />
<div id="restrict-to-coalition" class="ol-checkbox">
<label title="If true, commanders will be allowed to only spawn units that belong to their coalition. E.g. blue commanders will be able to spawn F/A-18 Hornets, but not MiG-29s.">
<input type="checkbox"/>
@ -25,7 +27,7 @@
<div class="ol-group">
<div id="setup-time" class="ol-text-input">
<input type="number" min="-99999" max="99999" step="1" value="10">
<input type="number" min="-99999" max="99999" step="1" value="10" />
</div>
<label>minutes</label>
</div>

View File

@ -4,26 +4,40 @@
</div>
<div class="ol-dialog-content">
<p><%= textContent %></p>
<% if (showFilenameInput) { %>
<div class="export-filename-container">
<label>Filename:</label>
<input id="export-filename">
<img src="resources/theme/images/icons/keyboard-solid.svg">
</div>
<% } %>
<div class="import-form" data-on-error="hide">
<p><%= textContent %></p>
<% if (showFilenameInput) { %>
<div class="export-filename-container">
<label>Filename:</label>
<input id="export-filename">
<img src="resources/theme/images/icons/keyboard-solid.svg">
</div>
<% } %>
<table class="categories-coalitions">
<thead>
</thead>
<tbody>
</tbody>
</table>
</div>
<table class="categories-coalitions">
<thead>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="import-errors" data-on-error="show">
<div class="ol-dialog-footer ol-group">
<button class="start-transfer"><%= submitButtonText %></button>
<button data-on-click="closeDialog">Close</button>
<p>Data could not be imported because:</p>
<ul class="import-error-reasons"></ul>
<div>Please correct the error(s) and run the import again.</div>
</div>
<div class="ol-dialog-footer ol-group">
<button class="start-transfer" data-on-error="hide"><%= submitButtonText %></button>
<button data-on-click="closeDialog">Close</button>
</div>
</div>
</div>

9
docker-compose.yml Normal file
View File

@ -0,0 +1,9 @@
version: '3'
services:
client:
build: client
ports:
- 3000:3000
volumes:
- ./olympus.json:/usr/src/olympus.json