Started readding old core frontend code

This commit is contained in:
Pax1601 2024-04-03 08:05:36 +02:00
parent 5d5c650162
commit d72a00a005
60 changed files with 16018 additions and 34 deletions

View File

@ -8,6 +8,6 @@
</head>
<body>
<div id="root" class="h-screen w-screen"></div>
<script type="module" src="/src/main.jsx"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -13,13 +13,19 @@
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@tanem/svg-injector": "^10.1.68",
"@turf/turf": "^6.5.0",
"@types/leaflet": "^1.9.8",
"@types/react-leaflet": "^3.0.0",
"@types/turf": "^3.5.32",
"js-sha256": "^0.11.0",
"leaflet": "^1.9.4",
"leaflet-control-mini-map": "^0.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-leaflet": "^4.2.1"
"react-leaflet": "^4.2.1",
"turf": "^3.0.14",
"usng": "^0.3.0"
},
"devDependencies": {
"@types/react": "^18.2.66",

39
frontend/react/src/dom.d.ts vendored Normal file
View File

@ -0,0 +1,39 @@
import { Unit } from "./unit/unit";
interface CustomEventMap {
"unitSelection": CustomEvent<Unit>,
"unitDeselection": CustomEvent<Unit>,
"unitsSelection": CustomEvent<Unit[]>,
"unitsDeselection": CustomEvent<Unit[]>,
"clearSelection": CustomEvent<any>,
"unitCreation": CustomEvent<Unit>,
"unitDeletion": CustomEvent<Unit>,
"unitDeath": CustomEvent<Unit>,
"unitUpdated": CustomEvent<Unit>,
"unitMoveCommand": CustomEvent<Unit>,
"unitAttackCommand": CustomEvent<Unit>,
"unitLandCommand": CustomEvent<Unit>,
"unitSetAltitudeCommand": CustomEvent<Unit>,
"unitSetSpeedCommand": CustomEvent<Unit>,
"unitSetOption": CustomEvent<Unit>,
"groupCreation": CustomEvent<Unit[]>,
"groupDeletion": CustomEvent<Unit[]>,
"mapStateChanged": CustomEvent<string>,
"mapContextMenu": CustomEvent<any>,
"mapOptionsChanged": CustomEvent<any>,
"commandModeOptionsChanged": CustomEvent<any>,
"contactsUpdated": CustomEvent<Unit>,
"activeCoalitionChanged": CustomEvent<any>
}
declare global {
interface Document {
addEventListener<K extends keyof CustomEventMap>(type: K,
listener: (this: Document, ev: CustomEventMap[K]) => void): void;
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void;
}
//function getOlympusPlugin(): OlympusPlugin;
}
export { };

View File

@ -1,7 +1,6 @@
@import "../node_modules/leaflet/dist/leaflet.css";
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;

View File

@ -1,10 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './app.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@ -0,0 +1,14 @@
/***************** UI *******************/
import React from 'react'
import ReactDOM from 'react-dom/client'
import UI from './ui.js'
import './index.css'
import { setupApp } from './olympusapp.js'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<UI />
</React.StrictMode>,
)
window.onload = setupApp;

View File

@ -0,0 +1,136 @@
import { Map } from 'leaflet';
import { Handler } from 'leaflet';
import { Util } from 'leaflet';
import { DomUtil } from 'leaflet';
import { DomEvent } from 'leaflet';
import { LatLngBounds } from 'leaflet';
import { Bounds } from 'leaflet';
export var BoxSelect = Handler.extend({
initialize: function (map) {
this._map = map;
this._container = map.getContainer();
this._pane = map.getPanes().overlayPane;
this._resetStateTimeout = 0;
map.on('unload', this._destroy, this);
},
addHooks: function () {
DomEvent.on(this._container, 'mousedown', this._onMouseDown, this);
},
removeHooks: function () {
DomEvent.off(this._container, 'mousedown', this._onMouseDown, this);
},
moved: function () {
return this._moved;
},
_destroy: function () {
DomUtil.remove(this._pane);
delete this._pane;
},
_resetState: function () {
this._resetStateTimeout = 0;
this._moved = false;
},
_clearDeferredResetState: function () {
if (this._resetStateTimeout !== 0) {
clearTimeout(this._resetStateTimeout);
this._resetStateTimeout = 0;
}
},
_onMouseDown: function (e: any) {
if ((e.which == 1 && e.button == 0 && e.shiftKey)) {
this._map.fire('selectionstart');
// Clear the deferred resetState if it hasn't executed yet, otherwise it
// will interrupt the interaction and orphan a box element in the container.
this._clearDeferredResetState();
this._resetState();
DomUtil.disableTextSelection();
DomUtil.disableImageDrag();
this._startPoint = this._map.mouseEventToContainerPoint(e);
//@ts-ignore
DomEvent.on(document, {
contextmenu: DomEvent.stop,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp,
keydown: this._onKeyDown
}, this);
} else {
return false;
}
},
_onMouseMove: function (e: any) {
if (!this._moved) {
this._moved = true;
this._box = DomUtil.create('div', 'leaflet-zoom-box', this._container);
DomUtil.addClass(this._container, 'leaflet-crosshair');
this._map.fire('boxzoomstart');
}
this._point = this._map.mouseEventToContainerPoint(e);
var bounds = new Bounds(this._point, this._startPoint),
size = bounds.getSize();
if (bounds.min != undefined)
DomUtil.setPosition(this._box, bounds.min);
this._box.style.width = size.x + 'px';
this._box.style.height = size.y + 'px';
},
_finish: function () {
if (this._moved) {
DomUtil.remove(this._box);
DomUtil.removeClass(this._container, 'leaflet-crosshair');
}
DomUtil.enableTextSelection();
DomUtil.enableImageDrag();
//@ts-ignore
DomEvent.off(document, {
contextmenu: DomEvent.stop,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp,
keydown: this._onKeyDown
}, this);
},
_onMouseUp: function (e: any) {
if ((e.which !== 1) && (e.button !== 0)) { return; }
this._finish();
if (!this._moved) { return; }
// Postpone to next JS tick so internal click event handling
// still see it as "moved".
window.setTimeout(Util.bind(this._resetState, this), 0);
var bounds = new LatLngBounds(
this._map.containerPointToLatLng(this._startPoint),
this._map.containerPointToLatLng(this._point));
this._map.fire('selectionend', { selectionBounds: bounds });
},
_onKeyDown: function (e: any) {
if (e.keyCode === 27) {
this._finish();
this._clearDeferredResetState();
this._resetState();
}
}
});

View File

@ -0,0 +1,12 @@
import { MiniMap, MiniMapOptions } from "leaflet-control-mini-map";
export class ClickableMiniMap extends MiniMap {
constructor(layer: L.TileLayer | L.LayerGroup, options?: MiniMapOptions) {
super(layer, options);
}
getMap() {
//@ts-ignore needed to access not exported member. A bit of a hack, required to access click events //TODO: fix me
return this._miniMap;
}
}

View File

@ -0,0 +1,166 @@
import { DomUtil, LatLng, LatLngExpression, Map, Point, Polygon, PolylineOptions } from "leaflet";
import { getApp } from "../../olympusapp";
import { CoalitionAreaHandle } from "./coalitionareahandle";
import { CoalitionAreaMiddleHandle } from "./coalitionareamiddlehandle";
import { BLUE_COMMANDER, RED_COMMANDER } from "../../constants/constants";
export class CoalitionArea extends Polygon {
#coalition: string = "blue";
#selected: boolean = true;
#editing: boolean = true;
#handles: CoalitionAreaHandle[] = [];
#middleHandles: CoalitionAreaMiddleHandle[] = [];
#activeIndex: number = 0;
constructor(latlngs: LatLngExpression[] | LatLngExpression[][] | LatLngExpression[][][], options?: PolylineOptions) {
if (options === undefined)
options = {};
options.bubblingMouseEvents = false;
options.interactive = false;
super(latlngs, options);
this.#setColors();
this.#registerCallbacks();
if ([BLUE_COMMANDER, RED_COMMANDER].includes(getApp().getMissionManager().getCommandModeOptions().commandMode))
this.setCoalition(getApp().getMissionManager().getCommandedCoalition());
}
setCoalition(coalition: string) {
this.#coalition = coalition;
this.#setColors();
}
getCoalition() {
return this.#coalition;
}
setSelected(selected: boolean) {
this.#selected = selected;
this.#setColors();
this.#setHandles();
this.setOpacity(selected? 1: 0.5);
if (!this.getSelected() && this.getEditing()) {
/* Remove the vertex we were working on */
var latlngs = this.getLatLngs()[0] as LatLng[];
latlngs.splice(this.#activeIndex, 1);
this.setLatLngs(latlngs);
this.setEditing(false);
}
}
getSelected() {
return this.#selected;
}
setEditing(editing: boolean) {
this.#editing = editing;
this.#setHandles();
var latlngs = this.getLatLngs()[0] as LatLng[];
/* Remove areas with less than 2 vertexes */
if (latlngs.length <= 2)
getApp().getMap().deleteCoalitionArea(this);
}
getEditing() {
return this.#editing;
}
addTemporaryLatLng(latlng: LatLng) {
this.#activeIndex++;
var latlngs = this.getLatLngs()[0] as LatLng[];
latlngs.splice(this.#activeIndex, 0, latlng);
this.setLatLngs(latlngs);
this.#setHandles();
}
moveActiveVertex(latlng: LatLng) {
var latlngs = this.getLatLngs()[0] as LatLng[];
latlngs[this.#activeIndex] = latlng;
this.setLatLngs(latlngs);
this.#setHandles();
}
setOpacity(opacity: number) {
this.setStyle({opacity: opacity, fillOpacity: opacity * 0.25});
}
onRemove(map: Map): this {
super.onRemove(map);
this.#handles.concat(this.#middleHandles).forEach((handle: CoalitionAreaHandle | CoalitionAreaMiddleHandle) => handle.removeFrom(getApp().getMap()));
return this;
}
#setColors() {
const coalitionColor = this.getCoalition() === "blue" ? "#247be2" : "#ff5858";
this.setStyle({ color: this.getSelected() ? "white" : coalitionColor, fillColor: coalitionColor });
}
#setHandles() {
this.#handles.forEach((handle: CoalitionAreaHandle) => handle.removeFrom(getApp().getMap()));
this.#handles = [];
if (this.getSelected()) {
var latlngs = this.getLatLngs()[0] as LatLng[];
latlngs.forEach((latlng: LatLng, idx: number) => {
/* Add the polygon vertex handle (for moving the vertex) */
const handle = new CoalitionAreaHandle(latlng);
handle.addTo(getApp().getMap());
handle.on("drag", (e: any) => {
var latlngs = this.getLatLngs()[0] as LatLng[];
latlngs[idx] = e.target.getLatLng();
this.setLatLngs(latlngs);
this.#setMiddleHandles();
});
this.#handles.push(handle);
});
}
this.#setMiddleHandles();
}
#setMiddleHandles() {
this.#middleHandles.forEach((handle: CoalitionAreaMiddleHandle) => handle.removeFrom(getApp().getMap()));
this.#middleHandles = [];
var latlngs = this.getLatLngs()[0] as LatLng[];
if (this.getSelected() && latlngs.length >= 2) {
var lastLatLng: LatLng | null = null;
latlngs.concat([latlngs[0]]).forEach((latlng: LatLng, idx: number) => {
/* Add the polygon middle point handle (for adding new vertexes) */
if (lastLatLng != null) {
const handle1Point = getApp().getMap().latLngToLayerPoint(latlng);
const handle2Point = getApp().getMap().latLngToLayerPoint(lastLatLng);
const middlePoint = new Point((handle1Point.x + handle2Point.x) / 2, (handle1Point.y + handle2Point.y) / 2);
const middleLatLng = getApp().getMap().layerPointToLatLng(middlePoint);
const middleHandle = new CoalitionAreaMiddleHandle(middleLatLng);
middleHandle.addTo(getApp().getMap());
middleHandle.on("click", (e: any) => {
this.#activeIndex = idx - 1;
this.addTemporaryLatLng(middleLatLng);
});
this.#middleHandles.push(middleHandle);
}
lastLatLng = latlng;
});
}
}
#registerCallbacks() {
this.on("click", (e: any) => {
getApp().getMap().deselectAllCoalitionAreas();
if (!this.getSelected()) {
this.setSelected(true);
}
});
this.on("contextmenu", (e: any) => {
if (!this.getEditing()) {
getApp().getMap().deselectAllCoalitionAreas();
this.setSelected(true);
}
else
this.setEditing(false);
});
}
}

View File

@ -0,0 +1,19 @@
import { DivIcon, LatLng } from "leaflet";
import { CustomMarker } from "../markers/custommarker";
export class CoalitionAreaHandle extends CustomMarker {
constructor(latlng: LatLng) {
super(latlng, {interactive: true, draggable: true});
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [24, 24],
iconAnchor: [12, 12],
className: "leaflet-coalitionarea-handle-marker",
}));
var el = document.createElement("div");
el.classList.add("ol-coalitionarea-handle-icon");
this.getElement()?.appendChild(el);
}
}

View File

@ -0,0 +1,19 @@
import { DivIcon, LatLng } from "leaflet";
import { CustomMarker } from "../markers/custommarker";
export class CoalitionAreaMiddleHandle extends CustomMarker {
constructor(latlng: LatLng) {
super(latlng, {interactive: true, draggable: false});
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [16, 16],
iconAnchor: [8, 8],
className: "leaflet-coalitionarea-middle-handle-marker",
}));
var el = document.createElement("div");
el.classList.add("ol-coalitionarea-middle-handle-icon");
this.getElement()?.appendChild(el);
}
}

View File

@ -0,0 +1,20 @@
import { DivIcon, LatLng } from "leaflet";
import { CustomMarker } from "../markers/custommarker";
export class DrawingCursor extends CustomMarker {
constructor() {
super(new LatLng(0, 0), {interactive: false})
this.setZIndexOffset(9999);
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [24, 24],
iconAnchor: [0, 24],
className: "leaflet-draw-marker",
}));
var el = document.createElement("div");
el.classList.add("ol-draw-icon");
this.getElement()?.appendChild(el);
}
}

View File

@ -0,0 +1,49 @@
import * as L from "leaflet"
export class DCSLayer extends L.TileLayer {
createTile(coords: L.Coords, done: L.DoneCallback) {
let newDone = (error?: Error, tile?: HTMLElement) => {
if (error === null && tile !== undefined && !tile.classList.contains('filtered')) {
// Create a canvas and set its width and height.
var canvas = document.createElement('canvas');
canvas.setAttribute('width', '256px');
canvas.setAttribute('height', '256px');
// Get the canvas drawing context, and draw the image to it.
var context = canvas.getContext('2d');
if (context) {
context.drawImage(tile as CanvasImageSource, 0, 0, canvas.width, canvas.height);
// Get the canvas image data.
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
// Create a function for preserving a specified colour.
var makeTransparent = function(imageData: ImageData, color: {r: number, g: number, b: number}) {
// Get the pixel data from the source.
var data = imageData.data;
// Iterate through all the pixels.
for (var i = 0; i < data.length; i += 4) {
// Check if the current pixel should have preserved transparency. This simply compares whether the color we passed in is equivalent to our pixel data.
var convert = data[i] > color.r - 5 && data[i] < color.r + 5
&& data[i + 1] > color.g - 5 && data[i + 1] < color.g + 5
&& data[i + 2] > color.b - 5 && data[i + 2] < color.b + 5;
// Either preserve the initial transparency or set the transparency to 0.
data[i + 3] = convert ? 100: data[i + 3];
}
return imageData;
};
// Get the new pixel data and set it to the canvas context.
var newData = makeTransparent(imageData, {r: 26, g: 109, b: 127});
context.putImageData(newData, 0, 0);
(tile as HTMLImageElement).src = canvas.toDataURL();
tile.classList.add('filtered');
}
} else {
return done(error, tile);
}
}
return super.createTile(coords, newDone);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
import { DivIcon, Map, Marker } from "leaflet";
import { MarkerOptions } from "leaflet";
import { LatLngExpression } from "leaflet";
export class CustomMarker extends Marker {
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
}
onAdd(map: Map): this {
this.setIcon(new DivIcon()); // Default empty icon
super.onAdd(map);
this.createIcon();
return this;
}
onRemove(map: Map): this {
super.onRemove(map);
return this;
}
createIcon() {
/* Overloaded by child classes */
}
}

View File

@ -0,0 +1,19 @@
import { DivIcon, LatLng } from "leaflet";
import { CustomMarker } from "../markers/custommarker";
export class DestinationPreviewHandle extends CustomMarker {
constructor(latlng: LatLng) {
super(latlng, {interactive: true, draggable: true});
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [18, 18],
iconAnchor: [9, 9],
className: "leaflet-destination-preview-handle-marker",
}));
var el = document.createElement("div");
el.classList.add("ol-destination-preview-handle-icon");
this.getElement()?.appendChild(el);
}
}

View File

@ -0,0 +1,20 @@
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
import { CustomMarker } from "./custommarker";
export class DestinationPreviewMarker extends CustomMarker {
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
this.setZIndexOffset(9999);
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [52, 52],
iconAnchor: [26, 26],
className: "leaflet-destination-preview",
}));
var el = document.createElement("div");
el.classList.add("ol-destination-preview-icon");
this.getElement()?.appendChild(el);
}
}

View File

@ -0,0 +1,31 @@
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
import { CustomMarker } from "./custommarker";
import { SVGInjector } from "@tanem/svg-injector";
import { getApp } from "../../olympusapp";
export class SmokeMarker extends CustomMarker {
#color: string;
constructor(latlng: LatLngExpression, color: string, options?: MarkerOptions) {
super(latlng, options);
this.setZIndexOffset(9999);
this.#color = color;
window.setTimeout(() => { this.removeFrom(getApp().getMap()); }, 300000) /* Remove the smoke after 5 minutes */
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [52, 52],
iconAnchor: [26, 52],
className: "leaflet-smoke-marker",
}));
var el = document.createElement("div");
el.classList.add("ol-smoke-icon");
el.setAttribute("data-color", this.#color);
var img = document.createElement("img");
img.src = "/resources/theme/images/markers/smoke.svg";
img.onload = () => SVGInjector(img);
el.appendChild(img);
this.getElement()?.appendChild(el);
}
}

View File

@ -0,0 +1,20 @@
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
import { CustomMarker } from "./custommarker";
export class TargetMarker extends CustomMarker {
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
this.setZIndexOffset(9999);
}
createIcon() {
this.setIcon(new DivIcon({
iconSize: [52, 52],
iconAnchor: [26, 26],
className: "leaflet-target-marker",
}));
var el = document.createElement("div");
el.classList.add("ol-target-icon");
this.getElement()?.appendChild(el);
}
}

View File

@ -0,0 +1,76 @@
import { CustomMarker } from "./custommarker";
import { DivIcon, LatLng } from "leaflet";
import { SVGInjector } from "@tanem/svg-injector";
import { getMarkerCategoryByName, getUnitDatabaseByCategory } from "../../other/utils";
import { getApp } from "../../olympusapp";
export class TemporaryUnitMarker extends CustomMarker {
#name: string;
#coalition: string;
#commandHash: string|undefined = undefined;
#timer: number = 0;
constructor(latlng: LatLng, name: string, coalition: string, commandHash?: string) {
super(latlng, {interactive: false});
this.#name = name;
this.#coalition = coalition;
this.#commandHash = commandHash;
if (commandHash !== undefined)
this.setCommandHash(commandHash)
}
setCommandHash(commandHash: string) {
this.#commandHash = commandHash;
this.#timer = window.setInterval(() => {
if (this.#commandHash !== undefined) {
getApp().getServerManager().isCommandExecuted((res: any) => {
if (res.commandExecuted) {
this.removeFrom(getApp().getMap());
window.clearInterval(this.#timer);
}
}, this.#commandHash)
}
}, 1000);
}
createIcon() {
const category = getMarkerCategoryByName(this.#name);
const databaseEntry = getUnitDatabaseByCategory(category)?.getByName(this.#name);
/* Set the icon */
var icon = new DivIcon({
className: 'leaflet-unit-icon',
iconAnchor: [25, 25],
iconSize: [50, 50],
});
this.setIcon(icon);
var el = document.createElement("div");
el.classList.add("unit");
el.setAttribute("data-object", `unit-${category}`);
el.setAttribute("data-coalition", this.#coalition);
// Main icon
var unitIcon = document.createElement("div");
unitIcon.classList.add("unit-icon");
var img = document.createElement("img");
img.src = `/resources/theme/images/units/${databaseEntry?.markerFile ?? category}.svg`;
img.onload = () => SVGInjector(img);
unitIcon.appendChild(img);
unitIcon.toggleAttribute("data-rotate-to-heading", false);
el.append(unitIcon);
// Short label
if (category == "aircraft" || category == "helicopter") {
var shortLabel = document.createElement("div");
shortLabel.classList.add("unit-short-label");
shortLabel.innerText = databaseEntry?.shortLabel || "";
el.append(shortLabel);
}
this.getElement()?.appendChild(el);
this.getElement()?.classList.add("ol-temporary-marker");
}
}

View File

@ -0,0 +1,56 @@
// @ts-nocheck
// This is a horrible hack. But it is needed at the moment to ovveride a default behaviour of Leaflet. TODO please fix me the proper way.
import { Circle, Point, Polyline } from 'leaflet';
/**
* This custom Circle object implements a faster render method for very big circles. When zoomed in, the default ctx.arc method
* is very slow since the circle is huge. Also, when zoomed in most of the circle points will be outside the screen and not needed. This
* simpler, faster renderer approximates the circle with line segements and only draws those currently visibile.
* A more refined version using arcs could be implemented but this works good enough.
*/
export class RangeCircle extends Circle {
_updatePath() {
if (!this._renderer._drawing || this._empty()) { return; }
var p = this._point,
ctx = this._renderer._ctx,
r = Math.max(Math.round(this._radius), 1),
s = (Math.max(Math.round(this._radiusY), 1) || r) / r;
if (s !== 1) {
ctx.save();
ctx.scale(1, s);
}
let pathBegun = false;
let dtheta = Math.PI * 2 / 120;
for (let theta = 0; theta <= Math.PI * 2; theta += dtheta) {
let p1 = new Point(p.x + r * Math.cos(theta), p.y / s + r * Math.sin(theta));
let p2 = new Point(p.x + r * Math.cos(theta + dtheta), p.y / s + r * Math.sin(theta + dtheta));
let l1 = this._map.layerPointToLatLng(p1);
let l2 = this._map.layerPointToLatLng(p2);
let line = new Polyline([l1, l2]);
if (this._map.getBounds().intersects(line.getBounds())) {
if (!pathBegun) {
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
pathBegun = true;
}
ctx.lineTo(p2.x, p2.y);
}
else {
if (pathBegun) {
this._renderer._fillStroke(ctx, this);
pathBegun = false;
}
}
}
if (pathBegun)
this._renderer._fillStroke(ctx, this);
if (s !== 1)
ctx.restore();
}
}

View File

@ -0,0 +1,136 @@
import { Map, Point } from 'leaflet';
import { Handler } from 'leaflet';
import { Util } from 'leaflet';
import { DomUtil } from 'leaflet';
import { DomEvent } from 'leaflet';
import { LatLngBounds } from 'leaflet';
import { Bounds } from 'leaflet';
export var TouchBoxSelect = Handler.extend({
initialize: function (map: Map) {
this._map = map;
this._container = map.getContainer();
this._pane = map.getPanes().overlayPane;
this._resetStateTimeout = 0;
this._doubleClicked = false;
map.on('unload', this._destroy, this);
},
addHooks: function () {
DomEvent.on(this._container, 'touchstart', this._onMouseDown, this);
},
removeHooks: function () {
DomEvent.off(this._container, 'touchstart', this._onMouseDown, this);
},
moved: function () {
return this._moved;
},
_destroy: function () {
DomUtil.remove(this._pane);
delete this._pane;
},
_resetState: function () {
this._resetStateTimeout = 0;
this._moved = false;
},
_clearDeferredResetState: function () {
if (this._resetStateTimeout !== 0) {
clearTimeout(this._resetStateTimeout);
this._resetStateTimeout = 0;
}
},
_onMouseDown: function (e: any) {
if ((e.which == 0)) {
this._map.fire('selectionstart');
// Clear the deferred resetState if it hasn't executed yet, otherwise it
// will interrupt the interaction and orphan a box element in the container.
this._clearDeferredResetState();
this._resetState();
DomUtil.disableTextSelection();
DomUtil.disableImageDrag();
this._startPoint = this._getMousePosition(e);
//@ts-ignore
DomEvent.on(document, {
contextmenu: DomEvent.stop,
touchmove: this._onMouseMove,
touchend: this._onMouseUp
}, this);
} else {
return false;
}
},
_onMouseMove: function (e: any) {
if (!this._moved) {
this._moved = true;
this._box = DomUtil.create('div', 'leaflet-zoom-box', this._container);
DomUtil.addClass(this._container, 'leaflet-crosshair');
}
this._point = this._getMousePosition(e);
var bounds = new Bounds(this._point, this._startPoint),
size = bounds.getSize();
if (bounds.min != undefined)
DomUtil.setPosition(this._box, bounds.min);
this._box.style.width = size.x + 'px';
this._box.style.height = size.y + 'px';
},
_finish: function () {
if (this._moved) {
DomUtil.remove(this._box);
DomUtil.removeClass(this._container, 'leaflet-crosshair');
}
DomUtil.enableTextSelection();
DomUtil.enableImageDrag();
//@ts-ignore
DomEvent.off(document, {
contextmenu: DomEvent.stop,
touchmove: this._onMouseMove,
touchend: this._onMouseUp
}, this);
},
_onMouseUp: function (e: any) {
if ((e.which !== 0)) { return; }
this._finish();
if (!this._moved) { return; }
// Postpone to next JS tick so internal click event handling
// still see it as "moved".
window.setTimeout(Util.bind(this._resetState, this), 0);
var bounds = new LatLngBounds(
this._map.containerPointToLatLng(this._startPoint),
this._map.containerPointToLatLng(this._point));
this._map.fire('selectionend', { selectionBounds: bounds });
},
_getMousePosition(e: any) {
var scale = DomUtil.getScale(this._container), offset = scale.boundingClientRect; // left and top values are in page scale (like the event clientX/Y)
return new Point(
// offset.left/top values are in page scale (like clientX/Y),
// whereas clientLeft/Top (border width) values are the original values (before CSS scale applies).
(e.touches[0].clientX - offset.left) / scale.x - this._container.clientLeft,
(e.touches[0].clientY - offset.top) / scale.y - this._container.clientTop
);
}
});

View File

@ -0,0 +1,96 @@
import { DivIcon } from 'leaflet';
import { CustomMarker } from '../map/markers/custommarker';
import { SVGInjector } from '@tanem/svg-injector';
import { AirbaseChartData, AirbaseOptions } from '../interfaces';
export class Airbase extends CustomMarker {
#name: string = "";
#chartData: AirbaseChartData = {
elevation: "",
ICAO: "",
TACAN: "",
runways: []
};
#coalition: string = "";
#hasChartDataBeenSet: boolean = false;
#properties: string[] = [];
#parkings: string[] = [];
constructor(options: AirbaseOptions) {
super(options.position, { riseOnHover: true });
this.#name = options.name;
}
chartDataHasBeenSet() {
return this.#hasChartDataBeenSet;
}
createIcon() {
var icon = new DivIcon({
className: 'leaflet-airbase-marker',
iconSize: [40, 40],
iconAnchor: [20, 20]
}); // Set the marker, className must be set to avoid white square
this.setIcon(icon);
var el = document.createElement("div");
el.classList.add("airbase-icon");
el.setAttribute("data-object", "airbase");
var img = document.createElement("img");
img.src = "/resources/theme/images/markers/airbase.svg";
img.onload = () => SVGInjector(img);
el.appendChild(img);
this.getElement()?.appendChild(el);
el.addEventListener( "mouseover", ( ev ) => {
document.dispatchEvent( new CustomEvent( "airbaseMouseover", { detail: this }));
});
el.addEventListener( "mouseout", ( ev ) => {
document.dispatchEvent( new CustomEvent( "airbaseMouseout", { detail: this }));
});
el.dataset.coalition = this.#coalition;
}
setCoalition(coalition: string) {
this.#coalition = coalition;
(<HTMLElement>this.getElement()?.querySelector(".airbase-icon")).dataset.coalition = this.#coalition;
}
getChartData() {
return this.#chartData;
}
getCoalition() {
return this.#coalition;
}
setName(name: string) {
this.#name = name;
}
getName() {
return this.#name;
}
setChartData(chartData: AirbaseChartData) {
this.#hasChartDataBeenSet = true;
this.#chartData = chartData;
}
setProperties(properties: string[]) {
this.#properties = properties;
}
getProperties() {
return this.#properties;
}
setParkings(parkings: string[]) {
this.#parkings = parkings;
}
getParkings() {
return this.#parkings;
}
}

View File

@ -0,0 +1,36 @@
import { DivIcon } from "leaflet";
import { CustomMarker } from "../map/markers/custommarker";
import { SVGInjector } from "@tanem/svg-injector";
export class Bullseye extends CustomMarker {
#coalition: string = "";
createIcon() {
var icon = new DivIcon({
className: 'leaflet-bullseye-marker',
iconSize: [40, 40],
iconAnchor: [20, 20]
}); // Set the marker, className must be set to avoid white square
this.setIcon(icon);
var el = document.createElement("div");
el.classList.add("bullseye-icon");
el.setAttribute("data-object", "bullseye");
var img = document.createElement("img");
img.src = "/resources/theme/images/markers/bullseye.svg";
img.onload = () => SVGInjector(img);
el.appendChild(img);
this.getElement()?.appendChild(el);
}
setCoalition(coalition: string)
{
this.#coalition = coalition;
(<HTMLElement> this.getElement()?.querySelector(".bullseye-icon")).dataset.coalition = this.#coalition;
}
getCoalition()
{
return this.#coalition;
}
}

View File

@ -0,0 +1,326 @@
import { LatLng } from "leaflet";
import { getApp } from "../olympusapp";
import { Airbase } from "./airbase";
import { Bullseye } from "./bullseye";
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";
import { aircraftDatabase } from "../unit/databases/aircraftdatabase";
import { helicopterDatabase } from "../unit/databases/helicopterdatabase";
import { navyUnitDatabase } from "../unit/databases/navyunitdatabase";
//import { Popup } from "../popups/popup";
import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionData } from "../interfaces";
/** The MissionManager */
export class MissionManager {
#bullseyes: { [name: string]: Bullseye } = {};
#airbases: { [name: string]: Airbase } = {};
#theatre: string = "";
#dateAndTime: DateAndTime = {date: {Year: 0, Month: 0, Day: 0}, time: {h: 0, m: 0, s: 0}, startTime: 0, elapsedTime: 0};
#commandModeOptions: CommandModeOptions = {commandMode: NONE, restrictSpawns: false, restrictToCoalition: false, setupTime: Infinity, spawnPoints: {red: Infinity, blue: Infinity}, eras: []};
#remainingSetupTime: number = 0;
#spentSpawnPoint: number = 0;
//#commandModeDialog: HTMLElement;
//#commandModeErasDropdown: Dropdown;
#coalitions: {red: string[], blue: string[]} = {red: [], blue: []};
constructor() {
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
*
* @param object <BulleyesData>
*/
updateBullseyes(data: BullseyesData) {
const commandMode = getApp().getMissionManager().getCommandModeOptions().commandMode;
for (let idx in data.bullseyes) {
const bullseye = data.bullseyes[idx];
// Prevent Red and Blue coalitions seeing each other's bulleye(s)
if ((bullseye.coalition === "red" && commandMode === BLUE_COMMANDER)
|| (bullseye.coalition === "blue" && commandMode === RED_COMMANDER)) {
continue;
}
if (!(idx in this.#bullseyes))
this.#bullseyes[idx] = new Bullseye([0, 0]).addTo(getApp().getMap());
if (bullseye.latitude && bullseye.longitude && bullseye.coalition) {
this.#bullseyes[idx].setLatLng(new LatLng(bullseye.latitude, bullseye.longitude));
this.#bullseyes[idx].setCoalition(bullseye.coalition);
}
}
}
/** Update airbase information
*
* @param object <AirbasesData>
*/
updateAirbases(data: AirbasesData) {
for (let idx in data.airbases) {
var airbase = data.airbases[idx]
if (this.#airbases[airbase.callsign] === undefined && airbase.callsign != '') {
this.#airbases[airbase.callsign] = new Airbase({
position: new LatLng(airbase.latitude, airbase.longitude),
name: airbase.callsign
}).addTo(getApp().getMap());
this.#airbases[airbase.callsign].on('contextmenu', (e) => this.#onAirbaseClick(e));
this.#loadAirbaseChartData(airbase.callsign);
}
if (this.#airbases[airbase.callsign] != undefined && airbase.latitude && airbase.longitude && airbase.coalition) {
this.#airbases[airbase.callsign].setLatLng(new LatLng(airbase.latitude, airbase.longitude));
this.#airbases[airbase.callsign].setCoalition(airbase.coalition);
}
}
}
/** Update mission information
*
* @param object <MissionData>
*/
updateMission(data: MissionData) {
if (data.mission) {
/* Set the mission theatre */
if (data.mission.theatre != this.#theatre) {
this.#theatre = data.mission.theatre;
getApp().getMap().setTheatre(this.#theatre);
//(getApp().getPopupsManager().get("infoPopup") as Popup).setText("Map set to " + this.#theatre);
}
/* Set the date and time data */
this.#dateAndTime = data.mission.dateAndTime;
data.mission.dateAndTime.time.s -= 1; // ED has seconds 1-60 and not 0-59?!
/* Set the coalition countries */
this.#coalitions = data.mission.coalitions;
/* Set the command mode options */
this.#setcommandModeOptions(data.mission.commandModeOptions);
this.#remainingSetupTime = this.getCommandModeOptions().setupTime - this.getDateAndTime().elapsedTime;
var commandModePhaseEl = document.querySelector("#command-mode-phase") as HTMLElement;
if (commandModePhaseEl) {
if (this.#remainingSetupTime > 0) {
var remainingTime = `-${new Date(this.#remainingSetupTime * 1000).toISOString().substring(14, 19)}`;
commandModePhaseEl.dataset.remainingTime = remainingTime;
}
commandModePhaseEl.classList.toggle("setup-phase", this.#remainingSetupTime > 0 && this.getCommandModeOptions().restrictSpawns);
//commandModePhaseEl.classList.toggle("game-commenced", this.#remainingSetupTime <= 0 || !this.getCommandModeOptions().restrictSpawns);
//commandModePhaseEl.classList.toggle("no-restrictions", !this.getCommandModeOptions().restrictSpawns);
}
}
}
/** Get the bullseyes set in this theatre
*
* @returns object
*/
getBullseyes() {
return this.#bullseyes;
}
/** Get the airbases in this theatre
*
* @returns object
*/
getAirbases() {
return this.#airbases;
}
/** Get the options/settings as set in the command mode
*
* @returns object
*/
getCommandModeOptions() {
return this.#commandModeOptions;
}
/** Get the current date and time of the mission (based on local time)
*
* @returns object
*/
getDateAndTime() {
return this.#dateAndTime;
}
/**
* Get the number of seconds left of setup time
* @returns number
*/
getRemainingSetupTime() {
return this.#remainingSetupTime;
}
/** Get an object with the coalitions in it
*
* @returns object
*/
getCoalitions() {
return this.#coalitions;
}
/** Get the current theatre (map) name
*
* @returns string
*/
getTheatre() {
return this.#theatre;
}
getAvailableSpawnPoints() {
if (this.getCommandModeOptions().commandMode === GAME_MASTER)
return Infinity;
else if (this.getCommandModeOptions().commandMode === BLUE_COMMANDER)
return this.getCommandModeOptions().spawnPoints.blue - this.#spentSpawnPoint;
else if (this.getCommandModeOptions().commandMode === RED_COMMANDER)
return this.getCommandModeOptions().spawnPoints.red - this.#spentSpawnPoint;
else
return 0;
}
getCommandedCoalition() {
if (this.getCommandModeOptions().commandMode === BLUE_COMMANDER)
return "blue";
else if (this.getCommandModeOptions().commandMode === RED_COMMANDER)
return "red";
else
return "all";
}
refreshSpawnPoints() {
var spawnPointsEl = document.querySelector("#spawn-points");
if (spawnPointsEl) {
spawnPointsEl.textContent = `${this.getAvailableSpawnPoints()}`;
}
}
setSpentSpawnPoints(spawnPoints: number) {
this.#spentSpawnPoint = spawnPoints;
this.refreshSpawnPoints();
}
showCommandModeDialog() {
//const options = this.getCommandModeOptions()
//const { restrictSpawns, restrictToCoalition, setupTime } = options;
//this.#toggleSpawnRestrictions(restrictSpawns);
//
///* Create the checkboxes to select the unit eras */
//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");
//
//const restrictSpawnsCheckbox = this.#commandModeDialog.querySelector("#restrict-spawns")?.querySelector("input") as HTMLInputElement;
//const restrictToCoalitionCheckbox = this.#commandModeDialog.querySelector("#restrict-to-coalition")?.querySelector("input") as HTMLInputElement;
//const blueSpawnPointsInput = this.#commandModeDialog.querySelector("#blue-spawn-points")?.querySelector("input") as HTMLInputElement;
//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 = 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() {
//this.#commandModeDialog.classList.add("hide");
//
//const restrictSpawnsCheckbox = this.#commandModeDialog.querySelector("#restrict-spawns")?.querySelector("input") as HTMLInputElement;
//const restrictToCoalitionCheckbox = this.#commandModeDialog.querySelector("#restrict-to-coalition")?.querySelector("input") as HTMLInputElement;
//const blueSpawnPointsInput = this.#commandModeDialog.querySelector("#blue-spawn-points")?.querySelector("input") as HTMLInputElement;
//const redSpawnPointsInput = this.#commandModeDialog.querySelector("#red-spawn-points")?.querySelector("input") as HTMLInputElement;
//const setupTimeInput = this.#commandModeDialog.querySelector("#setup-time")?.querySelector("input") as HTMLInputElement;
//
//var eras: string[] = [];
//const enabledEras = getCheckboxOptions(this.#commandModeErasDropdown);
//Object.keys(enabledEras).forEach((key: string) => {if (enabledEras[key]) eras.push(key)});
//getApp().getServerManager().setCommandModeOptions(restrictSpawnsCheckbox.checked, restrictToCoalitionCheckbox.checked, {blue: parseInt(blueSpawnPointsInput.value), red: parseInt(redSpawnPointsInput.value)}, eras, parseInt(setupTimeInput.value) * 60);
}
#setcommandModeOptions(commandModeOptions: CommandModeOptions) {
/* Refresh all the data if we have exited the NONE state */
var requestRefresh = false;
if (this.#commandModeOptions.commandMode === NONE && commandModeOptions.commandMode !== NONE)
requestRefresh = true;
/* Refresh the page if we have lost Game Master priviledges */
if (this.#commandModeOptions.commandMode === GAME_MASTER && commandModeOptions.commandMode !== GAME_MASTER)
location.reload();
/* Check if any option has changed */
var commandModeOptionsChanged = (!commandModeOptions.eras.every((value: string, idx: number) => {return value === this.getCommandModeOptions().eras[idx]}) ||
commandModeOptions.spawnPoints.red !== this.getCommandModeOptions().spawnPoints.red ||
commandModeOptions.spawnPoints.blue !== this.getCommandModeOptions().spawnPoints.blue ||
commandModeOptions.restrictSpawns !== this.getCommandModeOptions().restrictSpawns ||
commandModeOptions.restrictToCoalition !== this.getCommandModeOptions().restrictToCoalition);
this.#commandModeOptions = commandModeOptions;
this.setSpentSpawnPoints(0);
this.refreshSpawnPoints();
if (commandModeOptionsChanged) {
document.dispatchEvent(new CustomEvent("commandModeOptionsChanged", { detail: this }));
document.getElementById("command-mode-toolbar")?.classList.remove("hide");
const el = document.getElementById("command-mode");
if (el) {
el.dataset.mode = commandModeOptions.commandMode;
el.textContent = commandModeOptions.commandMode.toUpperCase();
}
}
document.querySelector("#spawn-points-container")?.classList.toggle("hide", this.getCommandModeOptions().commandMode === GAME_MASTER || !this.getCommandModeOptions().restrictSpawns);
document.querySelector("#command-mode-settings-button")?.classList.toggle("hide", this.getCommandModeOptions().commandMode !== GAME_MASTER);
if (requestRefresh)
getApp().getServerManager().refreshAll();
}
#onAirbaseClick(e: any) {
getApp().getMap().showAirbaseContextMenu(e.sourceTarget, e.originalEvent.x, e.originalEvent.y, e.latlng);
}
#loadAirbaseChartData(callsign: string) {
if ( !this.#theatre ) {
return;
}
var xhr = new XMLHttpRequest();
xhr.open('GET', `api/airbases/${this.#theatre.toLowerCase()}/${callsign}`, true);
xhr.responseType = 'json';
xhr.onload = () => {
var status = xhr.status;
if (status === 200) {
const data = xhr.response;
this.getAirbases()[callsign].setChartData(data);
} else {
console.error(`Error retrieving data for ${callsign} airbase`)
}
};
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

@ -0,0 +1,502 @@
/***************** APP *******************/
var app: OlympusApp;
export function setupApp() {
app = new OlympusApp();
app.start();
}
export function getApp() {
return app;
}
import { Map } from "./map/map";
import { MissionManager } from "./mission/missionmanager";
//import { ConnectionStatusPanel } from "./panels/connectionstatuspanel";
//import { HotgroupPanel } from "./panels/hotgrouppanel";
//import { LogPanel } from "./panels/logpanel";
//import { MouseInfoPanel } from "./panels/mouseinfopanel";
//import { ServerStatusPanel } from "./panels/serverstatuspanel";
//import { UnitControlPanel } from "./panels/unitcontrolpanel";
//import { UnitInfoPanel } from "./panels/unitinfopanel";
//import { PluginsManager } from "./plugin/pluginmanager";
//import { Popup } from "./popups/popup";
import { ShortcutManager } from "./shortcut/shortcutmanager";
//import { CommandModeToolbar } from "./toolbars/commandmodetoolbar";
//import { PrimaryToolbar } from "./toolbars/primarytoolbar";
import { UnitsManager } from "./unit/unitsmanager";
import { WeaponsManager } from "./weapon/weaponsmanager";
//import { Manager } from "./other/manager";
import { ServerManager } from "./server/servermanager";
import { sha256 } from 'js-sha256';
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";
import { helicopterDatabase } from "./unit/databases/helicopterdatabase";
import { groundUnitDatabase } from "./unit/databases/groundunitdatabase";
import { navyUnitDatabase } from "./unit/databases/navyunitdatabase";
//import { UnitListPanel } from "./panels/unitlistpanel";
//import { ContextManager } from "./context/contextmanager";
//import { Context } from "./context/context";
var VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
export class OlympusApp {
/* Global data */
#activeCoalition: string = "blue";
#latestVersion: string|undefined = undefined;
#config: any = {};
/* Main leaflet map, extended by custom methods */
#map: Map | null = null;
/* Managers */
//#contextManager!: ContextManager;
//#dialogManager!: Manager;
#missionManager: MissionManager | null = null;
//#panelsManager: Manager | null = null;
//#pluginsManager: PluginsManager | null = null;
//#popupsManager: Manager | null = null;
#serverManager: ServerManager | null = null;
#shortcutManager!: ShortcutManager;
//#toolbarsManager: Manager | null = null;
#unitsManager: UnitsManager | null = null;
#weaponsManager: WeaponsManager | null = null;
constructor() {
}
// TODO add checks on null
getDialogManager() {
return null //this.#dialogManager as Manager;
}
getMap() {
return this.#map as Map;
}
getCurrentContext() {
return null //this.getContextManager().getCurrentContext() as Context;
}
getContextManager() {
return null // this.#contextManager as ContextManager;
}
getServerManager() {
return this.#serverManager as ServerManager;
}
getPanelsManager() {
return null // this.#panelsManager as Manager;
}
getPopupsManager() {
return null // this.#popupsManager as Manager;
}
getToolbarsManager() {
return null // this.#toolbarsManager as Manager;
}
getShortcutManager() {
return this.#shortcutManager as ShortcutManager;
}
getUnitsManager() {
return this.#unitsManager as UnitsManager;
}
getWeaponsManager() {
return this.#weaponsManager as WeaponsManager;
}
getMissionManager() {
return this.#missionManager as MissionManager;
}
getPluginsManager() {
return null // this.#pluginsManager as PluginsManager;
}
/** Set the active coalition, i.e. the currently controlled coalition. A game master can change the active coalition, while a commander is bound to his/her coalition
*
* @param newActiveCoalition
*/
setActiveCoalition(newActiveCoalition: string) {
if (this.getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER) {
this.#activeCoalition = newActiveCoalition;
document.dispatchEvent(new CustomEvent("activeCoalitionChanged"));
}
}
/**
*
* @returns The active coalition
*/
getActiveCoalition() {
if (this.getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER)
return this.#activeCoalition;
else {
if (this.getMissionManager().getCommandModeOptions().commandMode == BLUE_COMMANDER)
return "blue";
else if (this.getMissionManager().getCommandModeOptions().commandMode == RED_COMMANDER)
return "red";
else
return "neutral";
}
}
/**
*
* @returns The aircraft database
*/
getAircraftDatabase() {
return aircraftDatabase;
}
/**
*
* @returns The helicopter database
*/
getHelicopterDatabase() {
return helicopterDatabase;
}
/**
*
* @returns The ground unit database
*/
getGroundUnitDatabase() {
return groundUnitDatabase;
}
/**
*
* @returns The navy unit database
*/
getNavyUnitDatabase() {
return navyUnitDatabase;
}
/** Set a message in the login splash screen
*
* @param status The message to show in the login splash screen
*/
setLoginStatus(status: string) {
const el = document.querySelector("#login-status") as HTMLElement;
if (el)
el.dataset["status"] = status;
}
start() {
/* Initialize base functionalitites */
//this.#contextManager = new ContextManager();
//this.#contextManager.add( "olympus", {} );
this.#map = new Map('map-container');
this.#missionManager = new MissionManager();
//this.#panelsManager = new Manager();
//this.#popupsManager = new Manager();
this.#serverManager = new ServerManager();
this.#shortcutManager = new ShortcutManager();
//this.#toolbarsManager = new Manager();
this.#unitsManager = new UnitsManager();
this.#weaponsManager = new WeaponsManager();
// Toolbars
//this.getToolbarsManager().add("primaryToolbar", new PrimaryToolbar("primary-toolbar"))
// .add("commandModeToolbar", new CommandModeToolbar("command-mode-toolbar"));
//
//// Panels
//this.getPanelsManager()
// .add("connectionStatus", new ConnectionStatusPanel("connection-status-panel"))
// .add("hotgroup", new HotgroupPanel("hotgroup-panel"))
// .add("mouseInfo", new MouseInfoPanel("mouse-info-panel"))
// .add("log", new LogPanel("log-panel"))
// .add("serverStatus", new ServerStatusPanel("server-status-panel"))
// .add("unitControl", new UnitControlPanel("unit-control-panel"))
// .add("unitInfo", new UnitInfoPanel("unit-info-panel"))
// .add("unitList", new UnitListPanel("unit-list-panel", "unit-list-panel-content"))
//
//// Popups
//this.getPopupsManager()
// .add("infoPopup", new Popup("info-popup"));
//
//this.#pluginsManager = new PluginsManager();
/* Set the address of the server */
this.getServerManager().setAddress(window.location.href.split('?')[0]);
/* Setup all global events */
this.#setupEvents();
/* Set the splash background image to a random image */
let splashScreen = document.getElementById("splash-screen") as HTMLElement;
let i = Math.round(Math.random() * 7 + 1);
if (splashScreen) {
new Promise((resolve, reject) => {
const image = new Image();
image.addEventListener('load', resolve);
image.addEventListener('error', resolve);
image.src = `/resources/theme/images/splash/${i}.jpg`;
}).then(() => {
splashScreen.style.backgroundImage = `url('/resources/theme/images/splash/${i}.jpg')`;
let loadingScreen = document.getElementById("loading-screen") as HTMLElement;
loadingScreen.classList.add("fade-out");
window.setInterval(() => { loadingScreen.classList.add("hide"); }, 1000);
})
}
/* Check if we are running the latest version */
const request = new Request("https://raw.githubusercontent.com/Pax1601/DCSOlympus/main/version.json");
fetch(request).then((response) => {
if (response.status === 200) {
return response.json();
} else {
throw new Error("Error connecting to Github to retrieve latest version");
}
}).then((res) => {
this.#latestVersion = res["version"];
const latestVersionSpan = document.getElementById("latest-version") as HTMLElement;
if (latestVersionSpan) {
latestVersionSpan.innerHTML = this.#latestVersion ?? "Unknown";
latestVersionSpan.classList.toggle("new-version", this.#latestVersion !== VERSION);
}
})
/* Load the config file from the server */
const configRequest = new Request("http://localhost:3000/" + "resources/config");
fetch(configRequest).then((response) => {
if (response.status === 200) {
return response.json();
} else {
throw new Error("Error retrieving config file");
}
}).then((res) => {
this.#config = res;
document.dispatchEvent(new CustomEvent("configLoaded"));
})
}
#setupEvents() {
/* Generic clicks */
document.addEventListener("click", (ev) => {
if (ev instanceof MouseEvent && ev.target instanceof HTMLElement) {
const target = ev.target;
if (target.classList.contains("olympus-dialog-close")) {
target.closest("div.olympus-dialog")?.classList.add("hide");
}
const triggerElement = target.closest("[data-on-click]");
if (triggerElement instanceof HTMLElement) {
const eventName: string = triggerElement.dataset.onClick || "";
let params = JSON.parse(triggerElement.dataset.onClickParams || "{}");
params._element = triggerElement;
if (eventName) {
document.dispatchEvent(new CustomEvent(eventName, {
detail: params
}));
}
}
}
});
const shortcutManager = this.getShortcutManager();
shortcutManager.addKeyboardShortcut("togglePause", {
"altKey": false,
"callback": () => {
this.getServerManager().setPaused(!this.getServerManager().getPaused());
},
"code": "Space",
"context": "olympus",
"ctrlKey": false
}).addKeyboardShortcut("deselectAll", {
"callback": (ev: KeyboardEvent) => {
this.getUnitsManager().deselectAllUnits();
},
"code": "Escape",
"context": "olympus"
}).addKeyboardShortcut("toggleUnitLabels", {
"altKey": false,
"callback": () => {
const chk = document.querySelector(`label[title="${SHOW_UNIT_LABELS}"] input[type="checkbox"]`);
if (chk instanceof HTMLElement) {
chk.click();
}
},
"code": "KeyL",
"context": "olympus",
"ctrlKey": false,
"shiftKey": false
}).addKeyboardShortcut("toggleAcquisitionRings", {
"altKey": false,
"callback": () => {
const chk = document.querySelector(`label[title="${SHOW_UNITS_ACQUISITION_RINGS}"] input[type="checkbox"]`);
if (chk instanceof HTMLElement) {
chk.click();
}
},
"code": "KeyE",
"context": "olympus",
"ctrlKey": false,
"shiftKey": false
}).addKeyboardShortcut("toggleEngagementRings", {
"altKey": false,
"callback": () => {
const chk = document.querySelector(`label[title="${SHOW_UNITS_ENGAGEMENT_RINGS}"] input[type="checkbox"]`);
if (chk instanceof HTMLElement) {
chk.click();
}
},
"code": "KeyQ",
"context": "olympus",
"ctrlKey": false,
"shiftKey": false
}).addKeyboardShortcut("toggleHideShortEngagementRings", {
"altKey": false,
"callback": () => {
const chk = document.querySelector(`label[title="${HIDE_UNITS_SHORT_RANGE_RINGS}"] input[type="checkbox"]`);
if (chk instanceof HTMLElement) {
chk.click();
}
},
"code": "KeyR",
"context": "olympus",
"ctrlKey": false,
"shiftKey": false
}).addKeyboardShortcut("toggleFillEngagementRings", {
"altKey": false,
"callback": () => {
const chk = document.querySelector(`label[title="${FILL_SELECTED_RING}"] input[type="checkbox"]`);
if (chk instanceof HTMLElement) {
chk.click();
}
},
"code": "KeyF",
"context": "olympus",
"ctrlKey": false,
"shiftKey": false
}).addKeyboardShortcut("increaseCameraZoom", {
"altKey": true,
"callback": () => {
//this.getMap().increaseCameraZoom();
},
"code": "Equal",
"context": "olympus",
"ctrlKey": false,
"shiftKey": false
}).addKeyboardShortcut("decreaseCameraZoom", {
"altKey": true,
"callback": () => {
//this.getMap().decreaseCameraZoom();
},
"code": "Minus",
"context": "olympus",
"ctrlKey": false,
"shiftKey": false
});
["KeyW", "KeyA", "KeyS", "KeyD", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].forEach(code => {
shortcutManager.addKeyboardShortcut(`pan${code}keydown`, {
"altKey": false,
"callback": (ev: KeyboardEvent) => {
//this.getMap().handleMapPanning(ev);
},
"code": code,
"context": "olympus",
"ctrlKey": false,
"event": "keydown"
});
shortcutManager.addKeyboardShortcut(`pan${code}keyup`, {
"callback": (ev: KeyboardEvent) => {
//this.getMap().handleMapPanning(ev);
},
"code": code,
"context": "olympus"
});
});
const digits = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9"];
digits.forEach(code => {
shortcutManager.addKeyboardShortcut(`hotgroup${code}`, {
"altKey": false,
"callback": (ev: KeyboardEvent) => {
if (ev.ctrlKey && ev.shiftKey)
this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false); // "Select hotgroup X in addition to any units already selected"
else if (ev.ctrlKey && !ev.shiftKey)
this.getUnitsManager().setHotgroup(parseInt(ev.code.substring(5))); // "These selected units are hotgroup X (forget any previous membership)"
else if (!ev.ctrlKey && ev.shiftKey)
this.getUnitsManager().addToHotgroup(parseInt(ev.code.substring(5))); // "Add (append) these units to hotgroup X (in addition to any existing members)"
else
this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it."
},
"code": code
});
// Stop hotgroup controls sending the browser to another tab
document.addEventListener("keydown", (ev: KeyboardEvent) => {
if (ev.code === code && ev.ctrlKey === true && ev.altKey === false && ev.shiftKey === false) {
ev.preventDefault();
}
});
});
// TODO: move from here in dedicated class
document.addEventListener("closeDialog", (ev: CustomEventInit) => {
ev.detail._element.closest(".ol-dialog").classList.add("hide");
document.getElementById("gray-out")?.classList.toggle("hide", true);
});
/* Try and connect with the Olympus REST server */
const loginForm = document.getElementById("authentication-form");
if (loginForm instanceof HTMLFormElement) {
loginForm.addEventListener("submit", (ev:SubmitEvent) => {
ev.preventDefault();
ev.stopPropagation();
var hash = sha256.create();
const username = (loginForm.querySelector("#username") as HTMLInputElement).value;
const password = hash.update((loginForm.querySelector("#password") as HTMLInputElement).value).hex();
// Update the user credentials
this.getServerManager().setCredentials(username, password);
// Start periodically requesting updates
this.getServerManager().startUpdate();
this.setLoginStatus("connecting");
});
} else {
console.error("Unable to find login form.");
}
/* Temporary */
this.getServerManager().setCredentials("admin", "4b8823ed9e5c2392ab4a791913bb8ce41956ea32e308b760eefb97536746dd33");
this.getServerManager().startUpdate();
/* Reload the page, used to mimic a restart of the app */
document.addEventListener("reloadPage", () => {
location.reload();
})
///* Inject the svgs with the corresponding svg code. This allows to dynamically manipulate the svg, like changing colors */
//document.querySelectorAll("[inject-svg]").forEach((el: Element) => {
// var img = el as HTMLImageElement;
// var isLoaded = img.complete;
// if (isLoaded)
// SVGInjector(img);
// else
// img.addEventListener("load", () => { SVGInjector(img); });
//})
}
getConfig() {
return this.#config;
}
}

View File

@ -0,0 +1,7 @@
import { Manager } from "./manager";
export abstract class EventsManager extends Manager {
constructor() {
super();
}
}

View File

@ -0,0 +1,37 @@
import { Context } from "../context/context";
export class Manager {
#items: { [key: string]: any } = {};
constructor() {
}
add(name: string, item: any) {
const regex = new RegExp("^[a-z][a-z0-9]{2,}$", "i");
if (regex.test(name) === false) {
throw new Error(`Item name "${name}" does not match regex: ${regex.toString()}.`);
}
if (this.#items.hasOwnProperty(name)) {
throw new Error(`Item with name "${name}" already exists.`);
}
this.#items[name] = item;
return this;
}
get(name: string) {
if (this.#items.hasOwnProperty(name)) {
return this.#items[name];
} else {
return false;
}
}
getAll() {
return this.#items;
}
}

View File

@ -0,0 +1,476 @@
import { LatLng, Point, Polygon } from "leaflet";
import * as turf from "@turf/turf";
import { UnitDatabase } from "../unit/databases/unitdatabase";
import { aircraftDatabase } from "../unit/databases/aircraftdatabase";
import { helicopterDatabase } from "../unit/databases/helicopterdatabase";
import { groundUnitDatabase } from "../unit/databases/groundunitdatabase";
//import { Buffer } from "buffer";
import { GROUND_UNIT_AIR_DEFENCE_REGEX, ROEs, emissionsCountermeasures, reactionsToThreat, states } from "../constants/constants";
import { navyUnitDatabase } from "../unit/databases/navyunitdatabase";
import { DateAndTime, UnitBlueprint } from "../interfaces";
import { Converter } from "usng";
export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) {
const φ1 = deg2rad(lat1); // φ, λ in radians
const φ2 = deg2rad(lat2);
const λ1 = deg2rad(lon1); // φ, λ in radians
const λ2 = deg2rad(lon2);
const y = Math.sin(λ2 - λ1) * Math.cos(φ2);
const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1);
const θ = Math.atan2(y, x);
const brng = (rad2deg(θ) + 360) % 360; // in degrees
return brng;
}
export function distance(lat1: number, lon1: number, lat2: number, lon2: number) {
const R = 6371e3; // metres
const φ1 = deg2rad(lat1); // φ, λ in radians
const φ2 = deg2rad(lat2);
const Δφ = deg2rad(lat2 - lat1);
const Δλ = deg2rad(lon2 - lon1);
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const d = R * c; // in metres
return d;
}
export function bearingAndDistanceToLatLng(lat: number, lon: number, brng: number, dist: number) {
const R = 6371e3; // metres
const φ1 = deg2rad(lat); // φ, λ in radians
const λ1 = deg2rad(lon);
const φ2 = Math.asin(Math.sin(φ1) * Math.cos(dist / R) + Math.cos(φ1) * Math.sin(dist / R) * Math.cos(brng));
const λ2 = λ1 + Math.atan2(Math.sin(brng) * Math.sin(dist / R) * Math.cos(φ1), Math.cos(dist / R) - Math.sin(φ1) * Math.sin(φ2));
return new LatLng(rad2deg(φ2), rad2deg(λ2));
}
export function ConvertDDToDMS(D: number, lng: boolean) {
var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N";
var deg = 0 | (D < 0 ? (D = -D) : D);
var min = 0 | (((D += 1e-9) % 1) * 60);
var sec = (0 | (((D * 60) % 1) * 6000)) / 100;
var dec = Math.round((sec - Math.floor(sec)) * 100);
var sec = Math.floor(sec);
if (lng)
return dir + zeroPad(deg, 3) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\"";
else
return dir + zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\"";
}
export function dataPointMap(container: HTMLElement, data: any) {
Object.keys(data).forEach((key) => {
const val = "" + data[key]; // Ensure a string
container.querySelectorAll(`[data-point="${key}"]`).forEach(el => {
// We could probably have options here
if (el instanceof HTMLInputElement) {
el.value = val;
} else if (el instanceof HTMLElement) {
el.innerText = val;
}
});
});
}
export function deg2rad(deg: number) {
var pi = Math.PI;
return deg * (pi / 180);
}
export function rad2deg(rad: number) {
var pi = Math.PI;
return rad / (pi / 180);
}
export function generateUUIDv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
export function keyEventWasInInput(event: KeyboardEvent) {
const target = event.target;
return (target instanceof HTMLElement && (["INPUT", "TEXTAREA"].includes(target.nodeName)));
}
export function reciprocalHeading(heading: number): number {
return heading > 180 ? heading - 180 : heading + 180;
}
/**
* Prepend numbers to the start of a string
*
* @param num <number> subject number
* @param places <number> places to pad
* @param decimal <boolean> whether this is a decimal number or not
*
* */
export const zeroAppend = function (num: number, places: number, decimal: boolean = false) {
var string = decimal ? num.toFixed(2) : String(num);
while (string.length < places) {
string = "0" + string;
}
return string;
}
export const zeroPad = function (num: number, places: number) {
var string = String(num);
while (string.length < places) {
string += "0";
}
return string;
}
export function similarity(s1: string, s2: string) {
var longer = s1;
var shorter = s2;
if (s1.length < s2.length) {
longer = s2;
shorter = s1;
}
var longerLength = longer.length;
if (longerLength == 0) {
return 1.0;
}
return (longerLength - editDistance(longer, shorter)) / longerLength;
}
export function editDistance(s1: string, s2: string) {
s1 = s1.toLowerCase();
s2 = s2.toLowerCase();
var costs = new Array();
for (var i = 0; i <= s1.length; i++) {
var lastValue = i;
for (var j = 0; j <= s2.length; j++) {
if (i == 0)
costs[j] = j;
else {
if (j > 0) {
var newValue = costs[j - 1];
if (s1.charAt(i - 1) != s2.charAt(j - 1))
newValue = Math.min(Math.min(newValue, lastValue),
costs[j]) + 1;
costs[j - 1] = lastValue;
lastValue = newValue;
}
}
}
if (i > 0)
costs[s2.length] = lastValue;
}
return costs[s2.length];
}
export type MGRS = {
bandLetter: string,
columnLetter: string,
easting: string,
groups: string[],
northing: string,
precision: number,
rowLetter: string,
string: string,
zoneNumber: string
}
export function latLngToMGRS(lat: number, lng: number, precision: number = 4): MGRS | false {
if (precision < 0 || precision > 6) {
console.error("latLngToMGRS: precision must be a number >= 0 and <= 6. Given precision: " + precision);
return false;
}
const mgrs = new Converter({}).LLtoMGRS(lat, lng, precision);
const match = mgrs.match(new RegExp(`^(\\d{2})([A-Z])([A-Z])([A-Z])(\\d+)$`));
const easting = match[5].substr(0, match[5].length / 2);
const northing = match[5].substr(match[5].length / 2);
let output: MGRS = {
bandLetter: match[2],
columnLetter: match[3],
groups: [match[1] + match[2], match[3] + match[4], easting, northing],
easting: easting,
northing: northing,
precision: precision,
rowLetter: match[4],
string: match[0],
zoneNumber: match[1]
}
return output;
}
export function latLngToUTM(lat: number, lng: number) {
return new Converter({}).LLtoUTM(lat, lng);
}
export function latLngToMercator(lat: number, lng: number): { x: number, y: number } {
var rMajor = 6378137; //Equatorial Radius, WGS84
var shift = Math.PI * rMajor;
var x = lng * shift / 180;
var y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
y = y * shift / 180;
return { x: x, y: y };
}
export function mercatorToLatLng(x: number, y: number) {
var rMajor = 6378137; //Equatorial Radius, WGS84
var shift = Math.PI * rMajor;
var lng = x / shift * 180.0;
var lat = y / shift * 180.0;
lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0);
return { lng: lng, lat: lat };
}
export function createDivWithClass(className: string) {
var el = document.createElement("div");
el.classList.add(className);
return el;
}
export function knotsToMs(knots: number) {
return knots / 1.94384;
}
export function msToKnots(ms: number) {
return ms * 1.94384;
}
export function ftToM(ft: number) {
return ft * 0.3048;
}
export function mToFt(m: number) {
return m / 0.3048;
}
export function mToNm(m: number) {
return m * 0.000539957;
}
export function nmToM(nm: number) {
return nm / 0.000539957;
}
export function nmToFt(nm: number) {
return nm * 6076.12;
}
export function polyContains(latlng: LatLng, polygon: Polygon) {
var poly = polygon.toGeoJSON();
return turf.inside(turf.point([latlng.lng, latlng.lat]), poly);
}
export function randomPointInPoly(polygon: Polygon): LatLng {
var bounds = polygon.getBounds();
var x_min = bounds.getEast();
var x_max = bounds.getWest();
var y_min = bounds.getSouth();
var y_max = bounds.getNorth();
var lat = y_min + (Math.random() * (y_max - y_min));
var lng = x_min + (Math.random() * (x_max - x_min));
var poly = polygon.toGeoJSON();
var inside = turf.inside(turf.point([lng, lat]), poly);
if (inside) {
return new LatLng(lat, lng);
} else {
return randomPointInPoly(polygon);
}
}
export function polygonArea(polygon: Polygon) {
var poly = polygon.toGeoJSON();
return turf.area(poly);
}
export function randomUnitBlueprint(unitDatabase: UnitDatabase, options: { type?: string, role?: string, ranges?: string[], eras?: string[], coalition?: string }) {
/* Start from all the unit blueprints in the database */
var unitBlueprints = Object.values(unitDatabase.getBlueprints());
/* If a specific type or role is provided, use only the blueprints of that type or role */
if (options.type && options.role) {
console.error("Can't create random unit if both type and role are provided. Either create by type or by role.")
return null;
}
if (options.type) {
unitBlueprints = unitDatabase.getByType(options.type);
}
else if (options.role) {
unitBlueprints = unitDatabase.getByType(options.role);
}
/* Keep only the units that have a range included in the requested values */
if (options.ranges) {
unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => {
var rangeType = "";
var range = unitBlueprint.acquisitionRange;
if (range !== undefined) {
if (range >= 0 && range < 10000)
rangeType = "Short range";
else if (range >= 10000 && range < 100000)
rangeType = "Medium range";
else if (range >= 100000 && range < 999999)
rangeType = "Long range";
}
return options.ranges?.includes(rangeType);
});
}
/* Keep only the units that have an era included in the requested values */
if (options.eras) {
unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => {
return unitBlueprint.era ? options.eras?.includes(unitBlueprint.era) : true;
});
}
/* Keep only the units that have the correct coalition, if selected */
if (options.coalition) {
unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => {
return (unitBlueprint.coalition && unitBlueprint.coalition !== "") ? options.coalition === unitBlueprint.coalition : true;
});
}
var index = Math.floor(Math.random() * unitBlueprints.length);
return unitBlueprints[index];
}
export function getMarkerCategoryByName(name: string) {
if (aircraftDatabase.getByName(name) != null)
return "aircraft";
else if (helicopterDatabase.getByName(name) != null)
return "helicopter";
else if (groundUnitDatabase.getByName(name) != null) {
var type = groundUnitDatabase.getByName(name)?.type ?? "";
if (/\bAAA|SAM\b/.test(type) || /\bmanpad|stinger\b/i.test(type))
return "groundunit-sam";
else
return "groundunit-other";
}
else if (navyUnitDatabase.getByName(name) != null)
return "navyunit";
else
return "aircraft"; // TODO add other unit types
}
export function getUnitDatabaseByCategory(category: string) {
if (category.toLowerCase() == "aircraft")
return aircraftDatabase;
else if (category.toLowerCase() == "helicopter")
return helicopterDatabase;
else if (category.toLowerCase().includes("groundunit"))
return groundUnitDatabase;
else if (category.toLowerCase().includes("navyunit"))
return navyUnitDatabase;
else
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;
}
export function enumToState(state: number) {
if (state < states.length)
return states[state];
else
return states[0];
}
export function enumToROE(ROE: number) {
if (ROE < ROEs.length)
return ROEs[ROE];
else
return ROEs[0];
}
export function enumToReactionToThreat(reactionToThreat: number) {
if (reactionToThreat < reactionsToThreat.length)
return reactionsToThreat[reactionToThreat];
else
return reactionsToThreat[0];
}
export function enumToEmissioNCountermeasure(emissionCountermeasure: number) {
if (emissionCountermeasure < emissionsCountermeasures.length)
return emissionsCountermeasures[emissionCountermeasure];
else
return emissionsCountermeasures[0];
}
export function enumToCoalition(coalitionID: number) {
switch (coalitionID) {
case 0: return "neutral";
case 1: return "red";
case 2: return "blue";
}
return "";
}
export function coalitionToEnum(coalition: string) {
switch (coalition) {
case "neutral": return 0;
case "red": return 1;
case "blue": return 2;
}
return 0;
}
export function convertDateAndTimeToDate(dateAndTime: DateAndTime) {
const date = dateAndTime.date;
const time = dateAndTime.time;
if (!date) {
return new Date();
}
let year = date.Year;
let month = date.Month - 1;
if (month < 0) {
month = 11;
year--;
}
return new Date(year, month, date.Day, time.h, time.m, time.s);
}
export function getGroundElevation(latlng: LatLng, callback: CallableFunction) {
/* Get the ground elevation from the server endpoint */
const xhr = new XMLHttpRequest();
xhr.open('GET', `api/elevation/${latlng.lat}/${latlng.lng}`, true);
xhr.timeout = 500; // ms
xhr.responseType = 'json';
xhr.onload = () => {
var status = xhr?.status;
if (status === 200) {
callback(xhr.response)
}
};
xhr.send();
}

View File

@ -0,0 +1,164 @@
import { LatLng } from "leaflet";
import { Ammo, Contact, GeneralSettings, Offset, Radio, TACAN } from "../interfaces";
export class DataExtractor {
#seekPosition = 0;
#dataview: DataView;
#decoder: TextDecoder;
#buffer: ArrayBuffer;
constructor(buffer: ArrayBuffer) {
this.#buffer = buffer;
this.#dataview = new DataView(this.#buffer);
this.#decoder = new TextDecoder("utf-8");
}
setSeekPosition(seekPosition: number) {
this.#seekPosition = seekPosition;
}
getSeekPosition() {
return this.#seekPosition;
}
extractBool() {
const value = this.#dataview.getUint8(this.#seekPosition);
this.#seekPosition += 1;
return value > 0;
}
extractUInt8() {
const value = this.#dataview.getUint8(this.#seekPosition);
this.#seekPosition += 1;
return value;
}
extractUInt16() {
const value = this.#dataview.getUint16(this.#seekPosition, true);
this.#seekPosition += 2;
return value;
}
extractUInt32() {
const value = this.#dataview.getUint32(this.#seekPosition, true);
this.#seekPosition += 4;
return value;
}
extractUInt64() {
const value = this.#dataview.getBigUint64(this.#seekPosition, true);
this.#seekPosition += 8;
return value;
}
extractFloat64() {
const value = this.#dataview.getFloat64(this.#seekPosition, true);
this.#seekPosition += 8;
return value;
}
extractLatLng() {
return new LatLng(this.extractFloat64(), this.extractFloat64(), this.extractFloat64())
}
extractFromBitmask(bitmask: number, position: number) {
return ((bitmask >> position) & 1) > 0;
}
extractString(length?: number) {
if (length === undefined)
length = this.extractUInt16()
var stringBuffer = this.#buffer.slice(this.#seekPosition, this.#seekPosition + length);
var view = new Int8Array(stringBuffer);
var stringLength = length;
view.every((value: number, idx: number) => {
if (value === 0) {
stringLength = idx;
return false;
} else
return true;
});
const value = this.#decoder.decode(stringBuffer);
this.#seekPosition += length;
return value.substring(0, stringLength).trim();
}
extractChar() {
return this.extractString(1);
}
extractTACAN() {
const value: TACAN = {
isOn: this.extractBool(),
channel: this.extractUInt8(),
XY: this.extractChar(),
callsign: this.extractString(4)
}
return value;
}
extractRadio() {
const value: Radio = {
frequency: this.extractUInt32(),
callsign: this.extractUInt8(),
callsignNumber: this.extractUInt8()
}
return value;
}
extractGeneralSettings() {
const value: GeneralSettings = {
prohibitJettison: this.extractBool(),
prohibitAA: this.extractBool(),
prohibitAG: this.extractBool(),
prohibitAfterburner: this.extractBool(),
prohibitAirWpn: this.extractBool(),
}
return value;
}
extractAmmo() {
const value: Ammo[] = [];
const size = this.extractUInt16();
for (let idx = 0; idx < size; idx++) {
value.push({
quantity: this.extractUInt16(),
name: this.extractString(33),
guidance: this.extractUInt8(),
category: this.extractUInt8(),
missileCategory: this.extractUInt8()
});
}
return value;
}
extractContacts(){
const value: Contact[] = [];
const size = this.extractUInt16();
for (let idx = 0; idx < size; idx++) {
value.push({
ID: this.extractUInt32(),
detectionMethod: this.extractUInt8()
});
}
return value;
}
extractActivePath() {
const value: LatLng[] = [];
const size = this.extractUInt16();
for (let idx = 0; idx < size; idx++) {
value.push(this.extractLatLng());
}
return value;
}
extractOffset() {
const value: Offset = {
x: this.extractFloat64(),
y: this.extractFloat64(),
z: this.extractFloat64(),
}
return value;
}
}

View File

@ -0,0 +1,604 @@
import { LatLng } from 'leaflet';
import { getApp } from '../olympusapp';
import { AIRBASES_URI, BULLSEYE_URI, COMMANDS_URI, LOGS_URI, MISSION_URI, NONE, ROEs, UNITS_URI, WEAPONS_URI, emissionsCountermeasures, reactionsToThreat } from '../constants/constants';
import { ServerStatusPanel } from '../panels/serverstatuspanel';
import { LogPanel } from '../panels/logpanel';
import { Popup } from '../popups/popup';
import { ConnectionStatusPanel } from '../panels/connectionstatuspanel';
import { AirbasesData, BullseyesData, GeneralSettings, MissionData, Radio, ServerRequestOptions, TACAN } from '../interfaces';
import { zeroAppend } from '../other/utils';
export class ServerManager {
#connected: boolean = false;
#paused: boolean = false;
#REST_ADDRESS = "http://localhost:3001/olympus";
#username = "";
#password = "";
#sessionHash: string | null = null;
#lastUpdateTimes: { [key: string]: number } = {}
#previousMissionElapsedTime: number = 0; // Track if mission elapsed time is increasing (i.e. is the server paused)
#serverIsPaused: boolean = false;
#intervals: number[] = [];
#requests: { [key: string]: XMLHttpRequest } = {};
constructor() {
this.#lastUpdateTimes[UNITS_URI] = Date.now();
this.#lastUpdateTimes[WEAPONS_URI] = Date.now();
this.#lastUpdateTimes[LOGS_URI] = Date.now();
this.#lastUpdateTimes[AIRBASES_URI] = Date.now();
this.#lastUpdateTimes[BULLSEYE_URI] = Date.now();
this.#lastUpdateTimes[MISSION_URI] = Date.now();
}
setCredentials(newUsername: string, newPassword: string) {
this.#username = newUsername;
this.#password = newPassword;
}
GET(callback: CallableFunction, uri: string, options?: ServerRequestOptions, responseType: string = 'text', force: boolean = false) {
var xmlHttp = new XMLHttpRequest();
/* If a request on this uri is still pending (meaning it's not done or did not yet fail), skip the request, to avoid clogging the TCP workers */
/* If we are forcing the request we don't care if one already exists, just send it. CAREFUL: this makes sense only for low frequency requests, like refreshes, when we
are reasonably confident any previous request will be done before we make a new one on the same URI. */
if (uri in this.#requests && this.#requests[uri].readyState !== 4 && !force) {
console.warn(`GET request on ${uri} URI still pending, skipping...`);
return;
}
if (!force)
this.#requests[uri] = xmlHttp;
/* Assemble the request options string */
var optionsString = '';
if (options?.time != undefined)
optionsString = `time=${options.time}`;
if (options?.commandHash != undefined)
optionsString = `commandHash=${options.commandHash}`;
/* On the connection */
xmlHttp.open("GET", `${this.#REST_ADDRESS}/${uri}${optionsString ? `?${optionsString}` : ''}`, true);
/* If provided, set the credentials */
if (this.#username && this.#password)
xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username}:${this.#password}`));
/* If specified, set the response type */
if (responseType)
xmlHttp.responseType = responseType as XMLHttpRequestResponseType;
xmlHttp.onload = (e) => {
if (xmlHttp.status == 200) {
/* Success */
this.setConnected(true);
if (xmlHttp.responseType == 'arraybuffer')
this.#lastUpdateTimes[uri] = callback(xmlHttp.response);
else {
const result = JSON.parse(xmlHttp.responseText);
this.#lastUpdateTimes[uri] = callback(result);
//if (result.frameRate !== undefined && result.load !== undefined)
// (getApp().getPanelsManager().get("serverStatus") as ServerStatusPanel).update(result.frameRate, result.load);
}
} else if (xmlHttp.status == 401) {
/* Bad credentials */
console.error("Incorrect username/password");
getApp().setLoginStatus("failed");
} else {
/* Failure, probably disconnected */
this.setConnected(false);
}
};
xmlHttp.onreadystatechange = (res) => {
if (xmlHttp.readyState == 4 && xmlHttp.status === 0) {
console.error("An error occurred during the XMLHttpRequest");
this.setConnected(false);
}
};
xmlHttp.send(null);
}
PUT(request: object, callback: CallableFunction) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("PUT", this.#REST_ADDRESS);
xmlHttp.setRequestHeader("Content-Type", "application/json");
if (this.#username && this.#password)
xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username}:${this.#password}`));
xmlHttp.onload = (res: any) => {
var res = JSON.parse(xmlHttp.responseText);
callback(res);
};
xmlHttp.send(JSON.stringify(request));
}
getConfig(callback: CallableFunction) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("GET", window.location.href.split('?')[0] + "config", true);
xmlHttp.onload = function (e) {
var data = JSON.parse(xmlHttp.responseText);
callback(data);
};
xmlHttp.onerror = function () {
console.error("An error occurred during the XMLHttpRequest, could not retrieve configuration file");
};
xmlHttp.send(null);
}
setAddress(address: string) {
// Temporary
address = "http://localhost:3000/"
this.#REST_ADDRESS = `${address}olympus`
console.log(`Setting REST address to ${this.#REST_ADDRESS}`)
}
getAirbases(callback: CallableFunction) {
this.GET(callback, AIRBASES_URI);
}
getBullseye(callback: CallableFunction) {
this.GET(callback, BULLSEYE_URI);
}
getLogs(callback: CallableFunction, refresh: boolean = false) {
this.GET(callback, LOGS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[LOGS_URI] }, 'text', refresh);
}
getMission(callback: CallableFunction) {
this.GET(callback, MISSION_URI);
}
getUnits(callback: CallableFunction, refresh: boolean = false) {
this.GET(callback, UNITS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[UNITS_URI] }, 'arraybuffer', refresh);
}
getWeapons(callback: CallableFunction, refresh: boolean = false) {
this.GET(callback, WEAPONS_URI, { time: refresh ? 0 : this.#lastUpdateTimes[WEAPONS_URI] }, 'arraybuffer', refresh);
}
isCommandExecuted(callback: CallableFunction, commandHash: string) {
this.GET(callback, COMMANDS_URI, { commandHash: commandHash });
}
addDestination(ID: number, path: any, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "path": path }
var data = { "setPath": command }
this.PUT(data, callback);
}
spawnSmoke(color: string, latlng: LatLng, callback: CallableFunction = () => { }) {
var command = { "color": color, "location": latlng };
var data = { "smoke": command }
this.PUT(data, callback);
}
spawnExplosion(intensity: number, explosionType: string, latlng: LatLng, callback: CallableFunction = () => { }) {
var command = { "explosionType": explosionType, "intensity": intensity, "location": latlng };
var data = { "explosion": command }
this.PUT(data, callback);
}
spawnAircrafts(units: any, coalition: string, airbaseName: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => { }) {
var command = { "units": units, "coalition": coalition, "airbaseName": airbaseName, "country": country, "immediate": immediate, "spawnPoints": spawnPoints };
var data = { "spawnAircrafts": command }
this.PUT(data, callback);
}
spawnHelicopters(units: any, coalition: string, airbaseName: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => { }) {
var command = { "units": units, "coalition": coalition, "airbaseName": airbaseName, "country": country, "immediate": immediate, "spawnPoints": spawnPoints };
var data = { "spawnHelicopters": command }
this.PUT(data, callback);
}
spawnGroundUnits(units: any, coalition: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => { }) {
var command = { "units": units, "coalition": coalition, "country": country, "immediate": immediate, "spawnPoints": spawnPoints };;
var data = { "spawnGroundUnits": command }
this.PUT(data, callback);
}
spawnNavyUnits(units: any, coalition: string, country: string, immediate: boolean, spawnPoints: number, callback: CallableFunction = () => { }) {
var command = { "units": units, "coalition": coalition, "country": country, "immediate": immediate, "spawnPoints": spawnPoints };
var data = { "spawnNavyUnits": command }
this.PUT(data, callback);
}
attackUnit(ID: number, targetID: number, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "targetID": targetID };
var data = { "attackUnit": command }
this.PUT(data, callback);
}
followUnit(ID: number, targetID: number, offset: { "x": number, "y": number, "z": number }, callback: CallableFunction = () => { }) {
// X: front-rear, positive front
// Y: top-bottom, positive bottom
// Z: left-right, positive right
var command = { "ID": ID, "targetID": targetID, "offsetX": offset["x"], "offsetY": offset["y"], "offsetZ": offset["z"] };
var data = { "followUnit": command }
this.PUT(data, callback);
}
cloneUnits(units: { ID: number, location: LatLng }[], deleteOriginal: boolean, spawnPoints: number, callback: CallableFunction = () => { }) {
var command = { "units": units, "deleteOriginal": deleteOriginal, "spawnPoints": spawnPoints };
var data = { "cloneUnits": command }
this.PUT(data, callback);
}
deleteUnit(ID: number, explosion: boolean, explosionType: string, immediate: boolean, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "explosion": explosion, "explosionType": explosionType, "immediate": immediate };
var data = { "deleteUnit": command }
this.PUT(data, callback);
}
landAt(ID: number, latlng: LatLng, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "location": latlng };
var data = { "landAt": command }
this.PUT(data, callback);
}
changeSpeed(ID: number, speedChange: string, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "change": speedChange }
var data = { "changeSpeed": command }
this.PUT(data, callback);
}
setSpeed(ID: number, speed: number, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "speed": speed }
var data = { "setSpeed": command }
this.PUT(data, callback);
}
setSpeedType(ID: number, speedType: string, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "speedType": speedType }
var data = { "setSpeedType": command }
this.PUT(data, callback);
}
changeAltitude(ID: number, altitudeChange: string, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "change": altitudeChange }
var data = { "changeAltitude": command }
this.PUT(data, callback);
}
setAltitudeType(ID: number, altitudeType: string, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "altitudeType": altitudeType }
var data = { "setAltitudeType": command }
this.PUT(data, callback);
}
setAltitude(ID: number, altitude: number, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "altitude": altitude }
var data = { "setAltitude": command }
this.PUT(data, callback);
}
createFormation(ID: number, isLeader: boolean, wingmenIDs: number[], callback: CallableFunction = () => { }) {
var command = { "ID": ID, "wingmenIDs": wingmenIDs, "isLeader": isLeader }
var data = { "setLeader": command }
this.PUT(data, callback);
}
setROE(ID: number, ROE: string, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "ROE": ROEs.indexOf(ROE) }
var data = { "setROE": command }
this.PUT(data, callback);
}
setReactionToThreat(ID: number, reactionToThreat: string, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "reactionToThreat": reactionsToThreat.indexOf(reactionToThreat) }
var data = { "setReactionToThreat": command }
this.PUT(data, callback);
}
setEmissionsCountermeasures(ID: number, emissionCountermeasure: string, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "emissionsCountermeasures": emissionsCountermeasures.indexOf(emissionCountermeasure) }
var data = { "setEmissionsCountermeasures": command }
this.PUT(data, callback);
}
setOnOff(ID: number, onOff: boolean, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "onOff": onOff }
var data = { "setOnOff": command }
this.PUT(data, callback);
}
setFollowRoads(ID: number, followRoads: boolean, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "followRoads": followRoads }
var data = { "setFollowRoads": command }
this.PUT(data, callback);
}
setOperateAs(ID: number, operateAs: number, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "operateAs": operateAs }
var data = { "setOperateAs": command }
this.PUT(data, callback);
}
refuel(ID: number, callback: CallableFunction = () => { }) {
var command = { "ID": ID };
var data = { "refuel": command }
this.PUT(data, callback);
}
bombPoint(ID: number, latlng: LatLng, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "location": latlng }
var data = { "bombPoint": command }
this.PUT(data, callback);
}
carpetBomb(ID: number, latlng: LatLng, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "location": latlng }
var data = { "carpetBomb": command }
this.PUT(data, callback);
}
bombBuilding(ID: number, latlng: LatLng, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "location": latlng }
var data = { "bombBuilding": command }
this.PUT(data, callback);
}
fireAtArea(ID: number, latlng: LatLng, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "location": latlng }
var data = { "fireAtArea": command }
this.PUT(data, callback);
}
simulateFireFight(ID: number, latlng: LatLng, altitude: number, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "location": latlng, "altitude": altitude }
var data = { "simulateFireFight": command }
this.PUT(data, callback);
}
// TODO: Remove coalition
scenicAAA(ID: number, coalition: string, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "coalition": coalition }
var data = { "scenicAAA": command }
this.PUT(data, callback);
}
// TODO: Remove coalition
missOnPurpose(ID: number, coalition: string, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "coalition": coalition }
var data = { "missOnPurpose": command }
this.PUT(data, callback);
}
landAtPoint(ID: number, latlng: LatLng, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "location": latlng }
var data = { "landAtPoint": command }
this.PUT(data, callback);
}
setShotsScatter(ID: number, shotsScatter: number, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "shotsScatter": shotsScatter }
var data = { "setShotsScatter": command }
this.PUT(data, callback);
}
setShotsIntensity(ID: number, shotsIntensity: number, callback: CallableFunction = () => { }) {
var command = { "ID": ID, "shotsIntensity": shotsIntensity }
var data = { "setShotsIntensity": command }
this.PUT(data, callback);
}
setAdvacedOptions(ID: number, isActiveTanker: boolean, isActiveAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings, callback: CallableFunction = () => { }) {
var command = {
"ID": ID,
"isActiveTanker": isActiveTanker,
"isActiveAWACS": isActiveAWACS,
"TACAN": TACAN,
"radio": radio,
"generalSettings": generalSettings
};
var data = { "setAdvancedOptions": command };
this.PUT(data, callback);
}
setCommandModeOptions(restrictSpawns: boolean, restrictToCoalition: boolean, spawnPoints: { blue: number, red: number }, eras: string[], setupTime: number, callback: CallableFunction = () => { }) {
var command = {
"restrictSpawns": restrictSpawns,
"restrictToCoalition": restrictToCoalition,
"spawnPoints": spawnPoints,
"eras": eras,
"setupTime": setupTime
};
var data = { "setCommandModeOptions": command };
this.PUT(data, callback);
}
reloadDatabases(callback: CallableFunction = () => { }) {
var data = { "reloadDatabases": {} };
this.PUT(data, callback);
}
startUpdate() {
/* Clear any existing interval */
this.#intervals.forEach((interval: number) => { window.clearInterval(interval); });
this.#intervals = [];
this.#intervals.push(window.setInterval(() => {
if (!this.getPaused()) {
this.getMission((data: MissionData) => {
this.checkSessionHash(data.sessionHash);
getApp().getMissionManager()?.updateMission(data);
return data.time;
});
}
}, 1000));
this.#intervals.push(window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getAirbases((data: AirbasesData) => {
this.checkSessionHash(data.sessionHash);
getApp().getMissionManager()?.updateAirbases(data);
return data.time;
});
}
}, 10000));
this.#intervals.push(window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getBullseye((data: BullseyesData) => {
this.checkSessionHash(data.sessionHash);
getApp().getMissionManager()?.updateBullseyes(data);
return data.time;
});
}
}, 10000));
this.#intervals.push(window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getLogs((data: any) => {
this.checkSessionHash(data.sessionHash);
(getApp().getPanelsManager().get("log") as LogPanel).appendLogs(data.logs)
return data.time;
});
}
}, 1000));
this.#intervals.push(window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getUnits((buffer: ArrayBuffer) => {
var time = getApp().getUnitsManager()?.update(buffer);
return time;
}, false);
}
}, 250));
this.#intervals.push(window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getWeapons((buffer: ArrayBuffer) => {
var time = getApp().getWeaponsManager()?.update(buffer);
return time;
}, false);
}
}, 250));
this.#intervals.push(window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getUnits((buffer: ArrayBuffer) => {
var time = getApp().getUnitsManager()?.update(buffer);
return time;
}, true);
const elapsedMissionTime = getApp().getMissionManager().getDateAndTime().elapsedTime;
this.#serverIsPaused = (elapsedMissionTime === this.#previousMissionElapsedTime);
this.#previousMissionElapsedTime = elapsedMissionTime;
const csp = (getApp().getPanelsManager().get("connectionStatus") as ConnectionStatusPanel);
if (this.getConnected()) {
if (this.getServerIsPaused()) {
csp.showServerPaused();
} else {
csp.showConnected();
}
} else {
csp.showDisconnected();
}
}
}, (this.getServerIsPaused() ? 500 : 5000)));
// Mission clock and elapsed time
this.#intervals.push(window.setInterval(() => {
if (!this.getConnected() || this.#serverIsPaused) {
return;
}
const elapsedMissionTime = getApp().getMissionManager().getDateAndTime().elapsedTime;
const csp = (getApp().getPanelsManager().get("connectionStatus") as ConnectionStatusPanel);
const mt = getApp().getMissionManager().getDateAndTime().time;
csp.setMissionTime([mt.h, mt.m, mt.s].map(n => zeroAppend(n, 2)).join(":"));
csp.setElapsedTime(new Date(elapsedMissionTime * 1000).toISOString().substring(11, 19));
}, 1000));
this.#intervals.push(window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getWeapons((buffer: ArrayBuffer) => {
var time = getApp().getWeaponsManager()?.update(buffer);
return time;
}, true);
}
}, 5000));
}
refreshAll() {
this.getAirbases((data: AirbasesData) => {
this.checkSessionHash(data.sessionHash);
getApp().getMissionManager()?.updateAirbases(data);
return data.time;
});
this.getBullseye((data: BullseyesData) => {
this.checkSessionHash(data.sessionHash);
getApp().getMissionManager()?.updateBullseyes(data);
return data.time;
});
this.getLogs((data: any) => {
this.checkSessionHash(data.sessionHash);
(getApp().getPanelsManager().get("log") as LogPanel).appendLogs(data.logs)
return data.time;
});
this.getWeapons((buffer: ArrayBuffer) => {
var time = getApp().getWeaponsManager()?.update(buffer);
return time;
}, true);
this.getUnits((buffer: ArrayBuffer) => {
var time = getApp().getUnitsManager()?.update(buffer);
return time;
}, true);
}
checkSessionHash(newSessionHash: string) {
if (this.#sessionHash != null) {
if (newSessionHash !== this.#sessionHash)
location.reload();
}
else
this.#sessionHash = newSessionHash;
}
setConnected(newConnected: boolean) {
if (this.#connected != newConnected) {
//newConnected ? (getApp().getPopupsManager().get("infoPopup") as Popup).setText("Connected to DCS Olympus server") : (getApp().getPopupsManager().get("infoPopup") as Popup).setText("Disconnected from DCS Olympus server");
if (newConnected) {
document.getElementById("splash-screen")?.classList.add("hide");
document.getElementById("gray-out")?.classList.add("hide");
}
}
this.#connected = newConnected;
}
getConnected() {
return this.#connected;
}
setPaused(newPaused: boolean) {
this.#paused = newPaused;
this.#paused ? (getApp().getPopupsManager().get("infoPopup") as Popup).setText("View paused") : (getApp().getPopupsManager().get("infoPopup") as Popup).setText("View unpaused");
}
getPaused() {
return this.#paused;
}
getServerIsPaused() {
return this.#serverIsPaused;
}
getRequests() {
return this.#requests;
}
}

View File

@ -0,0 +1,48 @@
import { getApp } from "../olympusapp";
import { ShortcutKeyboardOptions, ShortcutMouseOptions, ShortcutOptions } from "../interfaces";
import { keyEventWasInInput } from "../other/utils";
export abstract class Shortcut {
#config: ShortcutOptions
constructor(config: ShortcutOptions) {
this.#config = config;
}
getConfig() {
return this.#config;
}
}
export class ShortcutKeyboard extends Shortcut {
constructor(config: ShortcutKeyboardOptions) {
config.event = config.event || "keyup";
super(config);
document.addEventListener(config.event, (ev: any) => {
if ( typeof config.context === "string" && !getApp().getContextManager().currentContextIs( config.context ) ) {
return;
}
if (ev instanceof KeyboardEvent === false || keyEventWasInInput(ev)) {
return;
}
if (config.code !== ev.code) {
return;
}
if (((typeof config.altKey !== "boolean") || (typeof config.altKey === "boolean" && ev.altKey === config.altKey))
&& ((typeof config.ctrlKey !== "boolean") || (typeof config.ctrlKey === "boolean" && ev.ctrlKey === config.ctrlKey))
&& ((typeof config.shiftKey !== "boolean") || (typeof config.shiftKey === "boolean" && ev.shiftKey === config.shiftKey))) {
config.callback(ev);
}
});
}
}
export class ShortcutMouse extends Shortcut {
constructor(config: ShortcutMouseOptions) {
super(config);
}
}

View File

@ -0,0 +1,65 @@
import { ShortcutKeyboardOptions, ShortcutMouseOptions } from "../interfaces";
import { Manager } from "../other/manager";
import { ShortcutKeyboard, ShortcutMouse } from "./shortcut";
export class ShortcutManager extends Manager {
#keysBeingHeld: string[] = [];
#keyDownCallbacks: CallableFunction[] = [];
#keyUpCallbacks: CallableFunction[] = [];
constructor() {
super();
document.addEventListener("keydown", (ev: KeyboardEvent) => {
if (this.#keysBeingHeld.indexOf(ev.code) < 0) {
this.#keysBeingHeld.push(ev.code)
}
this.#keyDownCallbacks.forEach(callback => callback(ev));
});
document.addEventListener("keyup", (ev: KeyboardEvent) => {
this.#keysBeingHeld = this.#keysBeingHeld.filter(held => held !== ev.code);
this.#keyUpCallbacks.forEach(callback => callback(ev));
});
}
add(name: string, shortcut: any) {
console.error("ShortcutManager:add() cannot be used. Use addKeyboardShortcut or addMouseShortcut.");
return this;
}
addKeyboardShortcut(name: string, shortcutKeyboardOptions: ShortcutKeyboardOptions) {
super.add(name, new ShortcutKeyboard(shortcutKeyboardOptions));
return this;
}
addMouseShortcut(name: string, shortcutMouseOptions: ShortcutMouseOptions) {
super.add(name, new ShortcutMouse(shortcutMouseOptions));
return this;
}
getKeysBeingHeld() {
return this.#keysBeingHeld;
}
keyComboMatches(combo: string[]) {
const heldKeys = this.getKeysBeingHeld();
if (combo.length !== heldKeys.length) {
return false;
}
return combo.every(key => heldKeys.indexOf(key) > -1);
}
onKeyDown(callback: CallableFunction) {
this.#keyDownCallbacks.push(callback);
}
onKeyUp(callback: CallableFunction) {
this.#keyUpCallbacks.push(callback);
}
}

View File

@ -1,5 +1,5 @@
import { createContext } from "react";
import { OlympusState } from "./App";
import { OlympusState } from "./ui";
export const StateContext = createContext({
spawnMenuVisible: false,

View File

@ -1,12 +1,13 @@
import React from 'react'
import './app.css'
import './ui.css'
import { MapContainer, TileLayer } from 'react-leaflet'
import { Map } from './map/map'
import { Header } from './ui/header'
import { Header } from './ui/panels/header'
import { EventsProvider } from './eventscontext'
import { StateProvider } from './statecontext'
import { SpawnMenu } from './ui/panels/spawnmenu'
const position = [51.505, -0.09]
@ -17,7 +18,7 @@ export type OlympusState = {
drawingMenuVisible: boolean
}
export default class App extends React.Component<{}, OlympusState> {
export default class UI extends React.Component<{}, OlympusState> {
constructor(props) {
super(props);
@ -114,7 +115,7 @@ export default class App extends React.Component<{}, OlympusState> {
render() {
return (
<div style={{ width: "100%", height: "100%" }}>
<div className="h-full w-full font-sans">
<StateProvider value={this.state}>
<EventsProvider value={
{
@ -128,12 +129,14 @@ export default class App extends React.Component<{}, OlympusState> {
toggleDrawingMenu: this.toggleDrawingMenu
}
}>
<MapContainer center={position} zoom={13} className='absolute w-full h-full top-0 left-0'>
<TileLayer
url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
/>
</MapContainer>
<Header ></Header>
<div id='map-container' className='absolute top-0 left-0 w-full h-full'>
</div>
<div className='absolute top-0 left-0 z-ui w-full h-full flex flex-col'>
<Header ></Header>
<SpawnMenu ></SpawnMenu>
</div>
</EventsProvider>
</StateProvider>
</div>

View File

@ -18,7 +18,7 @@ export class StateButton extends React.Component<ButtonProps, {}> {
render() {
var computedClassName = "";
computedClassName += this.props.active? 'bg-white text-background-steel': 'bg-transparent text-white border-white';
computedClassName += this.props.active? 'bg-white text-background-darker': 'bg-transparent text-white border-white';
return (
<FontAwesomeIcon icon={this.props.icon as IconProp} className={computedClassName + " rounded w-5 h-5 p-2 border-2"} onClick={this.props.onClick as MouseEventHandler}>

View File

@ -0,0 +1,9 @@
import React from "react";
export class MenuTitle extends React.Component<{title: string}, {}> {
render() {
return <div className="h-12 w-full bg-background-dark flex items-center px-4 font-semibold">
{this.props.title}
</div>
}
}

View File

@ -1,9 +1,9 @@
import React from 'react'
import { StateButton } from './statebuttons';
import { StateButton } from '../buttons/statebutton';
import { faPlus, faGamepad, faRuler, faPencil } from '@fortawesome/free-solid-svg-icons';
import { library } from '@fortawesome/fontawesome-svg-core'
import { EventsConsumer, EventsContext } from '../eventscontext';
import { StateConsumer } from '../statecontext';
import { EventsConsumer, EventsContext } from '../../eventscontext';
import { StateConsumer } from '../../statecontext';
library.add(faPlus, faGamepad, faRuler, faPencil)
@ -18,7 +18,7 @@ export class Header extends React.Component<{}, {}> {
{(appState) =>
<EventsConsumer>
{(events) =>
<div className='absolute top-0 left-0 h-16 w-full z-ui bg-background-steel flex flex-row items-center px-5'>
<div className='h-16 w-full bg-background-darker flex flex-row items-center px-5'>
<div className="flex flex-row items-center gap-1">
<StateButton onClick={events.toggleSpawnMenu} active={appState.spawnMenuVisible} icon="fa-solid fa-plus"></StateButton>
<StateButton onClick={events.toggleUnitControlMenu} active={appState.unitControlMenuVisible} icon="fa-solid fa-gamepad"></StateButton>

View File

@ -0,0 +1,14 @@
import React from "react";
import { MenuTitle } from "./components/menutitle";
export class SpawnMenu extends React.Component<{}, {}> {
constructor(props) {
super(props);
}
render() {
return <div className="h-full w-96 bg-background-neutral z-ui">
<MenuTitle title="Spawn menu"></MenuTitle>
</div>
}
}

View File

@ -0,0 +1,60 @@
import { Unit } from "./unit";
export interface ContextActionOptions {
isScenic?: boolean
}
export class ContextAction {
#id: string = "";
#label: string = "";
#description: string = "";
#callback: CallableFunction | null = null;
#units: Unit[] = [];
#hideContextAfterExecution: boolean = true
#options: ContextActionOptions;
constructor(id: string, label: string, description: string, callback: CallableFunction, hideContextAfterExecution: boolean = true, options: ContextActionOptions) {
this.#id = id;
this.#label = label;
this.#description = description;
this.#callback = callback;
this.#hideContextAfterExecution = hideContextAfterExecution;
this.#options = {
"isScenic": false,
...options
}
}
addUnit(unit: Unit) {
this.#units.push(unit);
}
getId() {
return this.#id;
}
getLabel() {
return this.#label;
}
getOptions() {
return this.#options;
}
getDescription() {
return this.#description;
}
getCallback() {
return this.#callback;
}
executeCallback() {
if (this.#callback)
this.#callback(this.#units);
}
getHideContextAfterExecution() {
return this.#hideContextAfterExecution;
}
}

View File

@ -0,0 +1,25 @@
import { ContextAction, ContextActionOptions } from "./contextaction";
import { Unit } from "./unit";
export class ContextActionSet {
#contextActions: {[key: string]: ContextAction} = {};
constructor() {
}
addContextAction(unit: Unit, id: string, label: string, description: string, callback: CallableFunction, hideContextAfterExecution: boolean = true, options?:ContextActionOptions) {
options = options || {};
if (!(id in this.#contextActions)) {
this.#contextActions[id] = new ContextAction(id, label, description, callback, hideContextAfterExecution, options);
}
this.#contextActions[id].addUnit(unit);
}
getContextActions() {
return this.#contextActions;
}
}

View File

@ -0,0 +1,37 @@
import { getApp } from "../../olympusapp";
import { GAME_MASTER } from "../../constants/constants";
import { UnitDatabase } from "./unitdatabase"
export class AircraftDatabase extends UnitDatabase {
constructor() {
super('api/databases/units/aircraftdatabase');
}
getCategory() {
return "Aircraft";
}
getSpawnPointsByName(name: string) {
if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns)
return 0;
const blueprint = this.getByName(name);
if (blueprint?.cost != undefined)
return blueprint?.cost;
if (blueprint?.era == "WW2")
return 20;
else if (blueprint?.era == "Early Cold War")
return 50;
else if (blueprint?.era == "Mid Cold War")
return 100;
else if (blueprint?.era == "Late Cold War")
return 200;
else if (blueprint?.era == "Modern")
return 400;
return 0;
}
}
export var aircraftDatabase = new AircraftDatabase();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
import { getApp } from "../../olympusapp";
import { GAME_MASTER } from "../../constants/constants";
import { UnitDatabase } from "./unitdatabase"
export class GroundUnitDatabase extends UnitDatabase {
constructor() {
super('api/databases/units/groundunitdatabase');
}
getSpawnPointsByName(name: string) {
if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns)
return 0;
const blueprint = this.getByName(name);
if (blueprint?.cost != undefined)
return blueprint?.cost;
if (blueprint?.era == "WW2")
return 20;
else if (blueprint?.era == "Early Cold War")
return 50;
else if (blueprint?.era == "Mid Cold War")
return 100;
else if (blueprint?.era == "Late Cold War")
return 200;
else if (blueprint?.era == "Modern")
return 400;
return 0;
}
getCategory() {
return "GroundUnit";
}
}
export var groundUnitDatabase = new GroundUnitDatabase();

View File

@ -0,0 +1,37 @@
import { getApp } from "../../olympusapp";
import { GAME_MASTER } from "../../constants/constants";
import { UnitDatabase } from "./unitdatabase"
export class HelicopterDatabase extends UnitDatabase {
constructor() {
super('api/databases/units/helicopterdatabase');
}
getSpawnPointsByName(name: string) {
if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns)
return 0;
const blueprint = this.getByName(name);
if (blueprint?.cost != undefined)
return blueprint?.cost;
if (blueprint?.era == "WW2")
return 20;
else if (blueprint?.era == "Early Cold War")
return 50;
else if (blueprint?.era == "Mid Cold War")
return 100;
else if (blueprint?.era == "Late Cold War")
return 200;
else if (blueprint?.era == "Modern")
return 400;
return 0;
}
getCategory() {
return "Helicopter";
}
}
export var helicopterDatabase = new HelicopterDatabase();

View File

@ -0,0 +1,36 @@
import { getApp } from "../../olympusapp";
import { GAME_MASTER } from "../../constants/constants";
import { UnitDatabase } from "./unitdatabase"
export class NavyUnitDatabase extends UnitDatabase {
constructor() {
super('api/databases/units/navyunitdatabase');
}
getSpawnPointsByName(name: string) {
if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns)
return 0;
const blueprint = this.getByName(name);
if (blueprint?.cost != undefined)
return blueprint?.cost;
if (blueprint?.era == "WW2")
return 20;
else if (blueprint?.era == "Early Cold War")
return 50;
else if (blueprint?.era == "Mid Cold War")
return 100;
else if (blueprint?.era == "Late Cold War")
return 200;
else if (blueprint?.era == "Modern")
return 400;
return 0;
}
getCategory() {
return "NavyUnit";
}
}
export var navyUnitDatabase = new NavyUnitDatabase();

View File

@ -0,0 +1,239 @@
import { LatLng } from "leaflet";
import { getApp } from "../../olympusapp";
import { GAME_MASTER } from "../../constants/constants";
import { UnitBlueprint } from "../../interfaces";
export abstract class UnitDatabase {
blueprints: { [key: string]: UnitBlueprint } = {};
#url: string;
constructor(url: string = "") {
this.#url = url;
this.load(() => {});
}
load(callback: CallableFunction) {
if (this.#url !== "") {
var xhr = new XMLHttpRequest();
xhr.open('GET', this.#url, true);
xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0");
xhr.responseType = 'json';
xhr.onload = () => {
var status = xhr.status;
if (status === 200) {
this.blueprints = xhr.response;
callback();
} else {
console.error(`Error retrieving database from ${this.#url}`)
}
};
xhr.send();
}
}
abstract getCategory(): string;
/* Gets a specific blueprint by name */
getByName(name: string) {
if (name in this.blueprints)
return this.blueprints[name];
return null;
}
/* Gets a specific blueprint by label */
getByLabel(label: string) {
for (let unit in this.blueprints) {
if (this.blueprints[unit].label === label)
return this.blueprints[unit];
}
return null;
}
getBlueprints(includeDisabled: boolean = false) {
if (getApp().getMissionManager().getCommandModeOptions().commandMode == GAME_MASTER || !getApp().getMissionManager().getCommandModeOptions().restrictSpawns) {
var filteredBlueprints: { [key: string]: UnitBlueprint } = {};
for (let unit in this.blueprints) {
const blueprint = this.blueprints[unit];
if (blueprint.enabled || includeDisabled)
filteredBlueprints[unit] = blueprint;
}
return filteredBlueprints;
}
else {
var filteredBlueprints: { [key: string]: UnitBlueprint } = {};
for (let unit in this.blueprints) {
const blueprint = this.blueprints[unit];
if ((blueprint.enabled || includeDisabled) && this.getSpawnPointsByName(blueprint.name) <= getApp().getMissionManager().getAvailableSpawnPoints() &&
getApp().getMissionManager().getCommandModeOptions().eras.includes(blueprint.era) &&
(!getApp().getMissionManager().getCommandModeOptions().restrictToCoalition || blueprint.coalition === getApp().getMissionManager().getCommandedCoalition() || blueprint.coalition === undefined)) {
filteredBlueprints[unit] = blueprint;
}
}
return filteredBlueprints;
}
}
/* Returns a list of all possible roles in a database */
getRoles() {
var roles: string[] = [];
var filteredBlueprints = this.getBlueprints();
for (let unit in filteredBlueprints) {
var loadouts = filteredBlueprints[unit].loadouts;
if (loadouts) {
for (let loadout of loadouts) {
for (let role of loadout.roles) {
if (role !== "" && !roles.includes(role))
roles.push(role);
}
}
}
}
return roles;
}
/* Returns a list of all possible types in a database */
getTypes(unitFilter?:CallableFunction) {
var filteredBlueprints = this.getBlueprints();
var types: string[] = [];
for (let unit in filteredBlueprints) {
if ( typeof unitFilter === "function" && !unitFilter(filteredBlueprints[unit]))
continue;
var type = filteredBlueprints[unit].type;
if (type && type !== "" && !types.includes(type))
types.push(type);
}
return types;
}
/* Returns a list of all possible periods in a database */
getEras() {
var filteredBlueprints = this.getBlueprints();
var eras: string[] = [];
for (let unit in filteredBlueprints) {
var era = filteredBlueprints[unit].era;
if (era && era !== "" && !eras.includes(era))
eras.push(era);
}
return eras;
}
/* Get all blueprints by range */
getByRange(range: string) {
var filteredBlueprints = this.getBlueprints();
var unitswithrange = [];
var minRange = 0;
var maxRange = 0;
if (range === "Short range") {
minRange = 0;
maxRange = 10000;
}
else if (range === "Medium range") {
minRange = 10000;
maxRange = 100000;
}
else {
minRange = 100000;
maxRange = 999999;
}
for (let unit in filteredBlueprints) {
var engagementRange = filteredBlueprints[unit].engagementRange;
if (engagementRange !== undefined) {
if (engagementRange >= minRange && engagementRange < maxRange) {
unitswithrange.push(filteredBlueprints[unit]);
}
}
}
return unitswithrange;
}
/* Get all blueprints by type */
getByType(type: string) {
var filteredBlueprints = this.getBlueprints();
var units = [];
for (let unit in filteredBlueprints) {
if (filteredBlueprints[unit].type === type) {
units.push(filteredBlueprints[unit]);
}
}
return units;
}
/* Get all blueprints by role */
getByRole(role: string) {
var filteredBlueprints = this.getBlueprints();
var units = [];
for (let unit in filteredBlueprints) {
var loadouts = filteredBlueprints[unit].loadouts;
if (loadouts) {
for (let loadout of loadouts) {
if (loadout.roles.includes(role) || loadout.roles.includes(role.toLowerCase())) {
units.push(filteredBlueprints[unit])
break;
}
}
}
}
return units;
}
/* Get the names of all the loadouts for a specific unit and for a specific role */
getLoadoutNamesByRole(name: string, role: string) {
var filteredBlueprints = this.getBlueprints();
var loadoutsByRole = [];
var loadouts = filteredBlueprints[name].loadouts;
if (loadouts) {
for (let loadout of loadouts) {
if (loadout.roles.includes(role) || loadout.roles.includes("")) {
loadoutsByRole.push(loadout.name)
}
}
}
return loadoutsByRole;
}
/* Get the livery names for a specific unit */
getLiveryNamesByName(name: string) {
var liveries = this.blueprints[name].liveries;
if (liveries !== undefined)
return Object.values(liveries);
else
return [];
}
/* Get the loadout content from the unit name and loadout name */
getLoadoutByName(name: string, loadoutName: string) {
var loadouts = this.blueprints[name].loadouts;
if (loadouts) {
for (let loadout of loadouts) {
if (loadout.name === loadoutName)
return loadout;
}
}
return null;
}
getSpawnPointsByLabel(label: string) {
var blueprint = this.getByLabel(label);
if (blueprint)
return this.getSpawnPointsByName(blueprint.name);
else
return Infinity;
}
getSpawnPointsByName(name: string) {
return Infinity;
}
getUnkownUnit(name: string): UnitBlueprint {
return {
name: name,
enabled: true,
coalition: 'neutral',
era: 'N/A',
label: name,
shortLabel: ''
}
}
}

View File

@ -0,0 +1,45 @@
import { Unit } from "./unit";
export class Group {
#members: Unit[] = [];
#name: string;
constructor(name: string) {
this.#name = name;
document.addEventListener("unitDeath", (e: any) => {
if (this.#members.includes(e.detail))
this.getLeader()?.onGroupChanged(e.detail);
});
}
getName() {
return this.#name;
}
addMember(member: Unit) {
if (!this.#members.includes(member)) {
this.#members.push(member);
member.setGroup(this);
this.getLeader()?.onGroupChanged(member);
}
}
removeMember(member: Unit) {
if (this.#members.includes(member)) {
delete this.#members[this.#members.indexOf(member)];
member.setGroup(null);
this.getLeader()?.onGroupChanged(member);
}
}
getMembers() {
return this.#members;
}
getLeader() {
return this.#members.find((unit: Unit) => { return (unit.getIsLeader() && unit.getAlive())})
}
}

View File

@ -0,0 +1,66 @@
//import { Dialog } from "../../dialog/dialog";
//import { createCheckboxOption } from "../../other/utils";
var categoryMap = {
"Aircraft": "Aircraft",
"Helicopter": "Helicopter",
"GroundUnit": "Ground units",
"NavyUnit": "Naval units"
}
export abstract class UnitDataFile {
protected data: any;
//protected dialog!: Dialog;
constructor() { }
buildCategoryCoalitionTable() {
const categories = this.#getCategoriesFromData();
const coalitions = ["blue", "neutral", "red"];
let headersHTML: string = ``;
let matrixHTML: string = ``;
//categories.forEach((category: string, index) => {
// matrixHTML += `<tr><td>${categoryMap[category as keyof typeof categoryMap]}</td>`;
//
// coalitions.forEach((coalition: string) => {
// if (index === 0)
// headersHTML += `<th data-coalition="${coalition}">${coalition[0].toUpperCase() + coalition.substring(1)}</th>`;
//
// const optionIsValid = this.data[category].hasOwnProperty(coalition);
// let checkboxHTML = createCheckboxOption(``, category, optionIsValid, () => { }, {
// "disabled": !optionIsValid,
// "name": "category-coalition-selection",
// "readOnly": !optionIsValid,
// "value" : `${category}:${coalition}`
// }).outerHTML;
//
// if (optionIsValid)
// checkboxHTML = checkboxHTML.replace(`"checkbox"`, `"checkbox" checked`); // inner and outerHTML screw default checked up
//
// matrixHTML += `<td data-coalition="${coalition}">${checkboxHTML}</td>`;
//
// });
// matrixHTML += "</tr>";
//});
//
//const table = <HTMLTableElement>this.dialog.getElement().querySelector("table.categories-coalitions");
//(<HTMLElement>table.tHead).innerHTML = `<tr><td>&nbsp;</td>${headersHTML}</tr>`;
//(<HTMLElement>table.querySelector(`tbody`)).innerHTML = matrixHTML;
}
#getCategoriesFromData() {
const categories = Object.keys(this.data);
categories.sort();
return categories;
}
getData() {
return this.data;
}
}

View File

@ -0,0 +1,101 @@
import { getApp } from "../../olympusapp";
//import { Dialog } from "../../dialog/dialog";
import { zeroAppend } from "../../other/utils";
import { Unit } from "../unit";
import { UnitDataFile } from "./unitdatafile";
export class UnitDataFileExport extends UnitDataFile {
protected data!: any;
//protected dialog: Dialog;
#element!: HTMLElement;
#filename: string = "export.json";
constructor(elementId: string) {
super();
//this.dialog = new Dialog(elementId);
//this.#element = this.dialog.getElement();
this.#element.querySelector(".start-transfer")?.addEventListener("click", (ev: MouseEventInit) => {
this.#doExport();
});
}
/**
* 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());
//
//units.filter((unit: Unit) => unit.getAlive() && unitCanBeExported(unit)).forEach((unit: Unit) => {
// const category = unit.getCategory();
// 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();
//
//const date = new Date();
//this.#filename = `olympus_${getApp().getMissionManager().getTheatre().replace(/[^\w]/gi, "").toLowerCase()}_${date.getFullYear()}${zeroAppend(date.getMonth() + 1, 2)}${zeroAppend(date.getDate(), 2)}_${zeroAppend(date.getHours(), 2)}${zeroAppend(date.getMinutes(), 2)}${zeroAppend(date.getSeconds(), 2)}.json`;
//var input = this.#element.querySelector("#export-filename") as HTMLInputElement;
//input.onchange = (ev: Event) => {
// this.#filename = (ev.currentTarget as HTMLInputElement).value;
//}
//if (input)
// input.value = this.#filename;
}
#doExport() {
let selectedUnits: Unit[] = [];
this.#element.querySelectorAll(`input[type="checkbox"][name="category-coalition-selection"]:checked`).forEach(<HTMLInputElement>(checkbox: HTMLInputElement) => {
if (checkbox instanceof HTMLInputElement) {
const [category, coalition] = checkbox.value.split(":"); // e.g. "category:coalition"
selectedUnits = selectedUnits.concat(this.data[category][coalition]);
}
});
if (selectedUnits.length === 0) {
alert("Please select at least one option for export.");
return;
}
var unitsToExport: { [key: string]: any } = {};
selectedUnits.forEach((unit: Unit) => {
var data: any = unit.getData();
if (unit.getGroupName() in unitsToExport)
unitsToExport[unit.getGroupName()].push(data);
else
unitsToExport[unit.getGroupName()] = [data];
});
const a = document.createElement("a");
const file = new Blob([JSON.stringify(unitsToExport)], { type: 'text/plain' });
a.href = URL.createObjectURL(file);
var filename = this.#filename;
if (!this.#filename.toLowerCase().endsWith(".json"))
filename += ".json";
a.download = filename;
a.click();
//this.dialog.hide();
}
}

View File

@ -0,0 +1,150 @@
import { getApp } from "../../olympusapp";
//import { Dialog } from "../../dialog/dialog";
import { UnitData } from "../../interfaces";
//import { ImportFileJSONSchemaValidator } from "../../schemas/schema";
import { UnitDataFile } from "./unitdatafile";
export class UnitDataFileImport extends UnitDataFile {
protected data!: any;
//protected dialog: Dialog;
#fileData!: { [key: string]: UnitData[] };
constructor(elementId: string) {
super();
//this.dialog = new Dialog(elementId);
//this.dialog.getElement().querySelector(".start-transfer")?.addEventListener("click", (ev: MouseEventInit) => {
// this.#doImport();
// this.dialog.hide();
//});
}
#doImport() {
//let selectedCategories: any = {};
//const unitsManager = getApp().getUnitsManager();
//
//this.dialog.getElement().querySelectorAll(`input[type="checkbox"][name="category-coalition-selection"]:checked`).forEach(<HTMLInputElement>(checkbox: HTMLInputElement) => {
// if (checkbox instanceof HTMLInputElement) {
// const [category, coalition] = checkbox.value.split(":"); // e.g. "category:coalition"
// selectedCategories[category] = selectedCategories[category] || {};
// selectedCategories[category][coalition] = true;
// }
//});
//
//for (const [groupName, groupData] of Object.entries(this.#fileData)) {
// if (groupName === "" || groupData.length === 0 || !this.#unitGroupDataCanBeImported(groupData))
// continue;
//
// let { category, coalition } = groupData[0];
//
// if (!selectedCategories.hasOwnProperty(category)
// || !selectedCategories[category].hasOwnProperty(coalition)
// || selectedCategories[category][coalition] !== true)
// continue;
//
// let unitsToSpawn = groupData.filter((unitData: UnitData) => this.#unitDataCanBeImported(unitData)).map((unitData: UnitData) => {
// return { unitType: unitData.name, location: unitData.position, liveryID: "", skill: "High" }
// });
//
// unitsManager.spawnUnits(category, unitsToSpawn, coalition, false);
//}
}
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)) {
// if (group === "" || units.length === 0)
// continue;
//
// if (units.some((unit: UnitData) => !this.#unitDataCanBeImported(unit)))
// continue;
//
// const category = units[0].category;
//
// if (!data.hasOwnProperty(category)) {
// data[category] = {};
// }
//
// units.forEach((unit: UnitData) => {
// if (!data[category].hasOwnProperty(unit.coalition))
// data[category][unit.coalition] = [];
//
// data[category][unit.coalition].push(unit);
// });
//
//}
//
//this.data = data;
//this.buildCategoryCoalitionTable();
//this.dialog.show();
}
#unitDataCanBeImported(unitData: UnitData) {
return unitData.alive && this.#unitGroupDataCanBeImported([unitData]);
}
#unitGroupDataCanBeImported(unitGroupData: UnitData[]) {
return unitGroupData.every((unitData: UnitData) => {
return !["Aircraft", "Helicopter"].includes(unitData.category);
}) && unitGroupData.some((unitData: UnitData) => unitData.alive);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,316 @@
import { LatLng, DivIcon, Map } from 'leaflet';
import { getApp } from '../olympusapp';
import { enumToCoalition, mToFt, msToKnots, rad2deg, zeroAppend } from '../other/utils';
import { CustomMarker } from '../map/markers/custommarker';
import { SVGInjector } from '@tanem/svg-injector';
import { DLINK, DataIndexes, GAME_MASTER, IRST, OPTIC, RADAR, VISUAL } from '../constants/constants';
import { DataExtractor } from '../server/dataextractor';
import { ObjectIconOptions } from '../interfaces';
export class Weapon extends CustomMarker {
ID: number;
#alive: boolean = false;
#coalition: string = "neutral";
#name: string = "";
#position: LatLng = new LatLng(0, 0, 0);
#speed: number = 0;
#heading: number = 0;
#hidden: boolean = false;
#detectionMethods: number[] = [];
getAlive() {return this.#alive};
getCoalition() {return this.#coalition};
getName() {return this.#name};
getPosition() {return this.#position};
getSpeed() {return this.#speed};
getHeading() {return this.#heading};
static getConstructor(type: string) {
if (type === "Missile") return Missile;
if (type === "Bomb") return Bomb;
}
constructor(ID: number) {
super(new LatLng(0, 0), { riseOnHover: true, keyboard: false });
this.ID = ID;
/* Update the marker when the options change */
document.addEventListener("mapOptionsChanged", (ev: CustomEventInit) => {
this.#updateMarker();
});
}
getCategory() {
// Overloaded by child classes
return "";
}
/********************** Unit data *************************/
setData(dataExtractor: DataExtractor) {
var updateMarker = !getApp().getMap().hasLayer(this);
var datumIndex = 0;
while (datumIndex != DataIndexes.endOfData) {
datumIndex = dataExtractor.extractUInt8();
switch (datumIndex) {
case DataIndexes.category: dataExtractor.extractString(); break;
case DataIndexes.alive: this.setAlive(dataExtractor.extractBool()); updateMarker = true; break;
case DataIndexes.coalition: this.#coalition = enumToCoalition(dataExtractor.extractUInt8()); break;
case DataIndexes.name: this.#name = dataExtractor.extractString(); break;
case DataIndexes.position: this.#position = dataExtractor.extractLatLng(); updateMarker = true; break;
case DataIndexes.speed: this.#speed = dataExtractor.extractFloat64(); updateMarker = true; break;
case DataIndexes.heading: this.#heading = dataExtractor.extractFloat64(); updateMarker = true; break;
}
}
if (updateMarker)
this.#updateMarker();
}
getData() {
return {
category: this.getCategory(),
ID: this.ID,
alive: this.#alive,
coalition: this.#coalition,
name: this.#name,
position: this.#position,
speed: this.#speed,
heading: this.#heading
}
}
getMarkerCategory(): string {
return "";
}
getIconOptions(): ObjectIconOptions {
// Default values, overloaded by child classes if needed
return {
showState: false,
showVvi: false,
showHealth: false,
showHotgroup: false,
showUnitIcon: true,
showShortLabel: false,
showFuel: false,
showAmmo: false,
showSummary: true,
showCallsign: true,
rotateToHeading: false
}
}
setAlive(newAlive: boolean) {
this.#alive = newAlive;
}
belongsToCommandedCoalition() {
if (getApp().getMissionManager().getCommandModeOptions().commandMode !== GAME_MASTER && getApp().getMissionManager().getCommandedCoalition() !== this.#coalition)
return false;
return true;
}
getType() {
return "";
}
/********************** Icon *************************/
createIcon(): void {
/* Set the icon */
var icon = new DivIcon({
className: 'leaflet-unit-icon',
iconAnchor: [25, 25],
iconSize: [50, 50],
});
this.setIcon(icon);
var el = document.createElement("div");
el.classList.add("unit");
el.setAttribute("data-object", `unit-${this.getMarkerCategory()}`);
el.setAttribute("data-coalition", this.#coalition);
// Generate and append elements depending on active options
// Velocity vector
if (this.getIconOptions().showVvi) {
var vvi = document.createElement("div");
vvi.classList.add("unit-vvi");
vvi.toggleAttribute("data-rotate-to-heading");
el.append(vvi);
}
// Main icon
if (this.getIconOptions().showUnitIcon) {
var unitIcon = document.createElement("div");
unitIcon.classList.add("unit-icon");
var img = document.createElement("img");
img.src = `/resources/theme/images/units/${this.getMarkerCategory()}.svg`;
img.onload = () => SVGInjector(img);
unitIcon.appendChild(img);
unitIcon.toggleAttribute("data-rotate-to-heading", this.getIconOptions().rotateToHeading);
el.append(unitIcon);
}
this.getElement()?.appendChild(el);
}
/********************** Visibility *************************/
updateVisibility() {
const hiddenUnits = getApp().getMap().getHiddenTypes();
var hidden = (hiddenUnits.includes(this.getMarkerCategory())) ||
(hiddenUnits.includes(this.#coalition)) ||
(!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0);
this.setHidden(hidden || !this.#alive);
}
setHidden(hidden: boolean) {
this.#hidden = hidden;
/* Add the marker if not present */
if (!getApp().getMap().hasLayer(this) && !this.getHidden()) {
if (getApp().getMap().isZooming())
this.once("zoomend", () => {this.addTo(getApp().getMap())})
else
this.addTo(getApp().getMap());
}
/* Hide the marker if necessary*/
if (getApp().getMap().hasLayer(this) && this.getHidden()) {
getApp().getMap().removeLayer(this);
}
}
getHidden() {
return this.#hidden;
}
setDetectionMethods(newDetectionMethods: number[]) {
if (!this.belongsToCommandedCoalition()) {
/* Check if the detection methods of this unit have changed */
if (this.#detectionMethods.length !== newDetectionMethods.length || this.getDetectionMethods().some(value => !newDetectionMethods.includes(value))) {
/* Force a redraw of the unit to reflect the new status of the detection methods */
this.setHidden(true);
this.#detectionMethods = newDetectionMethods;
this.#updateMarker();
}
}
}
getDetectionMethods() {
return this.#detectionMethods;
}
/***********************************************/
onAdd(map: Map): this {
super.onAdd(map);
return this;
}
#updateMarker() {
this.updateVisibility();
/* Draw the marker */
if (!this.getHidden()) {
if (this.getLatLng().lat !== this.#position.lat || this.getLatLng().lng !== this.#position.lng) {
this.setLatLng(new LatLng(this.#position.lat, this.#position.lng));
}
var element = this.getElement();
if (element != null) {
/* Draw the velocity vector */
element.querySelector(".unit-vvi")?.setAttribute("style", `height: ${15 + this.#speed / 5}px;`);
/* Set dead/alive flag */
element.querySelector(".unit")?.toggleAttribute("data-is-dead", !this.#alive);
/* Set altitude and speed */
if (element.querySelector(".unit-altitude"))
(<HTMLElement>element.querySelector(".unit-altitude")).innerText = "FL" + zeroAppend(Math.floor(mToFt(this.#position.alt as number) / 100), 3);
if (element.querySelector(".unit-speed"))
(<HTMLElement>element.querySelector(".unit-speed")).innerText = String(Math.floor(msToKnots(this.#speed))) + "GS";
/* Rotate elements according to heading */
element.querySelectorAll("[data-rotate-to-heading]").forEach(el => {
const headingDeg = rad2deg(this.#heading);
let currentStyle = el.getAttribute("style") || "";
el.setAttribute("style", currentStyle + `transform:rotate(${headingDeg}deg);`);
});
}
/* Set vertical offset for altitude stacking */
var pos = getApp().getMap().latLngToLayerPoint(this.getLatLng()).round();
this.setZIndexOffset(1000 + Math.floor(this.#position.alt as number) - pos.y);
}
}
}
export class Missile extends Weapon {
constructor(ID: number) {
super(ID);
}
getCategory() {
return "Missile";
}
getMarkerCategory() {
if (this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC))
return "missile";
else
return "aircraft";
}
getIconOptions() {
return {
showState: false,
showVvi: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))),
showHealth: false,
showHotgroup: false,
showUnitIcon: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))),
showShortLabel: false,
showFuel: false,
showAmmo: false,
showSummary: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))),
showCallsign: false,
rotateToHeading: this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC)
};
}
}
export class Bomb extends Weapon {
constructor(ID: number) {
super(ID);
}
getCategory() {
return "Bomb";
}
getMarkerCategory() {
if (this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC))
return "bomb";
else
return "aircraft";
}
getIconOptions() {
return {
showState: false,
showVvi: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))),
showHealth: false,
showHotgroup: false,
showUnitIcon: (this.belongsToCommandedCoalition() || this.getDetectionMethods().some(value => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value))),
showShortLabel: false,
showFuel: false,
showAmmo: false,
showSummary: (!this.belongsToCommandedCoalition() && !this.getDetectionMethods().some(value => [VISUAL, OPTIC].includes(value)) && this.getDetectionMethods().some(value => [RADAR, IRST, DLINK].includes(value))),
showCallsign: false,
rotateToHeading: this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC)
};
}
}

View File

@ -0,0 +1,109 @@
import { getApp } from "../olympusapp";
import { Weapon } from "./weapon";
import { DataIndexes } from "../constants/constants";
import { DataExtractor } from "../server/dataextractor";
import { Contact } from "../interfaces";
/** The WeaponsManager handles the creation and update of weapons. Data is strictly updated by the server ONLY. */
export class WeaponsManager {
#weapons: { [ID: number]: Weapon };
constructor() {
this.#weapons = {};
document.addEventListener("commandModeOptionsChanged", () => {Object.values(this.#weapons).forEach((weapon: Weapon) => weapon.updateVisibility())});
}
/**
*
* @returns All the existing weapons, both active and destroyed
*/
getWeapons() {
return this.#weapons;
}
/** Get a weapon by ID
*
* @param ID ID of the weapon
* @returns Weapon object, or null if input ID does not exist
*/
getWeaponByID(ID: number) {
if (ID in this.#weapons)
return this.#weapons[ID];
else
return null;
}
/** Add a new weapon to the manager
*
* @param ID ID of the new weapon
* @param category Either "Missile" or "Bomb". Determines what class will be used to create the new unit accordingly.
*/
addWeapon(ID: number, category: string) {
if (category){
/* The name of the weapon category is exactly the same as the constructor name */
var constructor = Weapon.getConstructor(category);
if (constructor != undefined) {
this.#weapons[ID] = new constructor(ID);
}
}
}
/** Update the data of all the weapons. The data is directly decoded from the binary buffer received from the REST Server. This is necessary for performance and bandwidth reasons.
*
* @param buffer The arraybuffer, encoded according to the ICD defined in: TODO Add reference to ICD
* @returns The decoded updateTime of the data update.
*/
update(buffer: ArrayBuffer) {
/* Extract the data from the arraybuffer. Since data is encoded dynamically (not all data is always present, but rather only the data that was actually updated since the last request).
No a prori casting can be performed. On the contrary, the array is decoded incrementally, depending on the DataIndexes of the data. The actual data decoding is performed by the Weapon class directly.
Every time a piece of data is decoded the decoder seeker is incremented. */
var dataExtractor = new DataExtractor(buffer);
var updateTime = Number(dataExtractor.extractUInt64());
/* Run until all data is extracted or an error occurs */
while (dataExtractor.getSeekPosition() < buffer.byteLength) {
/* Extract the weapon ID */
const ID = dataExtractor.extractUInt32();
/* If the ID of the weapon does not yet exist, create the weapon, if the category is known. If it isn't, some data must have been lost and we need to wait for another update */
if (!(ID in this.#weapons)) {
const datumIndex = dataExtractor.extractUInt8();
if (datumIndex == DataIndexes.category) {
const category = dataExtractor.extractString();
this.addWeapon(ID, category);
}
else {
/* Inconsistent data, we need to wait for a refresh */
return updateTime;
}
}
/* Update the data of the weapon */
this.#weapons[ID]?.setData(dataExtractor);
}
return updateTime;
}
/** For a given weapon, it returns if and how it is being detected by other units. NOTE: this function will return how a weapon is being detected, i.e. how other units are detecting it. It will not return
* what the weapon is detecting (mostly because weapons can't detect units).
*
* @param weapon The unit of which to retrieve the "detected" methods.
* @returns Array of detection methods
*/
getWeaponDetectedMethods(weapon: Weapon) {
var detectionMethods: number[] = [];
var units = getApp().getUnitsManager().getUnits();
for (let idx in units) {
if (units[idx].getAlive() && units[idx].getIsLeader() && units[idx].getCoalition() !== "neutral" && units[idx].getCoalition() != weapon.getCoalition())
{
units[idx].getContacts().forEach((contact: Contact) => {
if (contact.ID == weapon.ID && !detectionMethods.includes(contact.detectionMethod))
detectionMethods.push(contact.detectionMethod);
});
}
}
return detectionMethods;
}
}

View File

@ -8,7 +8,9 @@ export default {
extend: {
colors: {
background: {
steel: "#202831"
darker: "#202831",
dark: "#2D3742",
neutral: "#394552"
}
}
},

View File

@ -5,6 +5,7 @@ module.exports = function (configLocation) {
var logger = require('morgan');
var fs = require('fs');
var bodyParser = require('body-parser');
var cors = require('cors');
const { createProxyMiddleware } = require('http-proxy-middleware');
/* Default routers */
@ -39,6 +40,7 @@ module.exports = function (configLocation) {
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
app.use(cors())
/* Apply routers */
app.use('/', indexRouter);

View File

@ -3,6 +3,7 @@ module.exports = function (configLocation) {
var logger = require('morgan');
var enc = new TextEncoder();
const path = require('path');
var cors = require('cors');
var express = require('express');
var fs = require('fs');
@ -13,6 +14,7 @@ module.exports = function (configLocation) {
var app = express();
app.use(logger('dev'));
app.use(cors())
const aircraftDatabase = require(path.join(path.dirname(configLocation), '../Mods/Services/Olympus/databases/units/aircraftdatabase.json'));
const helicopterDatabase = require(path.join(path.dirname(configLocation),'../Mods/Services/Olympus/databases/units/helicopterdatabase.json'));
@ -458,7 +460,7 @@ module.exports = function (configLocation) {
};
mission(req, res){
var ret = {mission: {theatre: "MarianaIslands"}};
var ret = {mission: {theatre: "Nevada"}};
ret.time = Date.now();
ret.mission.dateAndTime = {

View File

@ -27,5 +27,8 @@
"tcp-ping-port": "^1.0.1",
"uuid": "^9.0.1",
"yargs": "^17.7.2"
},
"devDependencies": {
"cors": "^2.8.5"
}
}