Merge pull request #408 from Pax1601/407-create-a-simple-plugin-to-manage-units-database

407 create a simple plugin to manage units database
This commit is contained in:
Pax1601 2023-09-29 15:28:07 +02:00 committed by GitHub
commit 15c8e32fef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 88812 additions and 44327 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ node_modules
/client/public/plugins
/client/plugins/controltips/index.js
hgt
/client/public/databases/units/old

View File

@ -4,17 +4,6 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Chrome",
"port": 9222,
"urlFilter": "http://localhost:3000/*",
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/public/",
"sourceMapPathOverrides": {
"src/*": "${workspaceFolder}/src/*"
}
},
{
"type": "chrome",
"request": "launch",
@ -24,7 +13,8 @@
"sourceMapPathOverrides": {
"src/*": "${workspaceFolder}/src/*"
},
"preLaunchTask": "server"
"preLaunchTask": "server",
"port": 9222
}
]
}

View File

@ -1,7 +1,3 @@
declare module "index" {
import { OlympusApp } from "app";
export function getApp(): OlympusApp;
}
declare module "map/boxselect" {
export var BoxSelect: (new (...args: any[]) => any) & typeof import("leaflet").Class;
}
@ -338,7 +334,7 @@ declare module "mission/airbase" {
}
declare module "interfaces" {
import { LatLng } from "leaflet";
import { OlympusApp } from "app";
import { OlympusApp } from "olympusapp";
import { Airbase } from "mission/airbase";
export interface OlympusPlugin {
getName: () => string;
@ -546,6 +542,8 @@ declare module "interfaces" {
};
};
cost?: number;
barrelHeight?: number;
muzzleVelocity?: number;
}
export interface UnitSpawnOptions {
roleType: string;
@ -606,10 +604,12 @@ declare module "unit/databases/unitdatabase" {
import { LatLng } from "leaflet";
import { UnitBlueprint } from "interfaces";
export class UnitDatabase {
#private;
blueprints: {
[key: string]: UnitBlueprint;
};
constructor(url?: string);
load(callback: CallableFunction): void;
getCategory(): string;
getByName(name: string): UnitBlueprint | null;
getByLabel(label: string): UnitBlueprint | null;
@ -726,6 +726,7 @@ declare module "other/utils" {
export function getCheckboxOptions(dropdown: Dropdown): {
[key: string]: boolean;
};
export function getGroundElevation(latlng: LatLng, callback: CallableFunction): void;
}
declare module "controls/slider" {
import { Control } from "controls/control";
@ -1084,6 +1085,7 @@ declare module "unit/unit" {
carpetBomb(latlng: LatLng): void;
bombBuilding(latlng: LatLng): void;
fireAtArea(latlng: LatLng): void;
simulateFireFight(latlng: LatLng, groundElevation: number | null): void;
/***********************************************/
onAdd(map: Map): this;
}
@ -1293,13 +1295,21 @@ declare module "panels/panel" {
}
declare module "popups/popup" {
import { Panel } from "panels/panel";
export class PopupMessage {
#private;
constructor(text: string, fateTime: number);
getElement(): HTMLDivElement;
}
export class Popup extends Panel {
#private;
constructor(elementId: string);
constructor(ID: string, stackAfter?: number);
setFadeTime(fadeTime: number): void;
setText(text: string): void;
}
}
declare module "map/touchboxselect" {
export var TouchBoxSelect: (new (...args: any[]) => any) & typeof import("leaflet").Class;
}
declare module "map/map" {
import * as L from "leaflet";
import { MapContextMenu } from "contextmenus/mapcontextmenu";
@ -1530,9 +1540,12 @@ declare module "toolbars/commandmodetoolbar" {
}
}
declare module "toolbars/primarytoolbar" {
import { Dropdown } from "controls/dropdown";
import { Toolbar } from "toolbars/toolbar";
export class PrimaryToolbar extends Toolbar {
#private;
constructor(ID: string);
getMainDropdown(): Dropdown;
}
}
declare module "unit/citiesDatabase" {
@ -1748,6 +1761,11 @@ declare module "unit/unitsmanager" {
* @param latlng Location to fire at
*/
selectedUnitsFireAtArea(latlng: LatLng): void;
/** Instruct the selected units to simulate a fire fight at specific coordinates
*
* @param latlng Location to fire at
*/
selectedUnitsSimulateFireFight(latlng: LatLng): void;
/*********************** Control operations on selected units ************************/
/** See getUnitsCategories for more info
*
@ -1887,7 +1905,7 @@ declare module "server/servermanager" {
toggleDemoEnabled(): void;
setCredentials(newUsername: string, newPassword: string): void;
GET(callback: CallableFunction, uri: string, options?: ServerRequestOptions, responseType?: string): void;
POST(request: object, callback: CallableFunction): void;
PUT(request: object, callback: CallableFunction): void;
getConfig(callback: CallableFunction): void;
setAddress(address: string, port: number): void;
getAirbases(callback: CallableFunction): void;
@ -1933,6 +1951,7 @@ declare module "server/servermanager" {
carpetBomb(ID: number, latlng: LatLng, callback?: CallableFunction): void;
bombBuilding(ID: number, latlng: LatLng, callback?: CallableFunction): void;
fireAtArea(ID: number, latlng: LatLng, callback?: CallableFunction): void;
simulateFireFight(ID: number, latlng: LatLng, altitude: number, callback?: CallableFunction): void;
setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings, callback?: CallableFunction): void;
setCommandModeOptions(restrictSpawns: boolean, restrictToCoalition: boolean, spawnPoints: {
blue: number;
@ -1947,7 +1966,7 @@ declare module "server/servermanager" {
getPaused(): boolean;
}
}
declare module "app" {
declare module "olympusapp" {
import { Map } from "map/map";
import { MissionManager } from "mission/missionmanager";
import { PluginsManager } from "plugin/pluginmanager";
@ -1979,6 +1998,26 @@ declare module "app" {
* @returns The active coalition
*/
getActiveCoalition(): string;
/**
*
* @returns The aircraft database
*/
getAircraftDatabase(): import("unit/databases/aircraftdatabase").AircraftDatabase;
/**
*
* @returns The helicopter database
*/
getHelicopterDatabase(): import("unit/databases/helicopterdatabase").HelicopterDatabase;
/**
*
* @returns The ground unit database
*/
getGroundUnitDatabase(): import("unit/databases/groundunitdatabase").GroundUnitDatabase;
/**
*
* @returns The navy unit database
*/
getNavyUnitDatabase(): import("unit/databases/navyunitdatabase").NavyUnitDatabase;
/** Set a message in the login splash screen
*
* @param status The message to show in the login splash screen
@ -1987,3 +2026,7 @@ declare module "app" {
start(): void;
}
}
declare module "index" {
import { OlympusApp } from "olympusapp";
export function getApp(): OlympusApp;
}

View File

@ -3,10 +3,12 @@ var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var fs = require('fs');
var bodyParser = require('body-parser');
var atcRouter = require('./routes/api/atc');
var airbasesRouter = require('./routes/api/airbases');
var elevationRouter = require('./routes/api/elevation');
var databasesRouter = require('./routes/api/databases');
var indexRouter = require('./routes/index');
var uikitRouter = require('./routes/uikit');
var usersRouter = require('./routes/users');
@ -16,8 +18,8 @@ var pluginsRouter = require('./routes/plugins');
var app = express();
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(bodyParser.json({limit: '50mb'}));
app.use(bodyParser.urlencoded({limit: '50mb', extended: true}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
@ -25,6 +27,7 @@ app.use('/', indexRouter);
app.use('/api/atc', atcRouter);
app.use('/api/airbases', airbasesRouter);
app.use('/api/elevation', elevationRouter);
app.use('/api/databases', databasesRouter);
app.use('/plugins', pluginsRouter)
app.use('/users', usersRouter);
app.use('/uikit', uikitRouter);

View File

@ -1,3 +1,4 @@
copy .\\node_modules\\leaflet\\dist\\leaflet.css .\\public\\stylesheets\\leaflet\\leaflet.css
copy .\\node_modules\\leaflet-gesture-handling\\dist\\leaflet-gesture-handling.css .\\public\\stylesheets\\leaflet\\leaflet-gesture-handling.css
copy .\\node_modules\\leaflet.nauticscale\\dist\\leaflet.nauticscale.js .\\public\\javascripts\\leaflet.nauticscale.js
copy .\\node_modules\\leaflet-path-drag\\dist\\L.Path.Drag.js .\\public\\javascripts\\L.Path.Drag.js
copy .\\node_modules\\leaflet-path-drag\\dist\\L.Path.Drag.js .\\public\\javascripts\\L.Path.Drag.js

View File

@ -14,7 +14,7 @@ const DEMO_UNIT_DATA = {
TACAN: { isOn: false, XY: 'Y', callsign: 'TKR', channel: 40 },
radio: { frequency: 124000000, callsign: 1, callsignNumber: 1 },
generalSettings: { prohibitAA: false, prohibitAfterburner: false, prohibitAG: false, prohibitAirWpn: false, prohibitJettison: false },
ammo: [{ quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 } ],
ammo: [{ quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 }, { quantity: 2, name: "A cool missile with a longer name\0Ciao", guidance: 0, category: 0, missileCategory: 0 }, { quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 } , { quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 } , { quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 } , { quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 } , { quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 } , { quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 } , { quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 } ],
contacts: [{ID: 2, detectionMethod: 1}, {ID: 3, detectionMethod: 4}, {ID: 5, detectionMethod: 4}],
activePath: [{lat: 38, lng: -115, alt: 0}, {lat: 38, lng: -114, alt: 0}]
},
@ -96,7 +96,23 @@ const DEMO_UNIT_DATA = {
ammo: [{ quantity: 2, name: "A cool missile", guidance: 0, category: 0, missileCategory: 0 } ],
contacts: [{ID: 1, detectionMethod: 16}],
activePath: [ ]
},
}, ["7"]:{ category: "GroundUnit", alive: true, human: false, controlled: true, coalition: 1, country: 0, name: "T-55", unitName: "Cool guy 2-1", groupName: "Cool group 10", state: 1, task: "Being cool",
hasTask: false, position: { lat: 37.2, lng: -116.2, alt: 1000 }, speed: 200, heading: 315 * Math.PI / 180, isTanker: false, isAWACS: false, onOff: true, followRoads: false, fuel: 50,
desiredSpeed: 300, desiredSpeedType: 1, desiredAltitude: 1000, desiredAltitudeType: 1, leaderID: 0,
formationOffset: { x: 0, y: 0, z: 0 },
targetID: 0,
targetPosition: { lat: 0, lng: 0, alt: 0 },
ROE: 1,
reactionToThreat: 1,
emissionsCountermeasures: 1,
TACAN: { isOn: false, XY: 'Y', callsign: 'TKR', channel: 40 },
radio: { frequency: 124000000, callsign: 1, callsignNumber: 1 },
generalSettings: { prohibitAA: false, prohibitAfterburner: false, prohibitAG: false, prohibitAirWpn: false, prohibitJettison: false },
ammo: [{ quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 } ],
contacts: [{ID: 1001, detectionMethod: 16}],
activePath: [ ],
isLeader: true
},
}
const DEMO_WEAPONS_DATA = {

View File

@ -4118,9 +4118,9 @@
"dev": true
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"optional": true
},
@ -4648,6 +4648,11 @@
"integrity": "sha512-qnetYIYFqlrAbX7MaGsAkBsuA2fg3Z4xDRlEXJN1wSrnhNsIhQUzXt7Z3vapdEzx4r7VUqWTaqHYd7eY5C6Gfw==",
"dev": true
},
"leaflet-gesture-handling": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/leaflet-gesture-handling/-/leaflet-gesture-handling-1.2.2.tgz",
"integrity": "sha512-Blf5V4PoNphWkzL7Y1qge+Spkd4OCQ2atjwUNhMhLIcjKzPcBH++x/lwOinaR9jSqLWqJ6oKYO8d0XdTffy4hQ=="
},
"leaflet-path-drag": {
"version": "1.8.0-beta.3",
"resolved": "https://registry.npmjs.org/leaflet-path-drag/-/leaflet-path-drag-1.8.0-beta.3.tgz",

View File

@ -5,16 +5,19 @@
"version": "v0.4.4-alpha",
"private": true,
"scripts": {
"emit-declarations": "tsc --project tsconfig.json --declaration --emitDeclarationOnly --outfile ./@types/olympus/index.d.ts",
"copy": "copy.bat",
"start": "npm run copy & concurrently --kill-others \"npm run watch\" \"nodemon ./bin/www\"",
"start": "npm run copy & concurrently --kill-others \"npm run watch\" \"nodemon --ignore ./public/databases/ ./bin/www\"",
"watch": "watchify .\\src\\index.ts --debug -o .\\public\\javascripts\\bundle.js -t [ babelify --global true --presets [ @babel/preset-env ] --extensions '.js'] -p [ tsify --noImplicitAny ]"
},
"dependencies": {
"body-parser": "^1.20.2",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"ejs": "^3.1.8",
"express": "~4.16.1",
"express-basic-auth": "^1.2.1",
"leaflet-gesture-handling": "^1.2.2",
"morgan": "~1.9.1",
"save": "^2.9.0",
"srtm-elevation": "^2.1.2"

View File

@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Chrome",
"port": 9222,
"urlFilter": "http://localhost:3000/*",
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}../../public/",
"sourceMapPathOverrides": {
"src/*": "src/*"
},
"preLaunchTask": "start"
}
]
}

View File

@ -0,0 +1,13 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "start",
"type": "shell",
"command": "npm run start",
"isBackground": true
}
]
}

View File

@ -0,0 +1,4 @@
mkdir .\\..\\..\\public\\plugins\\databasemanager
copy .\\plugin.json .\\..\\..\\public\\plugins\\databasemanager\\plugin.json
copy .\\style.css .\\..\\..\\public\\plugins\\databasemanager\\style.css

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,13 @@
{
"name": "DatabaseManagerPlugin",
"version": "v0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "DatabaseManagerPlugin",
"version": "v0.0.1",
"devDependencies": {}
}
}
}

View File

@ -0,0 +1,13 @@
{
"name": "DatabaseManagerPlugin",
"version": "v0.0.1",
"private": true,
"scripts": {
"build": "browserify ./src/index.ts -p [ tsify --noImplicitAny] > index.js && copy.bat",
"start": "npm run copy & concurrently --kill-others \"npm run watch\"",
"copy": "copy.bat",
"watch": "watchify ./src/index.ts --debug -o ../../public/plugins/databasemanager/index.js -t [ babelify --global true --presets [ @babel/preset-env ] --extensions '.js'] -p [ tsify --noImplicitAny ]"
},
"dependencies": {},
"devDependencies": {}
}

View File

@ -0,0 +1,6 @@
{
"name": "Control Tip Plugin",
"version": "0.0.1",
"description": "This plugin shows useful control tips on the right side of the screen. The tips change dynamically depending on what the user does",
"author": "Peekaboo"
}

View File

@ -0,0 +1,105 @@
import { LoadoutBlueprint, UnitBlueprint } from "interfaces";
import { UnitEditor } from "./uniteditor";
import { LoadoutEditor } from "./loadouteditor";
import { addDropdownInput, addLoadoutsScroll, addNewElementInput, addStringInput } from "./utils";
/** Database editor for Air Units, both Aircraft and Helicopter since they are identical in terms of datbase entries.
*
*/
export class AirUnitEditor extends UnitEditor {
#loadoutEditor: LoadoutEditor | null = null;
constructor(contentDiv1: HTMLElement, contentDiv2: HTMLElement, contentDiv3: HTMLElement) {
super(contentDiv1, contentDiv2, contentDiv3);
/* The loadout editor allows to edit the loadout (who could have thought eh?) */
this.#loadoutEditor = new LoadoutEditor(this.contentDiv3);
/* Refresh the loadout editor if needed */
this.contentDiv3.addEventListener("refresh", () => {
if (this.visible)
this.#loadoutEditor?.show();
});
}
/** Sets a unit blueprint as the currently active one
*
* @param blueprint The blueprint to edit
*/
setBlueprint(blueprint: UnitBlueprint) {
this.blueprint = blueprint;
if (this.blueprint !== null) {
this.contentDiv2.replaceChildren();
var title = document.createElement("label");
title.innerText = "Unit properties";
this.contentDiv2.appendChild(title);
addStringInput(this.contentDiv2, "Name", blueprint.name, "text", (value: string) => { blueprint.name = value; }, true);
addStringInput(this.contentDiv2, "Label", blueprint.label, "text", (value: string) => { blueprint.label = value; });
addStringInput(this.contentDiv2, "Short label", blueprint.shortLabel, "text", (value: string) => { blueprint.shortLabel = value; });
addDropdownInput(this.contentDiv2, "Coalition", blueprint.coalition, ["", "blue", "red"],);
addDropdownInput(this.contentDiv2, "Era", blueprint.era, ["WW2", "Early Cold War", "Mid Cold War", "Late Cold War", "Modern"]);
addStringInput(this.contentDiv2, "Filename", blueprint.filename ?? "", "text", (value: string) => { blueprint.filename = value; });
addStringInput(this.contentDiv2, "Cost", String(blueprint.cost) ?? "", "number", (value: string) => { blueprint.cost = parseFloat(value); });
/* Add a scrollable list of loadouts that the user can edit */
var title = document.createElement("label");
title.innerText = "Loadouts";
this.contentDiv2.appendChild(title);
addLoadoutsScroll(this.contentDiv2, blueprint.loadouts ?? [], (loadout: LoadoutBlueprint) => {
this.#loadoutEditor?.setLoadout(loadout);
this.#loadoutEditor?.show();
});
addNewElementInput(this.contentDiv2, (ev: MouseEvent, input: HTMLInputElement) => { this.addLoadout(input.value); });
this.#loadoutEditor?.hide();
}
}
/** Add a new empty blueprint
*
* @param key Blueprint key
*/
addBlueprint(key: string) {
if (this.database != null) {
this.database.blueprints[key] = {
name: key,
coalition: "",
label: "",
shortLabel: "",
era: "",
loadouts: []
}
this.show();
this.setBlueprint(this.database.blueprints[key]);
}
}
/** Add a new empty loadout to the currently active blueprint
*
* @param loadoutName The name of the new loadout
*/
addLoadout(loadoutName: string) {
if (loadoutName && this.blueprint !== null) {
this.blueprint.loadouts?.push({
name: loadoutName,
code: "",
fuel: 1,
items: [],
roles: []
})
this.setBlueprint(this.blueprint);
}
}
/** Hide the editor
*
*/
hide() {
super.hide();
this.#loadoutEditor?.hide();
}
}

View File

@ -0,0 +1,399 @@
import { OlympusPlugin, UnitBlueprint } from "interfaces";
import { AirUnitEditor } from "./airuniteditor";
import { OlympusApp } from "olympusapp";
import { GroundUnitEditor } from "./grounduniteditor";
import { PrimaryToolbar } from "toolbars/primarytoolbar";
import { NavyUnitEditor } from "./navyuniteditor";
/** Database Manager
*
* This database provides a user interface to allow easier and convenient unit databases manipulation. It allows to edit all the fields of the units databases, save them
* on the server, and restore the defaults.
*
* TODO:
* Add ability to manage liveries
*
*/
export class DatabaseManagerPlugin implements OlympusPlugin {
#app: OlympusApp | null = null;
#element: HTMLElement;
#mainContentContainer: HTMLElement;
#contentDiv1: HTMLElement;
#contentDiv2: HTMLElement;
#contentDiv3: HTMLElement;
/* Upper tab buttons */
#button1: HTMLButtonElement;
#button2: HTMLButtonElement;
#button3: HTMLButtonElement;
#button4: HTMLButtonElement;
/* Lower operation buttons */
#button5: HTMLButtonElement;
#button6: HTMLButtonElement;
#button7: HTMLButtonElement;
#button8: HTMLButtonElement;
#button9: HTMLButtonElement;
/* Database editors */
#aircraftEditor: AirUnitEditor;
#helicopterEditor: AirUnitEditor;
#groundUnitEditor: GroundUnitEditor;
#navyUnitEditor: NavyUnitEditor;
constructor() {
/* Create main HTML element */
this.#element = document.createElement("div");
this.#element.id = "database-manager-panel";
this.#element.oncontextmenu = () => { return false; }
this.#element.classList.add("ol-dialog");
document.body.appendChild(this.#element);
/* Start hidden */
this.toggle(false);
/* Create the top tab buttons container and buttons */
let topButtonContainer = document.createElement("div");
this.#button1 = document.createElement("button");
this.#button1.classList.add("tab-button");
this.#button1.textContent = "Aircraft database";
this.#button1.onclick = () => { this.#hideAll(); this.#aircraftEditor.show(); this.#button1.classList.add("selected"); };
topButtonContainer.appendChild(this.#button1);
this.#button2 = document.createElement("button");
this.#button2.classList.add("tab-button");
this.#button2.textContent = "Helicopter database";
this.#button2.onclick = () => { this.#hideAll(); this.#helicopterEditor.show(); this.#button2.classList.add("selected"); };
topButtonContainer.appendChild(this.#button2);
this.#button3 = document.createElement("button");
this.#button3.classList.add("tab-button");
this.#button3.textContent = "Ground Unit database";
this.#button3.onclick = () => { this.#hideAll(); this.#groundUnitEditor.show(); this.#button3.classList.add("selected"); };
topButtonContainer.appendChild(this.#button3);
this.#button4 = document.createElement("button");
this.#button4.classList.add("tab-button");
this.#button4.textContent = "Navy Unit database";
this.#button4.onclick = () => { this.#hideAll(); this.#navyUnitEditor.show(); this.#button4.classList.add("selected"); };
topButtonContainer.appendChild(this.#button4);
this.#element.appendChild(topButtonContainer);
/* Create the container for the database editor elements and the elements themselves */
this.#mainContentContainer = document.createElement("div");
this.#mainContentContainer.classList.add("dm-container");
this.#element.appendChild(this.#mainContentContainer)
this.#contentDiv1 = document.createElement("div");
this.#contentDiv1.classList.add("dm-content-container");
this.#mainContentContainer.appendChild(this.#contentDiv1);
this.#contentDiv2 = document.createElement("div");
this.#contentDiv2.classList.add("dm-content-container");
this.#mainContentContainer.appendChild(this.#contentDiv2);
this.#contentDiv3 = document.createElement("div");
this.#contentDiv3.classList.add("dm-content-container");
this.#mainContentContainer.appendChild(this.#contentDiv3);
/* Create the database editors, which use the three divs created before */
this.#aircraftEditor = new AirUnitEditor(this.#contentDiv1, this.#contentDiv2, this.#contentDiv3);
this.#helicopterEditor = new AirUnitEditor(this.#contentDiv1, this.#contentDiv2, this.#contentDiv3);
this.#groundUnitEditor = new GroundUnitEditor(this.#contentDiv1, this.#contentDiv2, this.#contentDiv3);
this.#navyUnitEditor = new NavyUnitEditor(this.#contentDiv1, this.#contentDiv2, this.#contentDiv3);
/* Create the bottom buttons container. These buttons allow to save, restore, reset, and discard the changes */
let bottomButtonContainer = document.createElement("div");
this.#button5 = document.createElement("button");
this.#button5.textContent = "Save";
this.#button5.title = "Save the changes on the server"
this.#button5.onclick = () => { this.#saveDatabases();};
bottomButtonContainer.appendChild(this.#button5);
this.#button6 = document.createElement("button");
this.#button6.textContent = "Discard";
this.#button6.title = "Discard all changes and reload the database from the server";
this.#button6.onclick = () => { this.#loadDatabases(); };
bottomButtonContainer.appendChild(this.#button6);
this.#button7 = document.createElement("button");
this.#button7.textContent = "Reset defaults";
this.#button7.onclick = () => { this.#resetToDefaultDatabases(); };
this.#button7.title = "Reset the databases to the default values";
bottomButtonContainer.appendChild(this.#button7);
this.#button8 = document.createElement("button");
this.#button8.textContent = "Restore previous";
this.#button8.onclick = () => { this.#restoreToPreviousDatabases(); };
this.#button8.title = "Restore the previously saved databases. Use this if you saved a database by mistake.";
bottomButtonContainer.appendChild(this.#button8);
this.#button9 = document.createElement("button");
this.#button9.textContent = "Close";
this.#button9.title = "Close the Database Manager"
this.#button9.onclick = () => { this.toggle(false); };
bottomButtonContainer.appendChild(this.#button9);
this.#element.appendChild(bottomButtonContainer);
}
/**
*
* @returns The name of the plugin
*/
getName() {
return "Database Control Plugin"
}
/** Initialize the plugin
*
* @param app The OlympusApp singleton
* @returns True if successfull
*/
initialize(app: any) {
this.#app = app;
/* Load the databases and initialize the editors */
this.#loadDatabases();
/* Add a button to the main Olympus App to allow the users to open the dialog */
var mainButtonDiv = document.createElement("div");
var mainButton = document.createElement("button");
mainButton.textContent = "Database manager";
mainButtonDiv.appendChild(mainButton);
var toolbar: PrimaryToolbar = this.#app?.getToolbarsManager().get("primaryToolbar") as PrimaryToolbar;
var elements = toolbar.getMainDropdown().getOptionElements();
var arr = Array.prototype.slice.call(elements);
arr.splice(arr.length - 1, 0, mainButtonDiv);
toolbar.getMainDropdown().setOptionsElements(arr);
mainButton.onclick = () => {
toolbar.getMainDropdown().close();
this.toggle();
}
return true;
}
/**
*
* @returns The main container element
*/
getElement() {
return this.#element;
}
/** Toggles the visibility of the dialog
*
* @param bool Force a specific visibility state
*/
toggle(bool?: boolean) {
if (bool)
this.getElement().classList.toggle("hide", !bool);
else
this.getElement().classList.toggle("hide");
}
/** Hide all the editors
*
*/
#hideAll() {
this.#aircraftEditor.hide();
this.#helicopterEditor.hide();
this.#groundUnitEditor.hide();
this.#navyUnitEditor.hide();
this.#button1.classList.remove("selected");
this.#button2.classList.remove("selected");
this.#button3.classList.remove("selected");
this.#button4.classList.remove("selected");
}
/** Load the databases from the app to the editor. Note, this does not reload the databases from the server to the app
*
*/
#loadDatabases() {
var aircraftDatabase = this.#app?.getAircraftDatabase();
if (aircraftDatabase != null)
this.#aircraftEditor.setDatabase(aircraftDatabase);
var helicopterDatabase = this.#app?.getHelicopterDatabase();
if (helicopterDatabase != null)
this.#helicopterEditor.setDatabase(helicopterDatabase);
var groundUnitDatabase = this.#app?.getGroundUnitDatabase();
if (groundUnitDatabase != null)
this.#groundUnitEditor.setDatabase(groundUnitDatabase);
var navyUnitDatabase = this.#app?.getNavyUnitDatabase();
if (navyUnitDatabase != null)
this.#navyUnitEditor.setDatabase(navyUnitDatabase);
this.#hideAll();
this.#aircraftEditor.show();
this.#button1.classList.add("selected");
}
/** Save the databases on the server and reloads it to apply the changes
*
*/
#saveDatabases() {
var aircraftDatabase = this.#aircraftEditor.getDatabase();
if (aircraftDatabase){
this.#uploadDatabase(aircraftDatabase, "aircraftdatabase", "Aircraft database", () => {
var helicopterDatabase = this.#helicopterEditor.getDatabase();
if (helicopterDatabase) {
this.#uploadDatabase(helicopterDatabase, "helicopterDatabase", "Helicopter database", () => {
var groundUnitDatabase = this.#groundUnitEditor.getDatabase();
if (groundUnitDatabase) {
this.#uploadDatabase(groundUnitDatabase, "groundUnitDatabase", "Ground Unit database", () => {
var navyUnitDatabase = this.#navyUnitEditor.getDatabase();
if (navyUnitDatabase) {
this.#uploadDatabase(navyUnitDatabase, "navyUnitDatabase", "Navy Unit database", () => {
this.#app?.getAircraftDatabase().load(() => {});
this.#app?.getHelicopterDatabase().load(() => {});
this.#app?.getGroundUnitDatabase().load(() => {});
this.#app?.getNavyUnitDatabase().load(() => {});
});
}
});
}
});
}
});
}
}
/** Resets the databases to the default values
*
*/
#resetToDefaultDatabases() {
this.#resetToDefaultDatabase("aircraftdatabase", "Aircraft database", () => {
this.#app?.getAircraftDatabase().load(() => {
this.#resetToDefaultDatabase("helicopterdatabase", "Helicopter database", () => {
this.#app?.getHelicopterDatabase().load(() => {
this.#resetToDefaultDatabase("groundunitdatabase", "Ground Unit database", () => {
this.#app?.getGroundUnitDatabase().load(() => {
this.#resetToDefaultDatabase("navyunitdatabase", "Navy Unit database", () => {
this.#app?.getNavyUnitDatabase().load(() => {
this.#loadDatabases();
this.#hideAll();
this.#aircraftEditor.show();
this.#button1.classList.add("selected");
});
});
});
});
});
});
});
});
}
/** Restores the databases to the previous saved values. This is useful if you saved the databases by mistake and want to undo the error.
*
*/
#restoreToPreviousDatabases() {
this.#restoreToPreviousDatabase("aircraftdatabase", "Aircraft database", () => {
this.#app?.getAircraftDatabase().load(() => {
this.#restoreToPreviousDatabase("helicopterdatabase", "Helicopter database", () => {
this.#app?.getHelicopterDatabase().load(() => {
this.#restoreToPreviousDatabase("groundunitdatabase", "Ground Unit database", () => {
this.#app?.getGroundUnitDatabase().load(() => {
this.#restoreToPreviousDatabase("navyunitdatabase", "Navy Unit database", () => {
this.#app?.getNavyUnitDatabase().load(() => {
this.#loadDatabases();
this.#hideAll();
this.#aircraftEditor.show();
this.#button1.classList.add("selected");
});
});
});
});
});
});
});
});
}
/** Upload a single database to the server
*
* @param database The database
* @param name The name of the database as it will be saved on the server
* @param label A label used in the info popup
*/
#uploadDatabase(database: { blueprints: { [key: string]: UnitBlueprint } }, name: string, label: string, callback: CallableFunction) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("PUT", "/api/databases/save/units/" + name);
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.onload = (res: any) => {
if (xmlHttp.status == 200) {
this.#app?.getPopupsManager().get("infoPopup")?.setText(label + " saved successfully");
callback();
}
else {
this.#app?.getPopupsManager().get("infoPopup")?.setText("An error has occurred while saving the " + label);
}
};
xmlHttp.onerror = (res: any) => {
this.#app?.getPopupsManager().get("infoPopup")?.setText("An error has occurred while saving the " + label);
}
xmlHttp.send(JSON.stringify(database));
}
/** Resets a database to its default values on the server. NOTE: this only resets the database on the server, it will not reload it in the app.
*
* @param name The name of the database as it is saved on the server
* @param label A label used in the info popup
* @param callback Called when the operation is completed
*/
#resetToDefaultDatabase(name: string, label: string, callback: CallableFunction) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("PUT", "/api/databases/reset/units/" + name);
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.onload = (res: any) => {
if (xmlHttp.status == 200) {
this.#app?.getPopupsManager().get("infoPopup")?.setText(label + " reset successfully");
callback();
}
else {
this.#app?.getPopupsManager().get("infoPopup")?.setText("An error has occurred while resetting the " + label);
}
};
xmlHttp.onerror = (res: any) => {
this.#app?.getPopupsManager().get("infoPopup")?.setText("An error has occurred while resetting the " + label)
}
xmlHttp.send("");
}
/** Restores a database to its previously saved values on the server. NOTE: this only restores the database on the server, it will not reload it in the app.
*
* @param name The name of the database as it is saved on the server
* @param label A label used in the info popup
* @param callback Called when the operation is completed
*/
#restoreToPreviousDatabase(name: string, label: string, callback: CallableFunction) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("PUT", "/api/databases/restore/units/" + name);
xmlHttp.setRequestHeader("Content-Type", "application/json");
xmlHttp.onload = (res: any) => {
if (xmlHttp.status == 200) {
this.#app?.getPopupsManager().get("infoPopup")?.setText(label + " restored successfully");
callback();
}
else {
this.#app?.getPopupsManager().get("infoPopup")?.setText("An error has occurred while restoring the " + label);
}
};
xmlHttp.onerror = (res: any) => {
this.#app?.getPopupsManager().get("infoPopup")?.setText("An error has occurred while restoring the " + label)
}
xmlHttp.send("");
}
}

View File

@ -0,0 +1,59 @@
import { UnitBlueprint } from "interfaces";
import { UnitEditor } from "./uniteditor";
import { addDropdownInput, addStringInput } from "./utils";
/** Database editor for ground units
*
*/
export class GroundUnitEditor extends UnitEditor {
#blueprint: UnitBlueprint | null = null;
constructor(contentDiv1: HTMLElement, contentDiv2: HTMLElement, contentDiv3: HTMLElement) {
super(contentDiv1, contentDiv2, contentDiv3);
}
/** Sets a unit blueprint as the currently active one
*
* @param blueprint The blueprint to edit
*/
setBlueprint(blueprint: UnitBlueprint) {
this.#blueprint = blueprint;
if (this.#blueprint !== null) {
this.contentDiv2.replaceChildren();
var title = document.createElement("label");
title.innerText = "Unit properties";
this.contentDiv2.appendChild(title);
addStringInput(this.contentDiv2, "Name", blueprint.name, "text", (value: string) => {blueprint.name = value; }, true);
addStringInput(this.contentDiv2, "Label", blueprint.label, "text", (value: string) => {blueprint.label = value; });
addStringInput(this.contentDiv2, "Short label", blueprint.shortLabel, "text", (value: string) => {blueprint.shortLabel = value; });
addStringInput(this.contentDiv2, "Type", blueprint.type?? "", "text", (value: string) => {blueprint.type = value; });
addDropdownInput(this.contentDiv2, "Coalition", blueprint.coalition, ["", "blue", "red"],);
addDropdownInput(this.contentDiv2, "Era", blueprint.era, ["WW2", "Early Cold War", "Mid Cold War", "Late Cold War", "Modern"]);
//addStringInput(this.contentDiv2, "Filename", blueprint.filename?? "", "text", (value: string) => {blueprint.filename = value; });
addStringInput(this.contentDiv2, "Cost", String(blueprint.cost)?? "", "number", (value: string) => {blueprint.cost = parseFloat(value); });
addStringInput(this.contentDiv2, "Barrel height [m]", String(blueprint.barrelHeight)?? "", "number", (value: string) => {blueprint.barrelHeight = parseFloat(value); });
addStringInput(this.contentDiv2, "Muzzle velocity [m/s]", String(blueprint.muzzleVelocity)?? "", "number", (value: string) => {blueprint.muzzleVelocity = parseFloat(value); });
}
}
/** Add a new empty blueprint
*
* @param key Blueprint key
*/
addBlueprint(key: string) {
if (this.database != null) {
this.database.blueprints[key] = {
name: key,
coalition: "",
label: "",
shortLabel: "",
era: ""
}
this.show();
this.setBlueprint(this.database.blueprints[key]);
}
}
}

View File

@ -0,0 +1,5 @@
import { DatabaseManagerPlugin } from "./databasemanagerplugin";
globalThis.getOlympusPlugin = () => {
return new DatabaseManagerPlugin();
}

View File

@ -0,0 +1,54 @@
import { LoadoutBlueprint } from "interfaces";
import { addLoadoutItemsEditor, addStringInput } from "./utils";
/** The LoadoutEditor allows the user to edit a loadout
*
*/
export class LoadoutEditor {
#contentDiv: HTMLElement;
#loadout: LoadoutBlueprint | null = null;
#visible: boolean = false;
constructor(contentDiv: HTMLElement) {
this.#contentDiv = contentDiv;
this.#contentDiv.addEventListener("refresh", () => {
if (this.#visible)
this.show();
})
}
/** Set the loadout to edit
*
* @param loadout The loadout to edit
*/
setLoadout(loadout: LoadoutBlueprint) {
this.#loadout = loadout;
}
/** Show the editor
*
*/
show() {
this.#visible = true;
this.#contentDiv.replaceChildren();
var title = document.createElement("label");
title.innerText = "Loadout properties";
this.#contentDiv.appendChild(title);
if (this.#loadout) {
var laodout = this.#loadout;
addStringInput(this.#contentDiv, "Name", laodout.name, "text", (value: string) => {laodout.name = value; this.#contentDiv.dispatchEvent(new Event("refresh"));});
addStringInput(this.#contentDiv, "Code", laodout.code, "text", (value: string) => {laodout.code = value; });
addLoadoutItemsEditor(this.#contentDiv, this.#loadout);
}
}
/** Hide the editor
*
*/
hide() {
this.#visible = false;
this.#contentDiv.replaceChildren();
}
}

View File

@ -0,0 +1,59 @@
import { UnitBlueprint } from "interfaces";
import { UnitEditor } from "./uniteditor";
import { addDropdownInput, addStringInput } from "./utils";
/** Database editor for navy units
*
*/
export class NavyUnitEditor extends UnitEditor {
#blueprint: UnitBlueprint | null = null;
constructor(contentDiv1: HTMLElement, contentDiv2: HTMLElement, contentDiv3: HTMLElement) {
super(contentDiv1, contentDiv2, contentDiv3);
}
/** Sets a unit blueprint as the currently active one
*
* @param blueprint The blueprint to edit
*/
setBlueprint(blueprint: UnitBlueprint) {
this.#blueprint = blueprint;
if (this.#blueprint !== null) {
this.contentDiv2.replaceChildren();
var title = document.createElement("label");
title.innerText = "Unit properties";
this.contentDiv2.appendChild(title);
addStringInput(this.contentDiv2, "Name", blueprint.name, "text", (value: string) => {blueprint.name = value; }, true);
addStringInput(this.contentDiv2, "Label", blueprint.label, "text", (value: string) => {blueprint.label = value; });
addStringInput(this.contentDiv2, "Short label", blueprint.shortLabel, "text", (value: string) => {blueprint.shortLabel = value; });
addStringInput(this.contentDiv2, "Type", blueprint.type?? "", "text", (value: string) => {blueprint.type = value; });
addDropdownInput(this.contentDiv2, "Coalition", blueprint.coalition, ["", "blue", "red"],);
addDropdownInput(this.contentDiv2, "Era", blueprint.era, ["WW2", "Early Cold War", "Mid Cold War", "Late Cold War", "Modern"]);
//addStringInput(this.contentDiv2, "Filename", blueprint.filename?? "", "text", (value: string) => {blueprint.filename = value; });
addStringInput(this.contentDiv2, "Cost", String(blueprint.cost)?? "", "number", (value: string) => {blueprint.cost = parseFloat(value); });
addStringInput(this.contentDiv2, "Barrel height [m]", String(blueprint.barrelHeight)?? "", "number", (value: string) => {blueprint.barrelHeight = parseFloat(value); });
addStringInput(this.contentDiv2, "Muzzle velocity [m/s]", String(blueprint.muzzleVelocity)?? "", "number", (value: string) => {blueprint.muzzleVelocity = parseFloat(value); });
}
}
/** Add a new empty blueprint
*
* @param key Blueprint key
*/
addBlueprint(key: string) {
if (this.database != null) {
this.database.blueprints[key] = {
name: key,
coalition: "",
label: "",
shortLabel: "",
era: ""
}
this.show();
this.setBlueprint(this.database.blueprints[key]);
}
}
}

View File

@ -0,0 +1,99 @@
import { UnitBlueprint } from "interfaces";
import { UnitDatabase } from "unit/databases/unitdatabase";
import { addBlueprintsScroll, addNewElementInput } from "./utils";
/** Base abstract class of Unit database editors
*
*/
export abstract class UnitEditor {
blueprint: UnitBlueprint | null = null;
database: {blueprints: {[key: string]: UnitBlueprint}} | null = null;
visible: boolean = false;
contentDiv1: HTMLElement;
contentDiv2: HTMLElement;
contentDiv3: HTMLElement;
constructor(contentDiv1: HTMLElement, contentDiv2: HTMLElement, contentDiv3: HTMLElement) {
this.contentDiv1 = contentDiv1;
this.contentDiv2 = contentDiv2;
this.contentDiv3 = contentDiv3;
/* Refresh the list of units if it changes */
this.contentDiv1.addEventListener("refresh", () => {
if (this.visible)
this.show();
})
/* If the unit properties or loadout are edited, reload the editor */
this.contentDiv2.addEventListener("refresh", () => {
if (this.visible) {
if (this.blueprint !== null)
this.setBlueprint(this.blueprint);
}
});
this.contentDiv3.addEventListener("refresh", () => {
if (this.visible) {
if (this.blueprint !== null)
this.setBlueprint(this.blueprint);
}
});
}
/**
*
* @param database The database that the editor will operate on
*/
setDatabase(database: UnitDatabase) {
this.database = JSON.parse(JSON.stringify(database));
}
/** Show the editor
*
*/
show() {
this.visible = true;
this.contentDiv1.replaceChildren();
this.contentDiv2.replaceChildren();
this.contentDiv3.replaceChildren();
/* Create the list of units. Each unit is clickable to activate the editor on it */
if (this.database != null) {
var title = document.createElement("label");
title.innerText = "Units list";
this.contentDiv1.appendChild(title);
addBlueprintsScroll(this.contentDiv1, this.database, (key: string) => {
if (this.database != null)
this.setBlueprint(this.database.blueprints[key])
})
addNewElementInput(this.contentDiv1, (ev: MouseEvent, input: HTMLInputElement) => {
if (input.value != "")
this.addBlueprint((input).value);
});
}
}
/** Hid the editor
*
*/
hide() {
this.visible = false;
this.contentDiv1.replaceChildren();
this.contentDiv2.replaceChildren();
this.contentDiv3.replaceChildren();
}
/**
*
* @returns The edited database
*/
getDatabase() {
return this.database;
}
/* Abstract methods which will depend on the specific type of units */
abstract setBlueprint(blueprint: UnitBlueprint): void;
abstract addBlueprint(key: string): void;
}

View File

@ -0,0 +1,212 @@
import { LoadoutBlueprint, LoadoutItemBlueprint, UnitBlueprint } from "interfaces";
/** This file contains a set of utility functions that are reused in the various editors and allows to declutter the classes
*
*/
/** Add a string input in the form of String: [ value ]
*
* @param div The HTMLElement that will contain the input
* @param key The key of the input, which will be used as label
* @param value The initial value of the input
* @param type The type of the input, e.g. "Text" or "Number" as per html standard
* @param callback Callback called when the user enters a new value
* @param disabled If true, the input will be disabled and read only
*/
export function addStringInput(div: HTMLElement, key: string, value: string, type: string, callback: CallableFunction, disabled?: boolean) {
var row = document.createElement("div");
var dt = document.createElement("dt");
var dd = document.createElement("dd");
dt.innerText = key;
var input = document.createElement("input");
input.value = value;
input.textContent = value;
input.type = type?? "text";
input.disabled = disabled?? false;
input.onchange = () => callback(input.value);
dd.appendChild(input);
row.appendChild(dt);
row.appendChild(dd);
row.classList.add("input-row");
div.appendChild(row);
}
/** Add a dropdown (select) input
*
* @param div The HTMLElement that will contain the input
* @param key The key of the input, which will be used as label
* @param value The initial value of the input
* @param options The dropdown options
*/
export function addDropdownInput(div: HTMLElement, key: string, value: string, options: string[]) {
var row = document.createElement("div");
var dt = document.createElement("dt");
var dd = document.createElement("dd");
dt.innerText = key;
var select = document.createElement("select");
options.forEach((option: string) => {
var el = document.createElement("option");
el.value = option;
el.innerText = option;
select.appendChild(el);
});
select.value = value;
dd.appendChild(select);
row.appendChild(dt);
row.appendChild(dd);
row.classList.add("input-row");
div.appendChild(row);
}
/** Create a loadout items editor. This editor allows to add or remove loadout items, as well as changing their name and quantity
*
* @param div The HTMLElement that will contain the editor
* @param loadout The loadout to edit
*/
export function addLoadoutItemsEditor(div: HTMLElement, loadout: LoadoutBlueprint) {
var itemsEl = document.createElement("div");
itemsEl.classList.add("dm-scroll-container", "dm-items-container");
/* Create a row for each loadout item to allow and change the name and quantity of the item itself */
loadout.items.forEach((item: LoadoutItemBlueprint, index: number) => {
var rowDiv = document.createElement("div");
var nameLabel = document.createElement("label");
nameLabel.innerText = "Name"
rowDiv.appendChild(nameLabel);
var nameInput = document.createElement("input");
rowDiv.appendChild(nameInput);
nameInput.textContent = item.name;
nameInput.value = item.name
nameInput.onchange = () => { loadout.items[index].name = nameInput.value; }
var quantityLabel = document.createElement("label");
quantityLabel.innerText = "Quantity"
rowDiv.appendChild(quantityLabel);
var quantityInput = document.createElement("input");
rowDiv.appendChild(quantityInput);
quantityInput.textContent = String(item.quantity);
quantityInput.value = String(item.quantity);
quantityInput.type = "number";
quantityInput.step = "1";
quantityInput.onchange = () => { loadout.items[index].quantity = parseInt(quantityInput.value); }
/* This button allows to remove the item */
var button = document.createElement("button");
button.innerText = "X";
button.onclick = () => {
loadout.items.splice(index, 1);
div.dispatchEvent(new Event("refresh"));
}
rowDiv.appendChild(button);
itemsEl.appendChild(rowDiv);
})
div.appendChild(itemsEl);
/* Button to add a new item to the loadout */
var inputDiv = document.createElement("div");
inputDiv.classList.add("dm-new-item-input");
var button = document.createElement("button");
button.innerText = "Add";
inputDiv.appendChild(button);
div.appendChild(inputDiv);
button.addEventListener("click", (ev: MouseEvent) => {
loadout?.items.push({
name: "",
quantity: 1
})
div.dispatchEvent(new Event("refresh"));
});
}
/** Add a input and button to create a new element in a list. It uses a generic callback to actually add the element.
*
* @param div The HTMLElement that will contain the input and button
* @param callback Callback called when the user clicks on "Add"
*/
export function addNewElementInput(div: HTMLElement, callback: CallableFunction) {
var inputDiv = document.createElement("div");
inputDiv.classList.add("dm-new-element-input");
var input = document.createElement("input");
inputDiv.appendChild(input);
var button = document.createElement("button");
button.innerText = "Add";
button.addEventListener("click", (ev: MouseEvent) => callback(ev, input));
inputDiv.appendChild(button);
div.appendChild(inputDiv);
}
/** Add a scrollable list of blueprints
*
* @param div The HTMLElement that will contain the list
* @param database The database that will be used to fill the list of blueprints
* @param callback Callback called when the user clicks on one of the elements
*/
export function addBlueprintsScroll(div: HTMLElement, database: {blueprints: {[key: string]: UnitBlueprint}}, callback: CallableFunction) {
var scrollDiv = document.createElement("div");
scrollDiv.classList.add("dm-scroll-container");
if (database !== null) {
var blueprints: {[key: string]: UnitBlueprint} = database.blueprints;
for (let key in blueprints) {
var rowDiv = document.createElement("div");
scrollDiv.appendChild(rowDiv);
var text = document.createElement("label");
text.textContent = key;
text.onclick = () => callback(key);
rowDiv.appendChild(text);
/* This button allows to remove an element from the list. It requires a refresh. */
var button = document.createElement("button");
button.innerText = "X";
button.onclick = () => {
delete blueprints[key];
div.dispatchEvent(new Event("refresh"));
}
rowDiv.appendChild(button);
}
}
div.appendChild(scrollDiv);
}
/** Add a scrollable list of loadouts
*
* @param div The HTMLElement that will contain the list
* @param loadouts The loadouts that will be used to fill the list
* @param callback Callback called when the user clicks on one of the elements
*/
export function addLoadoutsScroll(div: HTMLElement, loadouts: LoadoutBlueprint[], callback: CallableFunction) {
var loadoutsEl = document.createElement("div");
loadoutsEl.classList.add("dm-scroll-container", "dm-loadout-container")
loadouts.forEach((loadout: LoadoutBlueprint, index: number) => {
var rowDiv = document.createElement("div");
loadoutsEl.appendChild(rowDiv);
var text = document.createElement("label");
text.textContent = loadout.name;
text.onclick = () => { callback(loadout) };
rowDiv.appendChild(text);
/* The "Empty loadout" can not be removed */
if (loadout.name !== "Empty loadout") {
/* This button allows to remove an element from the list. It requires a refresh. */
var button = document.createElement("button");
button.innerText = "X";
button.onclick = () => {
loadouts.splice(index, 1);
div.dispatchEvent(new Event("refresh"));
}
rowDiv.appendChild(button);
}
});
div.appendChild(loadoutsEl);
}

View File

@ -0,0 +1,239 @@
#database-manager-panel {
flex-direction: column;
display: flex;
width: 80%;
height: 80%;
padding: 10px;
border-radius: 5px;
background-color: var(--background-steel) !important;
z-index: 9999999;
}
@media (orientation: landscape) {
.dm-container {
flex-direction: row;
}
}
@media (orientation: portrait) {
.dm-container {
flex-direction: column;
}
}
#database-manager-panel * {
font-size: 13;
font-family: 'Open Sans', sans-serif !important;
user-select: none;
}
#database-manager-panel>div:first-child {
display: flex;
align-items: center;
}
#database-manager-panel>div:last-child {
display: flex;
column-gap: 5px;
align-items: center;
justify-content: end;
justify-items: end;
margin-top: 5px;
}
#database-manager-panel>div:last-child>button {
border: 1px solid white;
}
.dm-container {
background-color: var(--background-grey);
border: 2px solid #777777;
position: relative;
display: flex;
width: 100%;
padding: 5px;
height: calc(100% - 64px - 5px);
border-radius: 0px 5px 5px 5px;
}
.dm-container>div:nth-child(2) {
width: 500px;
}
.dm-container>div:nth-child(3) {
flex: 1;
}
.dm-content-container {
position: relative;
margin: 10px;
display: flex;
flex-direction: column;
row-gap: 5px;
}
@media (orientation: landscape) {
.dm-content-container {
height: calc(100% - 20px);
min-width: 200px;
width: fit-content;
}
}
@media (orientation: portrait) {
.dm-content-container {
width: 100% - calc(20px);
height: 30%;
}
}
.dm-content-container>label {
font-size: 18px !important;
font-weight: bold;
}
.dm-scroll-container {
display: flex;
flex-direction: column;
overflow-y: auto;
max-height: 100%;
color: black;
font-weight: bold;
}
.dm-scroll-container>div:nth-child(even) {
background-color: gainsboro;
}
.dm-scroll-container>div:nth-child(odd) {
background-color: white;
}
.dm-scroll-container>div *:nth-child(1) {
height: 100%;
width: calc(100% - 25px);
padding: 2px;
text-wrap: wrap;
word-wrap: break-word;
}
.dm-scroll-container>div *:nth-child(1):hover {
background-color: var(--secondary-blue-text);
color: white;
cursor: pointer;
}
.dm-scroll-container>div {
display: flex;
align-items: center;
justify-content: space-between;
}
.dm-scroll-container>div>button {
height: 20px;
width: 20px;
padding: 0px;
}
.input-row {
width: 100%;
display: flex;
flex-direction: row;
}
.input-row>dt {
width: 250px;
}
.input-row>dd {
width: 100%;
}
.input-row>dd>* {
width: 100%;
font-weight: bold;
}
.dm-loadout-container {
max-height: 100%;
max-width: 500px;
width: 100%;
}
.dm-items-container {
max-height: 100%;
height: fit-content;
}
.dm-items-container>div {
display: flex;
align-items: center;
column-gap: 2px;
}
.dm-items-container>div>label {
width: 80px !important;
}
.dm-items-container div>input:nth-of-type(1) {
flex: 1;
font-weight: bold;
}
.dm-items-container div>input:nth-of-type(2) {
width: 40px;
font-weight: bold;
}
.dm-new-element-input {
display: flex;
flex-direction: row;
column-gap: 2px;
width: 100%;
align-items: center;
}
.dm-new-element-input>input {
width: 100%;
}
.dm-new-element-input>button {
width: 60px;
}
.dm-new-item-input {
display: flex;
justify-content: end;
}
.dm-new-item-input>button {
width: 60px;
}
.tab-button {
transform: translateY(+3px);
background-color: var(--background-steel);
border-radius: 0;
border-bottom: 2px solid transparent !important;
border-top: 2px solid #777777 !important;
border-left: 2px solid #777777 !important;
border-right: 0px solid #777777 !important;
}
.tab-button.selected {
background-color: var(--background-grey);
z-index: 10;
}
.tab-button:first-of-type {
border-top-left-radius: 5px;
}
.tab-button:last-of-type {
border-top-right-radius: 5px;
border-right: 2px solid #777777 !important;
}
#database-manager-panel button :not(.dm-scroll-container>div) {
border: 1px solid white;
}

View File

@ -0,0 +1,103 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ES2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"rootDirs": ["./src"], /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
"typeRoots": [
"./node_modules/@types",
"../../@types"
], /* Specify multiple folders that act like './node_modules/@types'. */
"types": [
"olympus"
], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": [
"src/*.ts"
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,767 +0,0 @@
{
"An-26B": {
"name": "An-26B",
"coalition": "red",
"label": "An-26B Curl",
"era": "Mid Cold War",
"shortLabel": "26",
"loadouts": [
{
"fuel": 1,
"items": [],
"roles": [
"Transport"
],
"code": "",
"name": "Empty Loadout"
}
],
"filename": "an-26.png",
"cost": 0
},
"An-30M": {
"name": "An-30M",
"coalition": "red",
"label": "An-30M Clank",
"era": "Mid Cold War",
"shortLabel": "30",
"loadouts": [
{
"fuel": 1,
"items": [],
"roles": [
"Transport"
],
"code": "",
"name": "Empty Loadout"
}
],
"filename": "a-50.png",
"cost": 0
},
"C-130": {
"name": "C-130",
"label": "C-130 Hercules",
"era": "Early Cold War",
"shortLabel": "130",
"loadouts": [
{
"fuel": 1,
"items": [],
"roles": [
"Transport"
],
"code": "",
"name": "Empty Loadout"
}
],
"filename": "c-130.png",
"cost": 0
},
"F-14A-135-GR": {
"name": "F-14A-135-GR",
"coalition": "blue",
"label": "F-14A-135-GR Tomcat",
"era": "Mid Cold War",
"shortLabel": "14A",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "fuel",
"quantity": 2
},
{
"name": "AIM-54A",
"quantity": 2
},
{
"name": "AIM-7F",
"quantity": 1
},
{
"name": "AIM-9L",
"quantity": 4
}
],
"roles": [
"CAP"
],
"code": "AIM-54A-MK47*2, AIM-7F*1, AIM-9L*4, XT*2",
"name": "Heavy / Fox 3 / Long Range"
},
{
"fuel": 1,
"items": [
{
"name": "fuel",
"quantity": 2
},
{
"name": "AIM-7F",
"quantity": 4
},
{
"name": "AIM-9L",
"quantity": 4
}
],
"roles": [
"CAP"
],
"code": "AIM-7F*4, AIM-9L*4, XT*2",
"name": "Heavy / Fox 1 / Long Range"
},
{
"fuel": 1,
"items": [
{
"name": "fuel",
"quantity": 2
},
{
"name": "AIM-7M",
"quantity": 1
},
{
"name": "AIM-9M",
"quantity": 2
},
{
"name": "GBU-12",
"quantity": 2
},
{
"name": "LANTIRN",
"quantity": 1
}
],
"roles": [
"Strike"
],
"code": "AIM-7M*1, AIM-9M*2, XT*2, GBU-12*2, LANTIRN",
"name": "Heavy / Fox 3, GBU-12 / Long Range"
},
{
"fuel": 1,
"items": [],
"roles": [
""
],
"code": "",
"name": "Empty Loadout"
}
],
"filename": "f-14.png",
"cost": 300,
"liveryID": "IRIAF Asia Minor"
},
"F-4E": {
"name": "F-4E",
"coalition": "blue",
"label": "F-4E Phantom II",
"era": "Mid Cold War",
"shortLabel": "4",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "fuel",
"quantity": 2
},
{
"name": "AIM-7M",
"quantity": 4
},
{
"name": "AIM-9M",
"quantity": 4
}
],
"roles": [
"CAP"
],
"code": "AIM-9*4,AIM-7*4,Fuel*2",
"name": "Heavy / Fox 1 / Long Range"
},
{
"fuel": 1,
"items": [
{
"name": "ECM",
"quantity": 1
},
{
"name": "AIM-7M",
"quantity": 2
},
{
"name": "Mk-82",
"quantity": 18
}
],
"roles": [
"Strike"
],
"code": "Mk-82*18,AIM-7*2,ECM",
"name": "Heavy / Fox 1, Mk-82 / Short Range"
},
{
"fuel": 1,
"items": [],
"roles": [
""
],
"code": "",
"name": "Empty Loadout"
}
],
"filename": "f-4.png",
"cost": 100,
"liveryID": "IRIAF Asia Minor"
},
"F-5E-3": {
"name": "F-5E-3",
"coalition": "blue",
"label": "F-5E Tiger",
"era": "Mid Cold War",
"shortLabel": "5",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "Fuel 275",
"quantity": 3
},
{
"name": "AIM-9P5",
"quantity": 2
}
],
"roles": [
"CAP"
],
"code": "AIM-9P5*2, Fuel 275*3",
"name": "Heavy / Fox 2 / Long Range"
},
{
"fuel": 1,
"items": [
{
"name": "Mk-82",
"quantity": 4
},
{
"name": "AIM-9P5",
"quantity": 2
},
{
"name": "Fuel 275",
"quantity": 1
}
],
"roles": [
"Strike"
],
"code": "Mk-82LD*4,AIM-9P*2,Fuel 275",
"name": "Heavy / Fox 2 / Short Range"
},
{
"fuel": 1,
"items": [],
"roles": [
""
],
"code": "",
"name": "Empty Loadout"
}
],
"liveryID": "ir iriaf 43rd tfs",
"filename": "f-5.png",
"cost": 80
},
"F-86F Sabre": {
"name": "F-86F Sabre",
"coalition": "blue",
"label": "F-86F Sabre",
"era": "Early Cold War",
"shortLabel": "86",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "120gal Fuel",
"quantity": 2
}
],
"roles": [
"CAP"
],
"code": "120gal Fuel*2",
"name": "Light / Guns / Short Range"
},
{
"fuel": 1,
"items": [
{
"name": "HVAR",
"quantity": 16
}
],
"roles": [
"CAS"
],
"code": "HVAR*16",
"name": "Light / HVAR / Short Range"
},
{
"fuel": 1,
"items": [
{
"name": "AN-M64",
"quantity": 2
}
],
"roles": [
"Strike"
],
"code": "AN-M64*2",
"name": "Light / AN-M64 / Short Range"
},
{
"fuel": 1,
"items": [],
"roles": [
""
],
"code": "",
"name": "Light / Guns / Short Range"
}
],
"filename": "f-86.png",
"cost": 40,
"liveryID": "iiaf bare metall"
},
"IL-76MD": {
"name": "IL-76MD",
"label": "IL-76MD Candid",
"era": "Mid Cold War",
"shortLabel": "76",
"loadouts": [
{
"fuel": 1,
"items": [],
"roles": [
"Transport"
],
"code": "",
"name": "Default Transport"
}
],
"filename": "il-76.png",
"cost": 0
},
"MiG-15bis": {
"name": "MiG-15bis",
"coalition": "red",
"label": "MiG-15 Fagot",
"era": "Early Cold War",
"shortLabel": "M15",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "300L Fuel Tanks",
"quantity": 2
}
],
"roles": [
"CAP"
],
"code": "2*300L",
"name": "Medium / Guns / Medium Range"
},
{
"fuel": 1,
"items": [
{
"name": "FAB-100M",
"quantity": 2
}
],
"roles": [
"Strike"
],
"code": "2*FAB-100M",
"name": "Medium / FAB-100M / Short Range"
},
{
"fuel": 1,
"items": [],
"roles": [
"CAP"
],
"code": "",
"name": "Light / Guns / Short Range"
}
],
"filename": "mig-15.png",
"cost": 30,
"liveryID": "Iraqi_Camo"
},
"MiG-21Bis": {
"name": "MiG-21Bis",
"coalition": "red",
"label": "MiG-21 Fishbed",
"era": "Mid Cold War",
"shortLabel": "21",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "R-3 Atoll",
"quantity": 2
},
{
"name": "R-60 Aphid",
"quantity": 2
},
{
"name": "130 gal tanks",
"quantity": 1
},
{
"name": "ASO-2 Countermeasures",
"quantity": 1
}
],
"roles": [
"CAP"
],
"code": "Patrol, short range",
"name": "Light / Fox-2 / Short range"
},
{
"fuel": 1,
"items": [
{
"name": "R-3 Atoll",
"quantity": 2
},
{
"name": "R-60 Aphid",
"quantity": 2
},
{
"name": "210 gal tanks",
"quantity": 1
},
{
"name": "ASO-2 Countermeasures",
"quantity": 1
}
],
"roles": [
"CAP"
],
"code": "Patrol, medium range",
"name": "Medium / Fox-2 / Medium range"
},
{
"fuel": 1,
"items": [
{
"name": "R-3R Atoll",
"quantity": 2
},
{
"name": "R-3S Atoll",
"quantity": 2
},
{
"name": "210 gal tanks",
"quantity": 1
},
{
"name": "ASO-2 Countermeasures",
"quantity": 1
}
],
"roles": [
"CAP"
],
"code": "Patrol, long range",
"name": "Medium / Fox-1, Fox-2 / Medium range"
},
{
"fuel": 1,
"items": [
{
"name": "GROM",
"quantity": 2
},
{
"name": "FAB-250",
"quantity": 2
},
{
"name": "210 gal tanks",
"quantity": 1
},
{
"name": "ASO-2 Countermeasures",
"quantity": 1
}
],
"roles": [
"Strike"
],
"code": "Few big targets, GROM + BOMBS",
"name": "Heavy / GROM, FAB250 / Medium range"
},
{
"fuel": 1,
"items": [],
"roles": [
"CAP"
],
"code": "",
"name": "Empty Loadout"
}
],
"filename": "mig-21.png",
"cost": 100,
"liveryID": "iran - standard"
},
"MiG-23MLD": {
"name": "MiG-23MLD",
"coalition": "red",
"label": "MiG-23 Flogger",
"era": "Mid Cold War",
"shortLabel": "23",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "Fuel-800",
"quantity": 1
},
{
"name": "R-60M",
"quantity": 4
},
{
"name": "R-24R",
"quantity": 2
}
],
"roles": [
"CAP"
],
"code": "R-24R*2,R-60M*4,Fuel-800",
"name": "Heavy / Fox 1 / Long Range"
},
{
"fuel": 1,
"items": [
{
"name": "Fuel-800",
"quantity": 1
},
{
"name": "FAB-500",
"quantity": 2
},
{
"name": "R-60M",
"quantity": 2
}
],
"roles": [
"Strike"
],
"code": "FAB-500*2,R-60M*2,Fuel-800",
"name": "Heavy / FAB-500 / Long Range"
},
{
"fuel": 1,
"items": [],
"roles": [
""
],
"code": "",
"name": "Empty Loadout"
}
],
"filename": "mig-23.png",
"cost": 200
},
"Mirage-F1EE": {
"name": "Mirage-F1EE",
"coalition": "red",
"label": "Mirage-F1EE",
"era": "Mid Cold War",
"shortLabel": "F1EE",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "AIM-9JULI",
"quantity": 2
},
{
"name": "R530EM",
"quantity": 2
},
{
"name": "1137L Fuel Tank",
"quantity": 1
}
],
"roles": [
"CAP"
],
"code": "2*AIM9-JULI, 2*R530EM, 1*Fuel Tank",
"name": "Medium / Fox 1 / Medium Range"
},
{
"fuel": 1,
"items": [
{
"name": "AIM-9JULI",
"quantity": 2
},
{
"name": "SAMP 400 LD",
"quantity": 8
}
],
"roles": [
"Strike"
],
"code": "2*AIM-9JULI, 8*SAMP 400 LD",
"name": "Heavy / SAMP400 / Short Range"
},
{
"fuel": 1,
"items": [],
"roles": [
""
],
"code": "",
"name": "Empty Loadout"
}
],
"filename": "mig-25.png",
"cost": 150,
"liveryID": "iriaf 3-6215 _ 1990-2010s desert (eq variant)"
},
"Mirage-F1CE": {
"name": "Mirage-F1CE",
"coalition": "red",
"label": "Mirage-F1CE",
"era": "Mid Cold War",
"shortLabel": "F1CE",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "AIM-9JULI",
"quantity": 2
},
{
"name": "R530IR",
"quantity": 2
},
{
"name": "1137L Fuel Tank",
"quantity": 1
}
],
"roles": [
"CAP"
],
"code": "2*AIM9-JULI, 2*R530IR, 1*Fuel Tank",
"name": "Medium / Fox 2 / Medium Range"
},
{
"fuel": 1,
"items": [
{
"name": "AIM-9JULI",
"quantity": 2
},
{
"name": "SAMP 400 LD",
"quantity": 8
}
],
"roles": [
"Strike"
],
"code": "2*AIM-9JULI, 8*SAMP 400 LD",
"name": "Heavy / SAMP400 / Short Range"
},
{
"fuel": 1,
"items": [],
"roles": [
""
],
"code": "",
"name": "Empty Loadout"
}
],
"filename": "mig-25.png",
"cost": 150,
"liveryID": "iraq air force (fictional eq version)"
},
"Su-24M": {
"name": "Su-24M",
"coalition": "red",
"label": "Su-24M Fencer",
"era": "Mid Cold War",
"shortLabel": "24",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "R-60M",
"quantity": 2
},
{
"name": "FAB-1500",
"quantity": 2
}
],
"roles": [
"Strike"
],
"code": "FAB-1500*2,R-60M*2",
"name": "Heavy / FAB-500 / Short Range"
},
{
"fuel": 1,
"items": [],
"roles": [
""
],
"code": "",
"name": "Empty Loadout"
}
],
"filename": "su-24.png",
"cost": 250,
"liveryID": "iran air force"
}
}

View File

@ -1,153 +0,0 @@
{
"BMP-1": {
"name": "BMP-1",
"coalition": "red",
"era": "Mid Cold War",
"label": "BMP-1",
"shortLabel": "BMP-1",
"filename": "",
"type": "IFV",
"cost": 10,
"liveryID": "desert"
},
"Hawk SAM Battery": {
"name": "Hawk SAM Battery",
"coalition": "blue",
"era": "Early Cold War",
"label": "Hawk SAM Battery",
"shortLabel": "Hawk SAM Battery",
"range": "Medium",
"filename": "",
"type": "SAM Site",
"cost": 500
},
"Infantry AK": {
"name": "Infantry AK",
"era": "Mid Cold War",
"label": "Infantry AK",
"shortLabel": "Infantry AK",
"filename": "",
"type": "Infantry",
"cost": 1
},
"M-113": {
"name": "M-113",
"coalition": "blue",
"era": "Early Cold War",
"label": "M-113",
"shortLabel": "M-113",
"filename": "",
"type": "APC",
"cost": 5
},
"M-60": {
"name": "M-60",
"coalition": "blue",
"era": "Early Cold War",
"label": "M-60",
"shortLabel": "M-60",
"filename": "",
"type": "Tank",
"cost": 10
},
"SA-2 SAM Battery": {
"name": "SA-2 SAM Battery",
"coalition": "red",
"era": "Early Cold War",
"label": "SA-2 SAM Battery",
"shortLabel": "SA-2 SAM Battery",
"range": "Long",
"filename": "",
"type": "SAM Site",
"cost": 500
},
"T-55": {
"name": "T-55",
"coalition": "red",
"era": "Early Cold War",
"label": "T-55",
"shortLabel": "T-55",
"filename": "",
"type": "Tank",
"cost": 10,
"liveryID": "desert"
},
"T-72B": {
"name": "T-72B",
"coalition": "red",
"era": "Mid Cold War",
"label": "T-72B",
"shortLabel": "T-72B",
"filename": "",
"type": "Tank",
"cost": 20,
"liveryID": "desert"
},
"ZSU-23-4 Shilka": {
"name": "ZSU-23-4 Shilka",
"era": "Late Cold War",
"label": "ZSU-23-4 Shilka",
"shortLabel": "ZSU-23-4 Shilka",
"filename": "",
"type": "AAA",
"cost": 100
},
"Ural-375 ZU-23": {
"name": "Ural-375 ZU-23",
"era": "Early Cold War",
"label": "Ural-375 ZU-23",
"shortLabel": "Ural-375 ZU-23",
"filename": "",
"type": "AAA",
"cost": 50
},
"SAU Gvozdika": {
"name": "SAU Gvozdika",
"coalition": "red",
"era": "Early Cold War",
"label": "SAU Gvozdika",
"shortLabel": "SAU Gvozdika",
"filename": "",
"type": "Gun Artillery",
"cost": 10,
"liveryID": "desert"
},
"Grad-URAL": {
"name": "Grad-URAL",
"coalition": "blue",
"era": "Early Cold War",
"label": "BM-21",
"shortLabel": "BM-21",
"filename": "",
"type": "Rocket Artillery",
"cost": 10
},
"Chieftain_mk3": {
"name": "Chieftain_mk3",
"coalition": "blue",
"era": "Early Cold War",
"label": "Chieftain Mk3",
"shortLabel": "Chieftain Mk3",
"filename": "",
"type": "Tank",
"cost": 20
},
"Scud_B": {
"name": "Scud_B",
"era": "Early Cold War",
"label": "SCUD",
"shortLabel": "SCUD",
"filename": "",
"type": "Rocket Artillery",
"cost": 10
},
"tt_ZU-23": {
"name": "tt_ZU-23",
"era": "Early Cold War",
"label": "Technical ZSU-23",
"shortLabel": "Technical ZSU-23",
"filename": "",
"type": "AAA",
"cost": 25
}
}

View File

@ -1,141 +0,0 @@
{
"Mi-24P": {
"name": "Mi-24P",
"coalition": "red",
"era": "Mid Cold War",
"label": "Mi-24P Hind",
"shortLabel": "Mi24",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "S-8KOM",
"quantity": 40
},
{
"name": "9M114 ATGM",
"quantity": 8
}
],
"roles": [
"CAS"
],
"code": "2xB8V20 (S-8KOM)+8xATGM 9M114",
"name": "Gun / ATGM / Rockets"
},
{
"fuel": 1,
"items": [
{
"name": "S-24B",
"quantity": 4
},
{
"name": "9M114 ATGM",
"quantity": 4
}
],
"roles": [
"Strike"
],
"code": "4xS-24B+4xATGM 9M114",
"name": "Gun / ATGM / Rockets"
},
{
"fuel": 1,
"items": [
{
"name": "GUV-1 Grenade Launcher",
"quantity": 4
},
{
"name": "9M114 ATGM",
"quantity": 4
}
],
"roles": [
"CAS"
],
"code": "4xGUV-1 AP30+4xATGM 9M114",
"name": "Gun / ATGM / Grenade Launcher"
},
{
"fuel": 1,
"items": [],
"roles": [
""
],
"code": "",
"name": "Empty Loadout"
}
],
"filename": "mi-24.png",
"cost": 150,
"liveryID": "IQAF"
},
"Mi-8MT": {
"name": "Mi-8MT",
"coalition": "blue",
"era": "Mid Cold War",
"label": "Mi-8MT Hip",
"shortLabel": "Mi8",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "UPK",
"quantity": 2
},
{
"name": "B8",
"quantity": 2
}
],
"roles": [
"CAS"
],
"code": "2 x UPK +2 x B8",
"name": "Rockets / Gunpods"
},
{
"fuel": 1,
"items": [],
"roles": [
"Transport"
],
"code": "",
"name": "Empty Loadout"
}
],
"filename": "mi-8.png",
"cost": 100,
"liveryID": "IR Iranian Special Police Forces"
},
"SA342M": {
"name": "SA342M",
"coalition": "blue",
"era": "Mid Cold War",
"label": "SA342M Gazelle",
"shortLabel": "342",
"loadouts": [
{
"fuel": 1,
"items": [
{
"name": "HOT3",
"quantity": 4
}
],
"roles": [
"CAS"
],
"code": "4x HOT3, IR Deflector, Sand Filter",
"name": "ATGM"
}
],
"filename": "sa-342.png",
"cost": 80
}
}

File diff suppressed because it is too large Load Diff

View File

@ -13,11 +13,30 @@
top: 10px;
z-index: 99999;
column-gap: 10px;
margin-right: 320px;
height: fit-content;
}
@media (max-width: 1820px) {
#toolbar-container {
flex-direction: column;
align-items: start;
row-gap: 10px;
}
}
#primary-toolbar {
align-items: center;
display: flex;
height: fit-content;
min-width: 650px;
}
@media (max-width: 1820px) {
#primary-toolbar {
row-gap: 10px;
flex-wrap: wrap;
}
}
#command-mode-toolbar {
@ -83,6 +102,18 @@
z-index: 9999;
}
@media (max-width: 1820px) {
#unit-control-panel {
top: 150px;
}
}
@media (max-width: 1350px) {
#unit-control-panel {
top: 190px;
}
}
#unit-info-panel {
bottom: 20px;
font-size: 12px;
@ -91,6 +122,15 @@
width: fit-content;
z-index: 9999;
padding: 24px 30px;
display: flex;
flex-direction: row;
justify-content: space-evenly;
}
@media (max-width: 1525px) {
#unit-info-panel {
flex-direction: column;
}
}
#info-popup {
@ -100,7 +140,7 @@
top: 100px;
left: 50%;
translate: -50% 0%;
z-index: 9999;
z-index: 9999999999;
display: flex;
align-items: center;
}

View File

@ -0,0 +1,47 @@
@-webkit-keyframes leaflet-gestures-fadein {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@keyframes leaflet-gestures-fadein {
0% {
opacity: 0; }
100% {
opacity: 1; } }
.leaflet-container:after {
-webkit-animation: leaflet-gestures-fadein 0.8s backwards;
animation: leaflet-gestures-fadein 0.8s backwards;
color: #fff;
font-family: "Roboto", Arial, sans-serif;
font-size: 22px;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
padding: 15px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 461;
pointer-events: none; }
.leaflet-gesture-handling-touch-warning:after,
.leaflet-gesture-handling-scroll-warning:after {
-webkit-animation: leaflet-gestures-fadein 0.8s forwards;
animation: leaflet-gestures-fadein 0.8s forwards; }
.leaflet-gesture-handling-touch-warning:after {
content: attr(data-gesture-handling-touch-content); }
.leaflet-gesture-handling-scroll-warning:after {
content: attr(data-gesture-handling-scroll-content); }

View File

@ -369,6 +369,10 @@
content: url("/resources/theme/images/icons/crosshairs-solid.svg");
}
#simulate-fire-fight::before {
content: url("/resources/theme/images/icons/crosshairs-solid.svg");
}
#follow::before {
content: url("/resources/theme/images/icons/follow.svg");
}

View File

@ -1,8 +1,28 @@
.ol-popup {
display: flex;
flex-direction: column;
row-gap: 5px;
}
.ol-popup > div {
background-color: var(--background-steel);
border-radius: var(--border-radius-md);
box-shadow: 0px 2px 5px #000A;
color: white;
font-size: 12px;
height: fit-content;
width: fit-content;
padding-top: 5px;
padding-bottom: 5px;
padding-left: 15px;
padding-right: 15px;
}
.ol-popup-stack {
margin-bottom: -20px;
z-index: -1;
}
.visible {
opacity: 1;
}

View File

@ -9,11 +9,27 @@
width: 100%;
}
#server-status-panel .ol-data-grid:first-of-type {
border-right: 1px solid gray;
padding-right: 10px;
@media (min-width: 1525px) {
#server-status-panel .ol-data-grid:first-of-type {
border-right: 1px solid gray;
padding-right: 10px;
}
}
@media (max-width: 1525px) {
#server-status-panel {
flex-direction: column;
row-gap: 10px;
width: 180px;
}
#server-status-panel .ol-data-grid:first-of-type {
border-bottom: 1px solid gray;
padding-bottom: 10px;
}
}
#server-status-panel dd {
font-weight: bold;
}

View File

@ -91,8 +91,8 @@ body.feature-forceShowUnitControlPanel #unit-control-panel {
justify-content: space-between;
}
#advanced-settings-dialog h4 {
white-space: nowrap;
#advanced-settings-dialog>.ol-dialog-content>div input[type="number"] {
width: 60px;
}
#advanced-settings-dialog hr {
@ -102,6 +102,12 @@ body.feature-forceShowUnitControlPanel #unit-control-panel {
#advanced-settings-dialog .ol-text-input input {
height: 40px;
width: fit-content;
}
#advanced-settings-dialog h4 {
width: fit-content;
text-wrap: nowrap;
}
#general-settings-grid {

View File

@ -4,6 +4,45 @@
bottom: 0px;
}
@media (min-width: 1525px) {
#unit-info-panel>.panel-section {
border-right: 1px solid #555;
padding: 0 30px;
}
#unit-info-panel>.panel-section:first-child {
padding-left: 0px;
}
#unit-info-panel>.panel-section:last-child {
padding-right: 0px;
}
#unit-info-panel>.panel-section:last-of-type {
border-right-width: 0;
}
}
@media (max-width: 1525px) {
#unit-info-panel>.panel-section {
border-bottom: 1px solid #555;
padding: 30px 0px;
}
#unit-info-panel>.panel-section:first-child {
padding-top: 0px;
}
#unit-info-panel>.panel-section:last-child {
padding-bottom: 0px;
}
#unit-info-panel>.panel-section:last-of-type {
border-bottom-width: 0;
}
}
#general {
display: flex;
flex-direction: column;
@ -58,10 +97,14 @@
}
#loadout-items {
margin-right: 20px;
align-self: center;
display: flex;
flex-flow: column nowrap;
flex-flow: column;
row-gap: 8px;
column-gap: 8px;
max-height: 90px;
flex-wrap: wrap;
}
#loadout-items>* {

View File

@ -13,6 +13,7 @@ body {
display: grid;
margin: 0;
padding: 0;
position: fixed;
}
html,
@ -117,7 +118,6 @@ form>div {
.ol-panel {
background-color: var(--background-steel);
border-radius: var(--border-radius-md);
;
box-shadow: 0px 2px 5px #000A;
color: white;
font-size: 12px;
@ -306,29 +306,6 @@ form>div {
max-width: 16px;
}
.ol-panel-board {
display: flex;
flex-direction: row;
justify-content: space-evenly;
}
.ol-panel-board>.panel-section {
border-right: 1px solid #555;
padding: 0 30px;
}
.ol-panel-board>.panel-section:first-child {
padding-left: 0px;
}
.ol-panel-board>.panel-section:last-child {
padding-right: 0px;
}
.ol-panel-board>.panel-section:last-of-type {
border-right-width: 0;
}
h1,
h2,
h3,
@ -703,15 +680,21 @@ nav.ol-panel> :last-child {
/****************************************************************************************/
#splash-screen {
background-image: url("/resources/theme/images/splash/1.png");
background-position: 100% 50%;
background-size: 60%;
border-radius: var(--border-radius-md);
overflow: hidden;
width: 1200px;
width: 70%;
max-width: 1200px;
z-index: 99999;
}
@media (min-width: 1700px) {
#splash-screen {
background-image: url("/resources/theme/images/splash/1.png");
background-position: 100% 50%;
background-size: contain;
}
}
#splash-content {
background-color: var(--background-steel);
display: flex;
@ -722,6 +705,12 @@ nav.ol-panel> :last-child {
width: 50%;
}
@media (max-width: 1700px) {
#splash-content {
width: auto;
}
}
#splash-content::after {
background-color: var(--background-steel);
content: "";
@ -761,6 +750,10 @@ nav.ol-panel> :last-child {
font-size: 11px;
}
#splash-content #legal-stuff {
width: 100%;
}
#splash-content #legal-stuff h5 {
text-transform: uppercase;
}
@ -771,12 +764,14 @@ nav.ol-panel> :last-child {
width: 120%;
}
#splash-content.ol-dialog-content {
margin: 0px;
@media (max-width: 1700px) {
#splash-content #legal-stuff p {
width: 100%;
}
}
.feature-splashScreen #splash-screen {
display: flex;
#splash-content.ol-dialog-content {
margin: 0px;
}
#gray-out {
@ -795,6 +790,8 @@ nav.ol-panel> :last-child {
display: flex;
flex-direction: row;
margin: 10px 0px;
flex-wrap: wrap;
width: 100%;
}
#authentication-form>div {

View File

@ -0,0 +1,58 @@
const express = require('express');
const router = express.Router();
const fs = require("fs");
const path = require("path");
router.get('/:type/:name', function (req, res) {
console.log(req.params.database)
});
router.put('/save/:type/:name', function (req, res) {
var dir = path.join("./public/databases", req.params.type, "old");
if (!fs.existsSync(dir)){
fs.mkdirSync(dir);
}
var filepath = path.join("./public/databases", req.params.type, req.params.name + ".json");
if (fs.existsSync(filepath)) {
var newFilepath = path.join("./public/databases/", req.params.type, "old", req.params.name + ".json");
fs.copyFileSync(filepath, newFilepath);
if (fs.existsSync(newFilepath)) {
try {
var json = JSON.stringify(req.body.blueprints, null, "\t" );
fs.writeFileSync(filepath, json, 'utf8');
res.send("OK");
} catch {
res.status(422).send("Error");
}
} else {
res.status(422).send("Error");
}
} else {
res.status(404).send('Not found');
}
});
router.put('/reset/:type/:name', function (req, res) {
var filepath = path.join("./public/databases", req.params.type, "default", req.params.name + ".json");
if (fs.existsSync(filepath)) {
var newFilepath = path.join("./public/databases", req.params.type, req.params.name + ".json");
fs.copyFileSync(filepath, newFilepath);
res.send("OK");
} else {
res.status(404).send('Not found');
}
});
router.put('/restore/:type/:name', function (req, res) {
var filepath = path.join("./public/databases", req.params.type, "old", req.params.name + ".json");
if (fs.existsSync(filepath)) {
var newFilepath = path.join("./public/databases", req.params.type, req.params.name + ".json");
fs.copyFileSync(filepath, newFilepath);
res.send("OK");
} else {
res.status(404).send('Not found');
}
});
module.exports = router;

View File

@ -20,7 +20,7 @@ export const IRST = 8;
export const RWR = 16;
export const DLINK = 32;
export const states: string[] = ["none", "idle", "reach-destination", "attack", "follow", "land", "refuel", "AWACS", "tanker", "bomb-point", "carpet-bomb", "bomb-building", "fire-at-area"];
export const states: string[] = ["none", "idle", "reach-destination", "attack", "follow", "land", "refuel", "AWACS", "tanker", "bomb-point", "carpet-bomb", "bomb-building", "fire-at-area", "simulate-fire-fight"];
export const ROEs: string[] = ["free", "designated", "", "return", "hold"];
export const reactionsToThreat: string[] = ["none", "manoeuvre", "passive", "evade"];
export const emissionsCountermeasures: string[] = ["silent", "attack", "defend", "free"];

View File

@ -1,4 +1,4 @@
import { OlympusApp } from "./app";
import { OlympusApp } from "./olympusapp";
var app: OlympusApp;

View File

@ -1,5 +1,5 @@
import { LatLng } from "leaflet";
import { OlympusApp } from "./app";
import { OlympusApp } from "./olympusapp";
import { Airbase } from "./mission/airbase";
export interface OlympusPlugin {
@ -204,6 +204,8 @@ export interface UnitBlueprint {
filename?: string;
liveries?: { [key: string]: { name: string, countries: string[] } };
cost?: number;
barrelHeight?: number;
muzzleVelocity?: number;
}
export interface UnitSpawnOptions {

View File

@ -19,8 +19,19 @@ import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextme
import { DrawingCursor } from "./coalitionarea/drawingcursor";
import { AirbaseSpawnContextMenu } from "../contextmenus/airbasespawnmenu";
import { Popup } from "../popups/popup";
import { GestureHandling } from "leaflet-gesture-handling";
import { TouchBoxSelect } from "./touchboxselect";
L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect);
var hasTouchScreen = false;
if ("maxTouchPoints" in navigator)
hasTouchScreen = navigator.maxTouchPoints > 0;
if (hasTouchScreen)
L.Map.addInitHook('addHandler', 'boxSelect', TouchBoxSelect);
else
L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect);
L.Map.addInitHook("addHandler", "gestureHandling", GestureHandling);
// TODO would be nice to convert to ts - yes
require("../../public/javascripts/leaflet.nauticscale.js")
@ -77,8 +88,22 @@ export class Map extends L.Map {
*/
constructor(ID: string){
/* Init the leaflet map */
//@ts-ignore Needed because the boxSelect option is non-standard
super(ID, { zoomSnap: 0, zoomDelta: 0.25, preferCanvas: true, doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true, zoomAnimation: true, maxBoundsViscosity: 1.0, minZoom: 7, keyboard: true, keyboardPanDelta: 0 });
super(ID, {
zoomSnap: 0,
zoomDelta: 0.25,
preferCanvas: true,
doubleClickZoom: false,
zoomControl: false,
boxZoom: false,
//@ts-ignore Needed because the boxSelect option is non-standard
boxSelect: true,
zoomAnimation: true,
maxBoundsViscosity: 1.0,
minZoom: 7,
keyboard: true,
keyboardPanDelta: 0,
gestureHandling: hasTouchScreen
});
this.setView([37.23, -115.8], 10);
this.#ID = ID;
@ -486,7 +511,7 @@ export class Map extends L.Map {
}
#onDoubleClick(e: any) {
this.deselectAllCoalitionAreas();
}
#onContextMenu(e: any) {
@ -580,8 +605,10 @@ export class Map extends L.Map {
}
}
else if (selectedUnitTypes.length === 1 && ["GroundUnit", "NavyUnit"].includes(selectedUnitTypes[0])) {
if (selectedUnits.every((unit: Unit) => { return ["Gun Artillery", "Rocket Artillery", "Infantry", "IFV", "Tank", "Cruiser", "Destroyer", "Frigate"].includes(unit.getType()) }))
if (selectedUnits.every((unit: Unit) => { return ["Gun Artillery", "Rocket Artillery", "Infantry", "IFV", "Tank", "Cruiser", "Destroyer", "Frigate"].includes(unit.getType()) })) {
options["fire-at-area"] = { text: "Fire at area", tooltip: "Fire at a large area" };
options["simulate-fire-fight"] = { text: "Simulate fire fight", tooltip: "Simulate a fire fight by shooting randomly in a certain large area" };
}
else
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Selected units can not perform point actions.`);
}
@ -605,6 +632,10 @@ export class Map extends L.Map {
getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE);
getApp().getUnitsManager().selectedUnitsFireAtArea(this.getMouseCoordinates());
}
else if (option === "simulate-fire-fight") {
getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE);
getApp().getUnitsManager().selectedUnitsSimulateFireFight(this.getMouseCoordinates());
}
});
}
}, 150);

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

@ -20,6 +20,10 @@ import { SVGInjector } from "@tanem/svg-injector";
import { ServerManager } from "./server/servermanager";
import { BLUE_COMMANDER, GAME_MASTER, RED_COMMANDER } 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 { ConfigurationOptions } from "./interfaces";
export class OlympusApp {
@ -40,10 +44,6 @@ export class OlympusApp {
#toolbarsManager: Manager | null = null;
#shortcutManager: ShortcutManager | null = null;
/* UI Toolbars */
#primaryToolbar: PrimaryToolbar | null = null;
#commandModeToolbar: CommandModeToolbar | null = null;
constructor() {
}
@ -115,6 +115,38 @@ export class OlympusApp {
}
}
/**
*
* @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
@ -155,7 +187,7 @@ export class OlympusApp {
// Toolbars
this.getToolbarsManager().add("primaryToolbar", new PrimaryToolbar("primary-toolbar"))
.add("commandModeToolbar", new PrimaryToolbar("command-mode-toolbar"));
.add("commandModeToolbar", new CommandModeToolbar("command-mode-toolbar"));
this.#pluginsManager = new PluginsManager();

View File

@ -398,4 +398,19 @@ export function getCheckboxOptions(dropdown: Dropdown) {
values[key] = value;
}
return values;
}
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

@ -1,31 +1,58 @@
import { Panel } from "../panels/panel";
export class Popup extends Panel {
export class PopupMessage {
#element: HTMLDivElement;
#fadeTime: number = 2000; // Milliseconds
constructor( elementId:string ) {
super( elementId );
constructor(text: string, fateTime: number) {
this.#element = document.createElement("div");
this.#fadeTime = fateTime;
this.#element.innerText = text;
this.#element.classList.remove("invisible");
this.#element.classList.add("visible");
window.setTimeout(() => {
this.#element.classList.remove("visible");
this.#element.classList.add("invisible");
window.setTimeout(() => {
this.#element.dispatchEvent(new Event("removed"));
this.#element.remove();
}, 2000);
}, this.#fadeTime);
}
getElement() {
return this.#element;
}
}
export class Popup extends Panel {
#fadeTime: number = 2000; // Milliseconds
#hideTimer: number | undefined = undefined;
#visibilityTimer: number | undefined = undefined;
#messages: PopupMessage[] = [];
#stackAfter: number;
constructor(ID: string, stackAfter: number = 3) {
super(ID);
this.show();
this.#stackAfter = stackAfter;
}
setFadeTime(fadeTime: number) {
this.#fadeTime = fadeTime;
}
setText(text: string) {
(<HTMLElement> this.getElement().querySelector("div")).innerText = text;
this.show();
this.getElement().classList.remove("invisible");
this.getElement().classList.add("visible");
clearTimeout(this.#visibilityTimer);
clearTimeout(this.#hideTimer);
this.#visibilityTimer = window.setTimeout(() => {
this.getElement().classList.remove("visible");
this.getElement().classList.add("invisible");
this.#hideTimer = window.setTimeout(() => this.hide(), 2000);
}, this.#fadeTime);
setText(text: string) {
var message = new PopupMessage(text, this.#fadeTime);
message.getElement().addEventListener("removed", () => {
var index = this.#messages.indexOf(message);
if (index !== -1)
this.#messages.splice(index, 1);
})
this.getElement().appendChild(message.getElement());
this.#messages.push(message);
if (this.#messages.length > this.#stackAfter) {
this.#messages[this.#messages.length - this.#stackAfter - 1].getElement().classList.add("ol-popup-stack");
}
}
}

View File

@ -86,7 +86,7 @@ export class ServerManager {
xmlHttp.send(null);
}
POST(request: object, callback: CallableFunction) {
PUT(request: object, callback: CallableFunction) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("PUT", this.#demoEnabled ? this.#DEMO_ADDRESS : this.#REST_ADDRESS);
xmlHttp.setRequestHeader("Content-Type", "application/json");
@ -148,49 +148,49 @@ export class ServerManager {
addDestination(ID: number, path: any, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "path": path }
var data = { "setPath": command }
this.POST(data, callback);
this.PUT(data, callback);
}
spawnSmoke(color: string, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { "color": color, "location": latlng };
var data = { "smoke": command }
this.POST(data, callback);
this.PUT(data, callback);
}
spawnExplosion(intensity: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { "intensity": intensity, "location": latlng };
var data = { "explosion": command }
this.POST(data, callback);
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.POST(data, callback);
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.POST(data, callback);
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.POST(data, callback);
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.POST(data, callback);
this.PUT(data, callback);
}
attackUnit(ID: number, targetID: number, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "targetID": targetID };
var data = { "attackUnit": command }
this.POST(data, callback);
this.PUT(data, callback);
}
followUnit(ID: number, targetID: number, offset: { "x": number, "y": number, "z": number }, callback: CallableFunction = () => {}) {
@ -200,127 +200,133 @@ export class ServerManager {
var command = { "ID": ID, "targetID": targetID, "offsetX": offset["x"], "offsetY": offset["y"], "offsetZ": offset["z"] };
var data = { "followUnit": command }
this.POST(data, callback);
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.POST(data, callback);
this.PUT(data, callback);
}
deleteUnit(ID: number, explosion: boolean, immediate: boolean, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "explosion": explosion, "immediate": immediate };
var data = { "deleteUnit": command }
this.POST(data, callback);
this.PUT(data, callback);
}
landAt(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "location": latlng };
var data = { "landAt": command }
this.POST(data, callback);
this.PUT(data, callback);
}
changeSpeed(ID: number, speedChange: string, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "change": speedChange }
var data = { "changeSpeed": command }
this.POST(data, callback);
this.PUT(data, callback);
}
setSpeed(ID: number, speed: number, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "speed": speed }
var data = { "setSpeed": command }
this.POST(data, callback);
this.PUT(data, callback);
}
setSpeedType(ID: number, speedType: string, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "speedType": speedType }
var data = { "setSpeedType": command }
this.POST(data, callback);
this.PUT(data, callback);
}
changeAltitude(ID: number, altitudeChange: string, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "change": altitudeChange }
var data = { "changeAltitude": command }
this.POST(data, callback);
this.PUT(data, callback);
}
setAltitudeType(ID: number, altitudeType: string, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "altitudeType": altitudeType }
var data = { "setAltitudeType": command }
this.POST(data, callback);
this.PUT(data, callback);
}
setAltitude(ID: number, altitude: number, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "altitude": altitude }
var data = { "setAltitude": command }
this.POST(data, callback);
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.POST(data, callback);
this.PUT(data, callback);
}
setROE(ID: number, ROE: string, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "ROE": ROEs.indexOf(ROE) }
var data = { "setROE": command }
this.POST(data, callback);
this.PUT(data, callback);
}
setReactionToThreat(ID: number, reactionToThreat: string, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "reactionToThreat": reactionsToThreat.indexOf(reactionToThreat) }
var data = { "setReactionToThreat": command }
this.POST(data, callback);
this.PUT(data, callback);
}
setEmissionsCountermeasures(ID: number, emissionCountermeasure: string, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "emissionsCountermeasures": emissionsCountermeasures.indexOf(emissionCountermeasure) }
var data = { "setEmissionsCountermeasures": command }
this.POST(data, callback);
this.PUT(data, callback);
}
setOnOff(ID: number, onOff: boolean, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "onOff": onOff }
var data = { "setOnOff": command }
this.POST(data, callback);
this.PUT(data, callback);
}
setFollowRoads(ID: number, followRoads: boolean, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "followRoads": followRoads }
var data = { "setFollowRoads": command }
this.POST(data, callback);
this.PUT(data, callback);
}
refuel(ID: number, callback: CallableFunction = () => {}) {
var command = { "ID": ID };
var data = { "refuel": command }
this.POST(data, callback);
this.PUT(data, callback);
}
bombPoint(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "location": latlng }
var data = { "bombPoint": command }
this.POST(data, callback);
this.PUT(data, callback);
}
carpetBomb(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "location": latlng }
var data = { "carpetBomb": command }
this.POST(data, callback);
this.PUT(data, callback);
}
bombBuilding(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "location": latlng }
var data = { "bombBuilding": command }
this.POST(data, callback);
this.PUT(data, callback);
}
fireAtArea(ID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { "ID": ID, "location": latlng }
var data = { "fireAtArea": command }
this.POST(data, callback);
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);
}
setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings, callback: CallableFunction = () => {}) {
@ -334,7 +340,7 @@ export class ServerManager {
};
var data = { "setAdvancedOptions": command };
this.POST(data, callback);
this.PUT(data, callback);
}
setCommandModeOptions(restrictSpawns: boolean, restrictToCoalition: boolean, spawnPoints: {blue: number, red: number}, eras: string[], setupTime: number, callback: CallableFunction = () => {}) {
@ -347,7 +353,7 @@ export class ServerManager {
};
var data = { "setCommandModeOptions": command };
this.POST(data, callback);
this.PUT(data, callback);
}
startUpdate() {

View File

@ -2,12 +2,16 @@ import { Dropdown } from "../controls/dropdown";
import { Toolbar } from "./toolbar";
export class PrimaryToolbar extends Toolbar {
#mainDropdown: Dropdown;
constructor(ID: string) {
super(ID);
// TODO move here all code about primary toolbar
/* The content of the dropdown is entirely defined in the .ejs file */
new Dropdown("app-icon", () => { });
this.#mainDropdown = new Dropdown("app-icon", () => { });
}
getMainDropdown() {
return this.#mainDropdown;
}
}

View File

@ -5,18 +5,26 @@ import { UnitBlueprint } from "../../interfaces";
export class UnitDatabase {
blueprints: { [key: string]: UnitBlueprint } = {};
#url: string;
constructor(url: string = "") {
if (url !== "") {
this.#url = url;
this.load(() => {});
}
load(callback: CallableFunction) {
if (this.#url !== "") {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
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 ${url}`)
console.error(`Error retrieving database from ${this.#url}`)
}
};
xhr.send();

View File

@ -723,6 +723,10 @@ export class Unit extends CustomMarker {
getApp().getServerManager().fireAtArea(this.ID, latlng);
}
simulateFireFight(latlng: LatLng, groundElevation: number | null) {
getApp().getServerManager().simulateFireFight(this.ID, latlng, groundElevation?? 0);
}
/***********************************************/
onAdd(map: Map): this {
super.onAdd(map);

View File

@ -1,7 +1,7 @@
import { LatLng, LatLngBounds } from "leaflet";
import { getApp } from "..";
import { Unit } from "./unit";
import { bearingAndDistanceToLatLng, deg2rad, getUnitDatabaseByCategory, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots, polyContains, polygonArea, randomPointInPoly, randomUnitBlueprint } from "../other/utils";
import { bearingAndDistanceToLatLng, deg2rad, getGroundElevation, getUnitDatabaseByCategory, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots, polyContains, polygonArea, randomPointInPoly, randomUnitBlueprint } from "../other/utils";
import { CoalitionArea } from "../map/coalitionarea/coalitionarea";
import { groundUnitDatabase } from "./databases/groundunitdatabase";
import { DataIndexes, GAME_MASTER, IADSDensities, IDLE, MOVE_UNIT } from "../constants/constants";
@ -591,7 +591,7 @@ export class UnitsManager {
for (let idx in selectedUnits) {
selectedUnits[idx].carpetBomb(latlng);
}
this.#showActionMessage(selectedUnits, `unit bombing point`);
this.#showActionMessage(selectedUnits, `unit carpet bombing point`);
}
/** Instruct the selected units to fire at specific coordinates
@ -603,7 +603,27 @@ export class UnitsManager {
for (let idx in selectedUnits) {
selectedUnits[idx].fireAtArea(latlng);
}
this.#showActionMessage(selectedUnits, `unit bombing point`);
this.#showActionMessage(selectedUnits, `unit firing at area`);
}
/** Instruct the selected units to simulate a fire fight at specific coordinates
*
* @param latlng Location to fire at
*/
selectedUnitsSimulateFireFight(latlng: LatLng) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true });
getGroundElevation(latlng, (response: string) => {
var groundElevation: number | null = null;
try {
groundElevation = parseFloat(response);
} catch {
console.log("Simulate fire fight: could not retrieve ground elevation")
}
for (let idx in selectedUnits) {
selectedUnits[idx].simulateFireFight(latlng, groundElevation);
}
});
this.#showActionMessage(selectedUnits, `unit simulating fire fight`);
}
/*********************** Control operations on selected units ************************/

View File

@ -48,9 +48,9 @@
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
"emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
//"emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
"outFile": "./@types/olympus/index.d.ts", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
//"outFile": "./@types/olympus/index.ts", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */

View File

@ -4,6 +4,7 @@
<title>Olympus client</title>
<link rel="stylesheet" type="text/css" href="stylesheets/olympus.css" />
<link rel="stylesheet" type="text/css" href="stylesheets/leaflet/leaflet.css">
<link rel="stylesheet" type="text/css" href="stylesheets/leaflet/leaflet-gesture-handling.css">
<link rel="stylesheet" type="text/css" href="/resources/theme/theme.css" /> <!-- Theme specifc css, autorouted to point to active theme -->

View File

@ -1,5 +1,3 @@
<div id="info-popup" class="ol-panel ol-popup hide" oncontextmenu="return false;">
<div>
<!-- Here the content of the popup will be shown -->
</div>
<div id="info-popup" class="ol-popup hide" oncontextmenu="return false;">
</div>

View File

@ -1,29 +1,25 @@
<div id="unit-info-panel" class="ol-panel" oncontextmenu="return false;">
<div class="ol-panel-board">
<div id="general" class="panel-section">
<h3 id="unit-name"></h3>
<div class="ol-group">
<div id="unit-label"></div>
<div id="unit-control"></div>
</div>
<div id="current-task" class="pill highlight-coalition" data-coalition="blue" data-current-task=""></div>
<div id="general" class="panel-section">
<h3 id="unit-name"></h3>
<div class="ol-group">
<div id="unit-label"></div>
<div id="unit-control"></div>
</div>
<div id="current-task" class="pill highlight-coalition" data-coalition="blue" data-current-task=""></div>
</div>
<div id="loadout-container" class="panel-section">
<div id="loadout">
<img id="loadout-silhouette"/>
<div id="loadout-items">
</div>
<div id="loadout-container" class="panel-section">
<div id="loadout">
<img id="loadout-silhouette"/>
<div id="loadout-items">
</div>
<div id="fuel-percentage" data-percentage=""></div>
<div id="fuel-display">
<div id="fuel-bar" class="highlight-coalition" data-coalition="blue" style="width:30%;"></div>
</div>
</div>
<div id="fuel-percentage" data-percentage=""></div>
<div id="fuel-display">
<div id="fuel-bar" class="highlight-coalition" data-coalition="blue" style="width:30%;"></div>
</div>
</div>

3
package-lock.json generated Normal file
View File

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}

View File

@ -1,6 +1,6 @@
local version = "v0.4.4-alpha"
local debug = false -- True enables debug printing using DCS messages
local debug = true -- True enables debug printing using DCS messages
-- .dll related variables
Olympus.OlympusDLL = nil
@ -229,13 +229,24 @@ function Olympus.buildTask(groupName, options)
-- Fire at a specific point
elseif options['id'] == 'FireAtPoint' and options['lat'] and options['lng'] and options['radius'] then
local point = coord.LLtoLO(options['lat'], options['lng'], 0)
task = {
id = 'FireAtPoint',
params = {
point = {x = point.x, y = point.z},
radius = options['radius']
}
}
if options['alt'] then
task = {
id = 'FireAtPoint',
params = {
point = {x = point.x, y = point.z},
radius = options['radius'],
altitude = options['alt']
}
}
else
task = {
id = 'FireAtPoint',
params = {
point = {x = point.x, y = point.z},
radius = options['radius']
}
}
end
end
end
return task

View File

@ -0,0 +1,21 @@
import sys
import json
import inspect
import difflib
from slpp import slpp as lua
SEARCH_FOLDER = "D:\\Eagle Dynamics\\DCS World OpenBeta"
sys.path.append("..\\..\\..\\dcs-master\\dcs-master")
from dcs.vehicles import *
with open("gundata.h", "w") as f:
for unit in vehicle_map.values():
if unit in Artillery.__dict__.values() or unit in Armor.__dict__.values() or unit in Infantry.__dict__.values():
f.write('{"' + unit.id + '", {0.9, 860}}, \n')
# Done!
print("Done!")

109
scripts/python/gundata.h Normal file
View File

@ -0,0 +1,109 @@
{"2B11 mortar", {0.9, 860}},
{"SAU Gvozdika", {0.9, 860}},
{"SAU Msta", {0.9, 860}},
{"SAU Akatsia", {0.9, 860}},
{"SAU 2-C9", {0.9, 860}},
{"M-109", {0.9, 860}},
{"SpGH_Dana", {0.9, 860}},
{"AAV7", {0.9, 860}},
{"BMD-1", {0.9, 860}},
{"BMP-1", {0.9, 860}},
{"BMP-2", {0.9, 860}},
{"BMP-3", {0.9, 860}},
{"BRDM-2", {0.9, 860}},
{"BTR_D", {0.9, 860}},
{"Cobra", {0.9, 860}},
{"LAV-25", {0.9, 860}},
{"M1043 HMMWV Armament", {0.9, 860}},
{"M1045 HMMWV TOW", {0.9, 860}},
{"M1126 Stryker ICV", {0.9, 860}},
{"M-113", {0.9, 860}},
{"M1134 Stryker ATGM", {0.9, 860}},
{"M-2 Bradley", {0.9, 860}},
{"MCV-80", {0.9, 860}},
{"MTLB", {0.9, 860}},
{"Marder", {0.9, 860}},
{"TPZ", {0.9, 860}},
{"Grad_FDDM", {0.9, 860}},
{"Paratrooper RPG-16", {0.9, 860}},
{"Paratrooper AKS-74", {0.9, 860}},
{"Infantry AK Ins", {0.9, 860}},
{"Soldier AK", {0.9, 860}},
{"Infantry AK", {0.9, 860}},
{"Soldier M249", {0.9, 860}},
{"Soldier M4", {0.9, 860}},
{"Soldier M4 GRG", {0.9, 860}},
{"Soldier RPG", {0.9, 860}},
{"MLRS FDDM", {0.9, 860}},
{"Infantry AK ver2", {0.9, 860}},
{"Infantry AK ver3", {0.9, 860}},
{"Grad-URAL", {0.9, 860}},
{"Uragan_BM-27", {0.9, 860}},
{"Smerch", {0.9, 860}},
{"Smerch_HE", {0.9, 860}},
{"MLRS", {0.9, 860}},
{"Challenger2", {0.9, 860}},
{"Leclerc", {0.9, 860}},
{"M-60", {0.9, 860}},
{"M1128 Stryker MGS", {0.9, 860}},
{"M-1 Abrams", {0.9, 860}},
{"T-55", {0.9, 860}},
{"T-72B", {0.9, 860}},
{"T-80UD", {0.9, 860}},
{"T-90", {0.9, 860}},
{"Leopard1A3", {0.9, 860}},
{"Merkava_Mk4", {0.9, 860}},
{"JTAC", {0.9, 860}},
{"Infantry Animated", {0.9, 860}},
{"HL_DSHK", {0.9, 860}},
{"HL_KORD", {0.9, 860}},
{"tt_DSHK", {0.9, 860}},
{"tt_KORD", {0.9, 860}},
{"HL_B8M1", {0.9, 860}},
{"tt_B8M1", {0.9, 860}},
{"M4_Sherman", {0.9, 860}},
{"M2A1_halftrack", {0.9, 860}},
{"BTR-80", {0.9, 860}},
{"T-72B3", {0.9, 860}},
{"PT_76", {0.9, 860}},
{"BTR-82A", {0.9, 860}},
{"Chieftain_mk3", {0.9, 860}},
{"Pz_IV_H", {0.9, 860}},
{"Leopard-2A5", {0.9, 860}},
{"Leopard-2", {0.9, 860}},
{"leopard-2A4", {0.9, 860}},
{"leopard-2A4_trs", {0.9, 860}},
{"Sd_Kfz_251", {0.9, 860}},
{"T155_Firtina", {0.9, 860}},
{"VAB_Mephisto", {0.9, 860}},
{"ZTZ96B", {0.9, 860}},
{"ZBD04A", {0.9, 860}},
{"PLZ05", {0.9, 860}},
{"TYPE-59", {0.9, 860}},
{"Tiger_I", {0.9, 860}},
{"Tiger_II_H", {0.9, 860}},
{"Pz_V_Panther_G", {0.9, 860}},
{"Jagdpanther_G1", {0.9, 860}},
{"JagdPz_IV", {0.9, 860}},
{"Stug_IV", {0.9, 860}},
{"SturmPzIV", {0.9, 860}},
{"Wespe124", {0.9, 860}},
{"Sd_Kfz_234_2_Puma", {0.9, 860}},
{"soldier_mauser98", {0.9, 860}},
{"Stug_III", {0.9, 860}},
{"Elefant_SdKfz_184", {0.9, 860}},
{"Pak40", {0.9, 860}},
{"LeFH_18-40-105", {0.9, 860}},
{"Cromwell_IV", {0.9, 860}},
{"M4A4_Sherman_FF", {0.9, 860}},
{"soldier_wwii_br_01", {0.9, 860}},
{"Centaur_IV", {0.9, 860}},
{"Churchill_VII", {0.9, 860}},
{"Daimler_AC", {0.9, 860}},
{"Tetrarch", {0.9, 860}},
{"M12_GMC", {0.9, 860}},
{"soldier_wwii_us", {0.9, 860}},
{"M10_GMC", {0.9, 860}},
{"M8_Greyhound", {0.9, 860}},
{"M2A1-105", {0.9, 860}},
{"M4_Tractor", {0.9, 860}},

View File

@ -6,6 +6,11 @@ class Aircraft : public AirUnit
public:
Aircraft(json::value json, unsigned int ID);
static void loadDatabase(string path);
virtual void changeSpeed(string change);
virtual void changeAltitude(string change);
protected:
static json::value database;
};

View File

@ -95,21 +95,26 @@ namespace ECMUse {
class Command
{
public:
Command(function<void(void)> callback) : callback(callback) {};
unsigned int getPriority() { return priority; }
virtual string getString() = 0;
virtual unsigned int getLoad() = 0;
const string getHash() { return hash; }
void executeCallback() { callback(); }
protected:
unsigned int priority = CommandPriority::LOW;
const string hash = random_string(16);
function<void(void)> callback;
};
/* Simple low priority move command (from user click) */
class Move : public Command
{
public:
Move(string groupName, Coords destination, double speed, string speedType, double altitude, string altitudeType, string taskOptions, string category):
Move(string groupName, Coords destination, double speed, string speedType, double altitude,
string altitudeType, string taskOptions, string category, function<void(void)> callback = []() {}) :
Command(callback),
groupName(groupName),
destination(destination),
speed(speed),
@ -139,7 +144,8 @@ private:
class Smoke : public Command
{
public:
Smoke(string color, Coords location) :
Smoke(string color, Coords location, function<void(void)> callback = [](){}) :
Command(callback),
color(color),
location(location)
{
@ -157,7 +163,8 @@ private:
class SpawnGroundUnits : public Command
{
public:
SpawnGroundUnits(string coalition, vector<SpawnOptions> spawnOptions, string country, bool immediate) :
SpawnGroundUnits(string coalition, vector<SpawnOptions> spawnOptions, string country, bool immediate, function<void(void)> callback = [](){}) :
Command(callback),
coalition(coalition),
spawnOptions(spawnOptions),
country(country),
@ -179,7 +186,8 @@ private:
class SpawnNavyUnits : public Command
{
public:
SpawnNavyUnits(string coalition, vector<SpawnOptions> spawnOptions, string country, bool immediate) :
SpawnNavyUnits(string coalition, vector<SpawnOptions> spawnOptions, string country, bool immediate, function<void(void)> callback = [](){}) :
Command(callback),
coalition(coalition),
spawnOptions(spawnOptions),
country(country),
@ -201,7 +209,8 @@ private:
class SpawnAircrafts : public Command
{
public:
SpawnAircrafts(string coalition, vector<SpawnOptions> spawnOptions, string airbaseName, string country, bool immediate) :
SpawnAircrafts(string coalition, vector<SpawnOptions> spawnOptions, string airbaseName, string country, bool immediate, function<void(void)> callback = [](){}) :
Command(callback),
coalition(coalition),
spawnOptions(spawnOptions),
airbaseName(airbaseName),
@ -221,12 +230,12 @@ private:
const bool immediate;
};
/* Spawn helicopter command */
class SpawnHelicopters : public Command
{
public:
SpawnHelicopters(string coalition, vector<SpawnOptions> spawnOptions, string airbaseName, string country, bool immediate) :
SpawnHelicopters(string coalition, vector<SpawnOptions> spawnOptions, string airbaseName, string country, bool immediate, function<void(void)> callback = [](){}) :
Command(callback),
coalition(coalition),
spawnOptions(spawnOptions),
airbaseName(airbaseName),
@ -250,7 +259,8 @@ private:
class Clone : public Command
{
public:
Clone(vector<CloneOptions> cloneOptions, bool deleteOriginal) :
Clone(vector<CloneOptions> cloneOptions, bool deleteOriginal, function<void(void)> callback = [](){}) :
Command(callback),
cloneOptions(cloneOptions),
deleteOriginal(deleteOriginal)
{
@ -268,7 +278,8 @@ private:
class Delete : public Command
{
public:
Delete(unsigned int ID, bool explosion, bool immediate ) :
Delete(unsigned int ID, bool explosion, bool immediate, function<void(void)> callback = [](){}) :
Command(callback),
ID(ID),
explosion(explosion),
immediate(immediate)
@ -289,7 +300,8 @@ private:
class SetTask : public Command
{
public:
SetTask(string groupName, string task) :
SetTask(string groupName, string task, function<void(void)> callback = [](){}) :
Command(callback),
groupName(groupName),
task(task)
{
@ -307,7 +319,8 @@ private:
class ResetTask : public Command
{
public:
ResetTask(string groupName) :
ResetTask(string groupName, function<void(void)> callback = [](){}) :
Command(callback),
groupName(groupName)
{
priority = CommandPriority::HIGH;
@ -323,7 +336,8 @@ private:
class SetCommand : public Command
{
public:
SetCommand(string groupName, string command) :
SetCommand(string groupName, string command, function<void(void)> callback = [](){}) :
Command(callback),
groupName(groupName),
command(command)
{
@ -341,7 +355,8 @@ private:
class SetOption : public Command
{
public:
SetOption(string groupName, unsigned int optionID, unsigned int optionValue) :
SetOption(string groupName, unsigned int optionID, unsigned int optionValue, function<void(void)> callback = [](){}) :
Command(callback),
groupName(groupName),
optionID(optionID),
optionValue(optionValue),
@ -351,7 +366,8 @@ public:
priority = CommandPriority::HIGH;
};
SetOption(string groupName, unsigned int optionID, bool optionBool) :
SetOption(string groupName, unsigned int optionID, bool optionBool, function<void(void)> callback = [](){}) :
Command(callback),
groupName(groupName),
optionID(optionID),
optionValue(0),
@ -375,7 +391,8 @@ private:
class SetOnOff : public Command
{
public:
SetOnOff(string groupName, bool onOff) :
SetOnOff(string groupName, bool onOff, function<void(void)> callback = [](){}) :
Command(callback),
groupName(groupName),
onOff(onOff)
{
@ -393,7 +410,8 @@ private:
class Explosion : public Command
{
public:
Explosion(unsigned int intensity, Coords location) :
Explosion(unsigned int intensity, Coords location, function<void(void)> callback = [](){}) :
Command(callback),
location(location),
intensity(intensity)
{

View File

@ -64,7 +64,8 @@ namespace State
BOMB_POINT,
CARPET_BOMB,
BOMB_BUILDING,
FIRE_AT_AREA
FIRE_AT_AREA,
SIMULATE_FIRE_FIGHT
};
};
@ -125,4 +126,4 @@ struct SpawnOptions {
struct CloneOptions {
unsigned int ID;
Coords location;
};
};

View File

@ -8,6 +8,8 @@ class GroundUnit : public Unit
public:
GroundUnit(json::value json, unsigned int ID);
static void loadDatabase(string path);
virtual void setState(unsigned char newState);
virtual void setDefaults(bool force = false);
@ -17,4 +19,5 @@ public:
protected:
virtual void AIloop();
static json::value database;
};

View File

@ -6,6 +6,11 @@ class Helicopter : public AirUnit
public:
Helicopter(json::value json, unsigned int ID);
static void loadDatabase(string path);
virtual void changeSpeed(string change);
virtual void changeAltitude(string change);
protected:
static json::value database;
};

View File

@ -8,6 +8,8 @@ class NavyUnit : public Unit
public:
NavyUnit(json::value json, unsigned int ID);
static void loadDatabase(string path);
virtual void setState(unsigned char newState);
virtual void setDefaults(bool force = false);
@ -16,5 +18,5 @@ public:
protected:
virtual void AIloop();
static json::value database;
};

View File

@ -54,6 +54,7 @@ public:
void resetTask();
bool checkTaskFailed();
void resetTaskFailedCounter();
void setHasTaskAssigned(bool newHasTaskAssigned);
void triggerUpdate(unsigned char datumIndex);
@ -185,6 +186,7 @@ protected:
/********** Other **********/
unsigned int taskCheckCounter = 0;
bool hasTaskAssigned = false;
double initialFuel = 0;
map<unsigned char, unsigned long long> updateTimeMap;
unsigned long long lastLoopTime = 0;

View File

@ -11,6 +11,26 @@ using namespace GeographicLib;
extern Scheduler* scheduler;
extern UnitsManager* unitsManager;
json::value Aircraft::database = json::value();
void Aircraft::loadDatabase(string path) {
char* buf = nullptr;
size_t sz = 0;
if (_dupenv_s(&buf, &sz, "DCSOLYMPUS_PATH") == 0 && buf != nullptr)
{
std::ifstream ifstream(string(buf) + path);
std::stringstream ss;
ss << ifstream.rdbuf();
std::error_code errorCode;
database = json::value::parse(ss.str(), errorCode);
if (database.is_object())
log("Aircrafts database loaded correctly");
else
log("Error reading Aircrafts database file");
free(buf);
}
}
/* Aircraft */
Aircraft::Aircraft(json::value json, unsigned int ID) : AirUnit(json, ID)

View File

@ -162,7 +162,7 @@ void AirUnit::AIloop()
desiredAltitude << ", speed = " << desiredSpeed << ", altitudeType = '" <<
(desiredAltitudeType ? "AGL" : "ASL") << "', speedType = '" << (desiredSpeedType ? "GS" : "CAS") << "'}";
}
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str()));
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command);
setHasTask(true);
}
@ -267,7 +267,7 @@ void AirUnit::AIloop()
<< "z = " << formationOffset.z
<< "},"
<< "}";
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str()));
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command);
setHasTask(true);
}
@ -283,7 +283,7 @@ void AirUnit::AIloop()
taskSS << "{"
<< "id = 'Refuel'"
<< "}";
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str()));
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command);
setHasTask(true);
}
@ -297,8 +297,10 @@ void AirUnit::AIloop()
if (!getHasTask()) {
std::ostringstream taskSS;
taskSS.precision(10);
taskSS << "{id = 'Bombing', lat = " << targetPosition.lat << ", lng = " << targetPosition.lng << "}";
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str()));
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command);
setHasTask(true);
}
@ -308,8 +310,10 @@ void AirUnit::AIloop()
if (!getHasTask()) {
std::ostringstream taskSS;
taskSS.precision(10);
taskSS << "{id = 'CarpetBombing', lat = " << targetPosition.lat << ", lng = " << targetPosition.lng << "}";
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str()));
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command);
setHasTask(true);
}
@ -320,8 +324,10 @@ void AirUnit::AIloop()
if (!getHasTask()) {
std::ostringstream taskSS;
taskSS.precision(10);
taskSS << "{id = 'AttackMapObject', lat = " << targetPosition.lat << ", lng = " << targetPosition.lng << "}";
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str()));
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command);
setHasTask(true);
}

View File

@ -7,6 +7,10 @@
#include "scheduler.h"
#include "scriptLoader.h"
#include "luatools.h"
#include "aircraft.h"
#include "helicopter.h"
#include "groundunit.h"
#include "navyunit.h"
#include <chrono>
using namespace std::chrono;
@ -59,6 +63,11 @@ extern "C" DllExport int coreInit(lua_State* L)
server = new Server(L);
scheduler = new Scheduler(L);
Aircraft::loadDatabase(AIRCRAFT_DATABASE_PATH);
Helicopter::loadDatabase(HELICOPTER_DATABASE_PATH);
GroundUnit::loadDatabase(GROUNDUNIT_DATABASE_PATH);
NavyUnit::loadDatabase(NAVYUNIT_DATABASE_PATH);
registerLuaFunctions(L);
server->start(L);

View File

@ -4,13 +4,33 @@
#include "commands.h"
#include "scheduler.h"
#include "defines.h"
#include "unitsManager.h"
#include "unitsmanager.h"
#include <GeographicLib/Geodesic.hpp>
using namespace GeographicLib;
extern Scheduler* scheduler;
extern UnitsManager* unitsManager;
json::value GroundUnit::database = json::value();
void GroundUnit::loadDatabase(string path) {
char* buf = nullptr;
size_t sz = 0;
if (_dupenv_s(&buf, &sz, "DCSOLYMPUS_PATH") == 0 && buf != nullptr)
{
std::ifstream ifstream(string(buf) + path);
std::stringstream ss;
ss << ifstream.rdbuf();
std::error_code errorCode;
database = json::value::parse(ss.str(), errorCode);
if (database.is_object())
log("Ground Units database loaded correctly");
else
log("Error reading Ground Units database file");
free(buf);
}
}
/* Ground unit */
GroundUnit::GroundUnit(json::value json, unsigned int ID) : Unit(json, ID)
@ -49,6 +69,10 @@ void GroundUnit::setState(unsigned char newState)
setTargetPosition(Coords(NULL));
break;
}
case State::SIMULATE_FIRE_FIGHT: {
setTargetPosition(Coords(NULL));
break;
}
default:
break;
}
@ -70,12 +94,16 @@ void GroundUnit::setState(unsigned char newState)
resetActiveDestination();
break;
}
case State::SIMULATE_FIRE_FIGHT: {
clearActivePath();
resetActiveDestination();
break;
}
default:
break;
}
if (newState != state)
resetTask();
resetTask();
log(unitName + " setting state from " + to_string(state) + " to " + to_string(newState));
state = newState;
@ -122,8 +150,46 @@ void GroundUnit::AIloop()
if (!getHasTask()) {
std::ostringstream taskSS;
taskSS.precision(10);
taskSS << "{id = 'FireAtPoint', lat = " << targetPosition.lat << ", lng = " << targetPosition.lng << ", radius = 1000}";
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str()));
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command);
setHasTask(true);
}
}
case State::SIMULATE_FIRE_FIGHT: {
setTask("Simulating fire fight");
if (!getHasTask() || ((double)(rand()) / (double)(RAND_MAX)) < 0.01) {
double dist;
double bearing1;
double bearing2;
Geodesic::WGS84().Inverse(position.lat, position.lng, targetPosition.lat, targetPosition.lng, dist, bearing1, bearing2);
double r = 5; /* m */
/* Default gun values */
double barrelHeight = 1.0; /* m */
double muzzleVelocity = 860; /* m/s */
if (database.has_object_field(to_wstring(name))) {
json::value databaseEntry = database[to_wstring(name)];
if (databaseEntry.has_number_field(L"barrelHeight") && databaseEntry.has_number_field(L"muzzleVelocity")) {
barrelHeight = databaseEntry[L"barrelHeight"].as_number().to_double();
muzzleVelocity = databaseEntry[L"muzzleVelocity"].as_number().to_double();
log(to_string(barrelHeight) + " " + to_string(muzzleVelocity));
}
}
double barrelElevation = r * (9.81 * dist / (2 * muzzleVelocity * muzzleVelocity) + (targetPosition.alt - (position.alt + barrelHeight)) / dist); /* m */
double lat = 0;
double lng = 0;
double randomBearing = bearing1 + (((double)(rand()) / (double)(RAND_MAX) - 0.5) * 2) * 15;
Geodesic::WGS84().Direct(position.lat, position.lng, randomBearing, r, lat, lng);
std::ostringstream taskSS;
taskSS.precision(10);
taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << barrelElevation + barrelHeight << ", radius = 0.001}";
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command);
setHasTask(true);
}
@ -161,4 +227,4 @@ void GroundUnit::setFollowRoads(bool newFollowRoads, bool force)
Unit::setFollowRoads(newFollowRoads, force);
resetActiveDestination(); /* Reset active destination to apply option*/
}
}
}

View File

@ -11,6 +11,26 @@ using namespace GeographicLib;
extern Scheduler* scheduler;
extern UnitsManager* unitsManager;
json::value Helicopter::database = json::value();
void Helicopter::loadDatabase(string path) {
char* buf = nullptr;
size_t sz = 0;
if (_dupenv_s(&buf, &sz, "DCSOLYMPUS_PATH") == 0 && buf != nullptr)
{
std::ifstream ifstream(string(buf) + path);
std::stringstream ss;
ss << ifstream.rdbuf();
std::error_code errorCode;
database = json::value::parse(ss.str(), errorCode);
if (database.is_object())
log("Helicopters database loaded correctly");
else
log("Error reading Helicopters database file");
free(buf);
}
}
/* Helicopter */
Helicopter::Helicopter(json::value json, unsigned int ID) : AirUnit(json, ID)

View File

@ -11,6 +11,26 @@ using namespace GeographicLib;
extern Scheduler* scheduler;
extern UnitsManager* unitsManager;
json::value NavyUnit::database = json::value();
void NavyUnit::loadDatabase(string path) {
char* buf = nullptr;
size_t sz = 0;
if (_dupenv_s(&buf, &sz, "DCSOLYMPUS_PATH") == 0 && buf != nullptr)
{
std::ifstream ifstream(string(buf) + path);
std::stringstream ss;
ss << ifstream.rdbuf();
std::error_code errorCode;
database = json::value::parse(ss.str(), errorCode);
if (database.is_object())
log("Navy Units database loaded correctly");
else
log("Error reading Navy Units database file");
free(buf);
}
}
/* Navy Unit */
NavyUnit::NavyUnit(json::value json, unsigned int ID) : Unit(json, ID)
@ -49,6 +69,10 @@ void NavyUnit::setState(unsigned char newState)
setTargetPosition(Coords(NULL));
break;
}
case State::SIMULATE_FIRE_FIGHT: {
setTargetPosition(Coords(NULL));
break;
}
default:
break;
}
@ -68,6 +92,13 @@ void NavyUnit::setState(unsigned char newState)
case State::FIRE_AT_AREA: {
clearActivePath();
resetActiveDestination();
resetTask();
break;
}
case State::SIMULATE_FIRE_FIGHT: {
clearActivePath();
resetActiveDestination();
resetTask();
break;
}
default:
@ -118,8 +149,23 @@ void NavyUnit::AIloop()
if (!getHasTask()) {
std::ostringstream taskSS;
taskSS.precision(10);
taskSS << "{id = 'FireAtPoint', lat = " << targetPosition.lat << ", lng = " << targetPosition.lng << ", radius = 1000}";
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str()));
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command);
setHasTask(true);
}
}
case State::SIMULATE_FIRE_FIGHT: {
setTask("Simulating fire fight");
if (!getHasTask()) {
std::ostringstream taskSS;
taskSS.precision(10);
taskSS << "{id = 'FireAtPoint', lat = " << targetPosition.lat << ", lng = " << targetPosition.lng << ", radius = 1}";
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command);
setHasTask(true);
}

View File

@ -61,6 +61,7 @@ void Scheduler::execute(lua_State* L)
load = command->getLoad();
commands.remove(command);
executedCommandsHashes.push_back(command->getHash());
command->executeCallback(); /* Execute the command callback (this is a lambda function that can be used to execute a function when the command is run) */
delete command;
return;
}
@ -561,6 +562,19 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
unit->setTargetPosition(loc);
log(username + " tasked unit " + unit->getName() + " to fire at area", true);
}
else if (key.compare("simulateFireFight") == 0)
{
unsigned int ID = value[L"ID"].as_integer();
unitsManager->acquireControl(ID);
double lat = value[L"location"][L"lat"].as_double();
double lng = value[L"location"][L"lng"].as_double();
double alt = value[L"altitude"].as_double();
Coords loc; loc.lat = lat; loc.lng = lng; loc.alt = alt;
Unit* unit = unitsManager->getGroupLeader(ID);
unit->setState(State::SIMULATE_FIRE_FIGHT);
unit->setTargetPosition(loc);
log(username + " tasked unit " + unit->getName() + " to simulate a fire fight", true);
}
else if (key.compare("setCommandModeOptions") == 0) {
setCommandModeOptions(value);
log(username + " updated the Command Mode Options", true);

View File

@ -291,7 +291,7 @@ void Server::task()
size_t sz = 0;
if (_dupenv_s(&buf, &sz, "DCSOLYMPUS_PATH") == 0 && buf != nullptr)
{
std::ifstream ifstream(string(buf) + "\\olympus.json");
std::ifstream ifstream(string(buf) + OLYMPUS_JSON_PATH);
std::stringstream ss;
ss << ifstream.rdbuf();
std::error_code errorCode;

View File

@ -146,8 +146,10 @@ void Unit::runAILoop() {
/* If the unit is alive, controlled, is the leader of the group and it is not a human, run the AI Loop that performs the requested commands and instructions (moving, attacking, etc) */
if (getAlive() && getControlled() && !getHuman() && getIsLeader()) {
if (checkTaskFailed() && state != State::IDLE && state != State::LAND)
if (checkTaskFailed() && state != State::IDLE && state != State::LAND) {
log(unitName + " has no task, switching to IDLE state");
setState(State::IDLE);
}
AIloop();
}
@ -397,7 +399,7 @@ void Unit::resetActiveDestination()
void Unit::resetTask()
{
Command* command = dynamic_cast<Command*>(new ResetTask(groupName));
Command* command = dynamic_cast<Command*>(new ResetTask(groupName, [this]() { this->setHasTaskAssigned(false); }));
scheduler->appendCommand(command);
setHasTask(false);
resetTaskFailedCounter();
@ -660,7 +662,7 @@ void Unit::goToDestination(string enrouteTask)
{
if (activeDestination != NULL)
{
Command* command = dynamic_cast<Command*>(new Move(groupName, activeDestination, getDesiredSpeed(), getDesiredSpeedType() ? "GS" : "CAS", getDesiredAltitude(), getDesiredAltitudeType() ? "AGL" : "ASL", enrouteTask, getCategory()));
Command* command = dynamic_cast<Command*>(new Move(groupName, activeDestination, getDesiredSpeed(), getDesiredSpeedType() ? "GS" : "CAS", getDesiredAltitude(), getDesiredAltitudeType() ? "AGL" : "ASL", enrouteTask, getCategory(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command);
setHasTask(true);
}
@ -732,7 +734,7 @@ bool Unit::checkTaskFailed()
return false;
else {
if (taskCheckCounter > 0)
taskCheckCounter--;
taskCheckCounter -= hasTaskAssigned;
return taskCheckCounter == 0;
}
}
@ -741,6 +743,14 @@ void Unit::resetTaskFailedCounter() {
taskCheckCounter = TASK_CHECK_INIT_VALUE;
}
void Unit::setHasTaskAssigned(bool newHasTaskAssigned) {
hasTaskAssigned = newHasTaskAssigned;
if (hasTaskAssigned)
log(unitName + " was assigned a new task");
else
log(unitName + " no task assigned");
}
void Unit::triggerUpdate(unsigned char datumIndex) {
updateTimeMap[datumIndex] = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
}

View File

@ -12,4 +12,10 @@
#define MISSION_URI "mission"
#define COMMANDS_URI "commands"
#define FRAMERATE_TIME_INTERVAL 0.05
#define FRAMERATE_TIME_INTERVAL 0.05
#define OLYMPUS_JSON_PATH "\\olympus.json"
#define AIRCRAFT_DATABASE_PATH "\\client\\public\\databases\\units\\aircraftdatabase.json"
#define HELICOPTER_DATABASE_PATH "\\client\\public\\databases\\units\\helicopterdatabase.json"
#define GROUNDUNIT_DATABASE_PATH "\\client\\public\\databases\\units\\groundunitdatabase.json"
#define NAVYUNIT_DATABASE_PATH "\\client\\public\\databases\\units\\navyunitdatabase.json"