18 Commits

Author SHA1 Message Date
Pax1601
283b9e682e v0.4.7 2023-11-17 22:04:21 +01:00
Pax1601
8b5956e76b Merge pull request #536 from Pax1601/489-show-all-the-action-options-of-the-selected-units-even-if-some-units-dont-have-them
489 show all the action options of the selected units even if some units dont have them
2023-11-17 22:02:51 +01:00
Pax1601
017a89b945 getDesc().category reimplemented
unit:getCategory() reports "Airplane" for Helicopters
2023-11-17 22:02:32 +01:00
Pax1601
5bc685182b Merge branch 'main' into 489-show-all-the-action-options-of-the-selected-units-even-if-some-units-dont-have-them 2023-11-17 21:40:09 +01:00
Pax1601
f47fc2fb19 Merge pull request #535 from Pax1601/534-olympus-not-sending-data-with-dcs-open-beta-29148111
Changed unit:getCategory to Object.getCategory(unit)
2023-11-17 21:35:20 +01:00
Pax1601
11d6f25606 Changed unit:getCategory to Object.getCategory(unit)
And added some isExist guards
2023-11-17 21:34:50 +01:00
Pax1601
331692e3d3 Merge branch '489-show-all-the-action-options-of-the-selected-units-even-if-some-units-dont-have-them' of https://github.com/Pax1601/DCSOlympus into 489-show-all-the-action-options-of-the-selected-units-even-if-some-units-dont-have-them 2023-11-17 21:10:49 +01:00
Pax1601
7483225e0d Small tweak to login page 2023-11-17 21:10:46 +01:00
Pax1601
22ae882032 Unit gets forcefully deselected when hidden 2023-11-17 17:45:10 +01:00
Pax1601
fbab82e4de Merge branch 'main' into 489-show-all-the-action-options-of-the-selected-units-even-if-some-units-dont-have-them 2023-11-17 17:26:56 +01:00
Pax1601
ca81d1c4ce Merge pull request #530 from Pax1601/529-add-favicon
Favicons
2023-11-17 17:24:03 +01:00
Pax1601
a1884ab19f Merge pull request #527 from Pax1601/522-flashpulse-robot-lock-when-trying-to-give-orders-to-protected-units
Lock flashes on protected robot interaction
2023-11-17 17:23:18 +01:00
Pax1601
8905cd5e85 Merge pull request #526 from Pax1601/523-selected-units-dont-get-hidden
Selected units now hidden on request
2023-11-17 17:22:36 +01:00
PeekabooSteam
8d5ed33ad8 Favicons 2023-11-17 11:35:55 +00:00
Pax1601
b747752b76 Fixed error in follow command 2023-11-17 07:59:04 +01:00
PeekabooSteam
4e13daa270 Lock flashes on protected robot interaction 2023-11-16 23:08:01 +00:00
PeekabooSteam
c00096bd9a Selected units now hidden on request 2023-11-16 22:26:09 +00:00
Pax1601
4a54011aac Large rework of context menus for units and map 2023-11-16 15:31:07 +01:00
32 changed files with 739 additions and 628 deletions

View File

@@ -54,7 +54,7 @@ class DemoDataGenerator {
/* /*
UNCOMMENT TO TEST ALL UNITS ***************** UNCOMMENT TO TEST ALL UNITS ****************
var databases = Object.assign({}, aircraftDatabase, helicopterDatabase, groundUnitDatabase, navyUnitDatabase); var databases = Object.assign({}, aircraftDatabase, helicopterDatabase, groundUnitDatabase, navyUnitDatabase);
var t = Object.keys(databases).length; var t = Object.keys(databases).length;
@@ -114,6 +114,39 @@ class DemoDataGenerator {
DEMO_UNIT_DATA[idx].position.lat += idx / 100; DEMO_UNIT_DATA[idx].position.lat += idx / 100;
DEMO_UNIT_DATA[idx].category = "GroundUnit"; DEMO_UNIT_DATA[idx].category = "GroundUnit";
DEMO_UNIT_DATA[idx].isLeader = false; DEMO_UNIT_DATA[idx].isLeader = false;
idx += 1;
DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData));
DEMO_UNIT_DATA[idx].name = "F-14B";
DEMO_UNIT_DATA[idx].groupName = `Group-1`;
DEMO_UNIT_DATA[idx].position.lat += idx / 100;
DEMO_UNIT_DATA[idx].category = "Aircraft";
DEMO_UNIT_DATA[idx].isLeader = false;
idx += 1;
DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData));
DEMO_UNIT_DATA[idx].name = "Infantry AK";
DEMO_UNIT_DATA[idx].groupName = `Group-2`;
DEMO_UNIT_DATA[idx].position.lat += idx / 100;
DEMO_UNIT_DATA[idx].category = "GroundUnit";
DEMO_UNIT_DATA[idx].isLeader = true;
idx += 1;
DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData));
DEMO_UNIT_DATA[idx].name = "Infantry AK";
DEMO_UNIT_DATA[idx].groupName = `Group-3`;
DEMO_UNIT_DATA[idx].position.lat += idx / 100;
DEMO_UNIT_DATA[idx].category = "GroundUnit";
DEMO_UNIT_DATA[idx].isLeader = true;
idx += 1;
DEMO_UNIT_DATA[idx] = JSON.parse(JSON.stringify(baseData));
DEMO_UNIT_DATA[idx].name = "KC-135";
DEMO_UNIT_DATA[idx].groupName = `Group-4`;
DEMO_UNIT_DATA[idx].position.lat += idx / 100;
DEMO_UNIT_DATA[idx].category = "Aircraft";
DEMO_UNIT_DATA[idx].isLeader = true;
this.startTime = Date.now(); this.startTime = Date.now();
} }

View File

@@ -1,12 +1,12 @@
{ {
"name": "DCSOlympus", "name": "DCSOlympus",
"version": "v0.4.6-alpha", "version": "v0.4.7-alpha",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "DCSOlympus", "name": "DCSOlympus",
"version": "v0.4.6-alpha", "version": "v0.4.7-alpha",
"dependencies": { "dependencies": {
"@turf/turf": "^6.5.0", "@turf/turf": "^6.5.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",

View File

@@ -2,7 +2,7 @@
"name": "DCSOlympus", "name": "DCSOlympus",
"node-main": "./bin/www", "node-main": "./bin/www",
"main": "http://localhost:3000", "main": "http://localhost:3000",
"version": "v0.4.6-alpha", "version": "v0.4.7-alpha",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "browserify .\\src\\index.ts --debug -o .\\public\\javascripts\\bundle.js -t [ babelify --global true --presets [ @babel/preset-env ] --extensions '.js'] -p [ tsify --noImplicitAny ] && copy.bat", "build": "browserify .\\src\\index.ts --debug -o .\\public\\javascripts\\bundle.js -t [ babelify --global true --presets [ @babel/preset-env ] --extensions '.js'] -p [ tsify --noImplicitAny ] && copy.bat",

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,16 @@
{
"name": "DCS Olympus",
"short_name": "DCS Olympus",
"icons": [{
"src": "/images/favicons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
}, {
"src": "/images/favicons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -687,6 +687,19 @@ nav.ol-panel> :last-child {
width:10px; width:10px;
} }
@keyframes lock-prompt {
100% {
opacity: 1;
}
0% {
opacity: 0;
}
}
.ol-navbar-buttons-group > .protectable > button[data-protected].lock.prompt svg {
animation: lock-prompt .25s alternate infinite;
}
.ol-navbar-buttons-group > .protectable > button.lock svg.locked * { .ol-navbar-buttons-group > .protectable > button.lock svg.locked * {
fill:white !important; fill:white !important;
} }

View File

@@ -15,11 +15,11 @@ export const BLUE_COMMANDER = "Blue commander";
export const RED_COMMANDER = "Red commander"; export const RED_COMMANDER = "Red commander";
export const VISUAL = 1; export const VISUAL = 1;
export const OPTIC = 2; export const OPTIC = 2;
export const RADAR = 4; export const RADAR = 4;
export const IRST = 8; export const IRST = 8;
export const RWR = 16; export const RWR = 16;
export const DLINK = 32; 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", "simulate-fire-fight", "scenic-aaa", "miss-on-purpose", "land-at-point"]; 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", "scenic-aaa", "miss-on-purpose", "land-at-point"];
export const ROEs: string[] = ["free", "designated", "", "return", "hold"]; export const ROEs: string[] = ["free", "designated", "", "return", "hold"];
@@ -35,16 +35,16 @@ export const ROEDescriptions: string[] = [
]; ];
export const reactionsToThreatDescriptions: string[] = [ export const reactionsToThreatDescriptions: string[] = [
"None (No reaction)", "None (No reaction)",
"Manoeuvre (no countermeasures)", "Manoeuvre (no countermeasures)",
"Passive (Countermeasures only, no manoeuvre)", "Passive (Countermeasures only, no manoeuvre)",
"Evade (Countermeasures and manoeuvers)" "Evade (Countermeasures and manoeuvers)"
]; ];
export const emissionsCountermeasuresDescriptions: string[] = [ export const emissionsCountermeasuresDescriptions: string[] = [
"Silent (Radar OFF, no ECM)", "Silent (Radar OFF, no ECM)",
"Attack (Radar only for targeting, ECM only if locked)", "Attack (Radar only for targeting, ECM only if locked)",
"Defend (Radar for searching, ECM if locked)", "Defend (Radar for searching, ECM if locked)",
"Always on (Radar and ECM always on)" "Always on (Radar and ECM always on)"
]; ];
@@ -118,25 +118,25 @@ export const mapLayers = {
"ArcGIS Satellite": { "ArcGIS Satellite": {
urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
minZoom: 1, minZoom: 1,
maxZoom: 18, maxZoom: 19,
attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, GetApp().getMap()ping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, GetApp().getMap()ping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community"
}, },
"USGS Topo": { "USGS Topo": {
urlTemplate: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', urlTemplate: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}',
minZoom: 1, minZoom: 1,
maxZoom: 18, maxZoom: 14,
attribution: 'Tiles courtesy of the <a href="https://usgs.gov/">U.S. Geological Survey</a>' attribution: 'Tiles courtesy of the <a href="https://usgs.gov/">U.S. Geological Survey</a>'
}, },
"OpenStreetMap Mapnik": { "OpenStreetMap Mapnik": {
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
minZoom: 1, minZoom: 1,
maxZoom: 18, maxZoom: 20,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}, },
"OPENVKarte": { "OPENVKarte": {
urlTemplate: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', urlTemplate: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png',
minZoom: 1, minZoom: 1,
maxZoom: 18, maxZoom: 20,
attribution: 'Map <a href="https://memomaps.de/">memomaps.de</a> <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution: 'Map <a href="https://memomaps.de/">memomaps.de</a> <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}, },
"Esri.DeLorme": { "Esri.DeLorme": {
@@ -148,7 +148,7 @@ export const mapLayers = {
"CyclOSM": { "CyclOSM": {
urlTemplate: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', urlTemplate: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png',
minZoom: 1, minZoom: 1,
maxZoom: 18, maxZoom: 20,
attribution: '<a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> | Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution: '<a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> | Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
} }
} }
@@ -160,62 +160,62 @@ export const COALITIONAREA_DRAW_POLYGON = "Draw Coalition Area";
export const visibilityControls: string[] = ["human", "dcs", "aircraft", "helicopter", "groundunit-sam", "groundunit-other", "navyunit", "airbase"]; export const visibilityControls: string[] = ["human", "dcs", "aircraft", "helicopter", "groundunit-sam", "groundunit-other", "navyunit", "airbase"];
export const visibilityControlsTypes: string[][] = [["human"], ["dcs"], ["aircraft"], ["helicopter"], ["groundunit-sam", "groundunit-sam-radar", "groundunit-sam-launcher"], ["groundunit-other", "groundunit-ewr"], ["navyunit"], ["airbase"]]; export const visibilityControlsTypes: string[][] = [["human"], ["dcs"], ["aircraft"], ["helicopter"], ["groundunit-sam", "groundunit-sam-radar", "groundunit-sam-launcher"], ["groundunit-other", "groundunit-ewr"], ["navyunit"], ["airbase"]];
export const visibilityControlsTooltips: string[] = ["Toggle human players visibility", "Toggle DCS controlled units visibility", "Toggle aircrafts visibility", "Toggle helicopter visibility", "Toggle SAM units visibility", "Toggle ground units (not SAM) visibility", "Toggle navy units visibility", "Toggle airbases visibility"]; export const visibilityControlsTooltips: string[] = ["Toggle human players visibility", "Toggle DCS controlled units visibility", "Toggle aircrafts visibility", "Toggle helicopter visibility", "Toggle SAM units visibility", "Toggle ground units (not SAM) visibility", "Toggle navy units visibility", "Toggle airbases visibility"];
export const MAP_MARKER_CONTROLS:MapMarkerControl[] = [{ export const MAP_MARKER_CONTROLS: MapMarkerControl[] = [{
"name":"Human", "name": "Human",
"image": "visibility/human.svg", "image": "visibility/human.svg",
"toggles": [ "human" ], "toggles": ["human"],
"tooltip": "Toggle human players' visibility" "tooltip": "Toggle human players' visibility"
}, { }, {
"image": "visibility/dcs.svg", "image": "visibility/dcs.svg",
"isProtected": true, "isProtected": true,
"name":"DCS", "name": "DCS",
"protectable": true, "protectable": true,
"toggles": [ "dcs" ], "toggles": ["dcs"],
"tooltip": "Toggle DCS-controlled units' visibility" "tooltip": "Toggle DCS-controlled units' visibility"
}, { }, {
"image": "visibility/aircraft.svg", "image": "visibility/aircraft.svg",
"name":"Aircraft", "name": "Aircraft",
"toggles": [ "aircraft" ], "toggles": ["aircraft"],
"tooltip": "Toggle aircraft's visibility" "tooltip": "Toggle aircraft's visibility"
}, { }, {
"image": "visibility/helicopter.svg", "image": "visibility/helicopter.svg",
"name":"Helicopter", "name": "Helicopter",
"toggles": [ "helicopter" ], "toggles": ["helicopter"],
"tooltip": "Toggle helicopters' visibility" "tooltip": "Toggle helicopters' visibility"
}, { }, {
"image": "visibility/groundunit-sam.svg", "image": "visibility/groundunit-sam.svg",
"name":"Air defence", "name": "Air defence",
"toggles": [ "groundunit-sam" ], "toggles": ["groundunit-sam"],
"tooltip": "Toggle air defence units' visibility" "tooltip": "Toggle air defence units' visibility"
}, { }, {
"image": "visibility/groundunit-other.svg", "image": "visibility/groundunit-other.svg",
"name":"Ground units", "name": "Ground units",
"toggles": [ "groundunit-other" ], "toggles": ["groundunit-other"],
"tooltip": "Toggle ground units' visibility" "tooltip": "Toggle ground units' visibility"
}, { }, {
"image": "visibility/navyunit.svg", "image": "visibility/navyunit.svg",
"name":"Naval", "name": "Naval",
"toggles": [ "navyunit" ], "toggles": ["navyunit"],
"tooltip": "Toggle naval units' visibility" "tooltip": "Toggle naval units' visibility"
}, { }, {
"image": "visibility/airbase.svg", "image": "visibility/airbase.svg",
"name":"Airbase", "name": "Airbase",
"toggles": [ "airbase" ], "toggles": ["airbase"],
"tooltip": "Toggle airbase' visibility" "tooltip": "Toggle airbase' visibility"
}]; }];
export const IADSTypes = ["AAA", "MANPADS", "SAM Site", "Radar"]; export const IADSTypes = ["AAA", "MANPADS", "SAM Site", "Radar"];
export const IADSDensities: {[key: string]: number}= {"AAA": 0.8, "MANPADS": 0.3, "SAM Site": 0.1, "Radar": 0.05}; export const IADSDensities: { [key: string]: number } = { "AAA": 0.8, "MANPADS": 0.3, "SAM Site": 0.1, "Radar": 0.05 };
export const HIDE_GROUP_MEMBERS = "Hide group members when zoomed out"; export const HIDE_GROUP_MEMBERS = "Hide group members when zoomed out";
export const SHOW_UNIT_LABELS = "Show unit labels (L)"; export const SHOW_UNIT_LABELS = "Show unit labels (L)";
export const SHOW_UNITS_ENGAGEMENT_RINGS = "Show units threat range rings (Q)"; export const SHOW_UNITS_ENGAGEMENT_RINGS = "Show units threat range rings (Q)";
export const HIDE_UNITS_SHORT_RANGE_RINGS = "Hide short range units threat range rings (R)"; export const HIDE_UNITS_SHORT_RANGE_RINGS = "Hide short range units threat range rings (R)";
export const SHOW_UNITS_ACQUISITION_RINGS = "Show units detection range rings (E)"; export const SHOW_UNITS_ACQUISITION_RINGS = "Show units detection range rings (E)";
export const FILL_SELECTED_RING = "Fill the threat range rings of selected units (F)"; export const FILL_SELECTED_RING = "Fill the threat range rings of selected units (F)";
export const SHOW_UNIT_CONTACTS = "Show selected units contact lines"; export const SHOW_UNIT_CONTACTS = "Show selected units contact lines";
export const SHOW_UNIT_PATHS = "Show selected unit paths"; export const SHOW_UNIT_PATHS = "Show selected unit paths";
export const SHOW_UNIT_TARGETS = "Show selected unit targets"; export const SHOW_UNIT_TARGETS = "Show selected unit targets";
export enum DataIndexes { export enum DataIndexes {
startOfData = 0, startOfData = 0,
@@ -267,12 +267,12 @@ export enum DataIndexes {
}; };
export const MGRS_PRECISION_10KM = 2; export const MGRS_PRECISION_10KM = 2;
export const MGRS_PRECISION_1KM = 3; export const MGRS_PRECISION_1KM = 3;
export const MGRS_PRECISION_100M = 4; export const MGRS_PRECISION_100M = 4;
export const MGRS_PRECISION_10M = 5; export const MGRS_PRECISION_10M = 5;
export const MGRS_PRECISION_1M = 6; export const MGRS_PRECISION_1M = 6;
export const DELETE_CYCLE_TIME = 0.05; export const DELETE_CYCLE_TIME = 0.05;
export const DELETE_SLOW_THRESHOLD = 50; export const DELETE_SLOW_THRESHOLD = 50;
export const GROUPING_ZOOM_TRANSITION = 13; export const GROUPING_ZOOM_TRANSITION = 13;

View File

@@ -24,7 +24,7 @@ export class AirbaseContextMenu extends ContextMenu {
document.addEventListener("contextMenuLandAirbase", (e: any) => { document.addEventListener("contextMenuLandAirbase", (e: any) => {
if (this.#airbase) if (this.#airbase)
getApp().getUnitsManager().selectedUnitsLandAt(this.#airbase.getLatLng()); getApp().getUnitsManager().landAt(this.#airbase.getLatLng());
this.hide(); this.hide();
}) })
} }
@@ -111,7 +111,7 @@ export class AirbaseContextMenu extends ContextMenu {
#showSpawnMenu() { #showSpawnMenu() {
if (this.#airbase != null) { if (this.#airbase != null) {
getApp().setActiveCoalition(this.#airbase.getCoalition()); getApp().setActiveCoalition(this.#airbase.getCoalition());
getApp().getMap().showAirbaseSpawnMenu(this.getX(), this.getY(), this.getLatLng(), this.#airbase); getApp().getMap().showAirbaseSpawnMenu(this.#airbase, this.getX(), this.getY(), this.getLatLng());
} }
} }

View File

@@ -46,7 +46,7 @@ export class AirbaseSpawnContextMenu extends ContextMenu {
* @param x X screen coordinate of the top left corner of the context menu * @param x X screen coordinate of the top left corner of the context menu
* @param y Y screen coordinate of the top left corner of the context menu * @param y Y screen coordinate of the top left corner of the context menu
*/ */
show(x: number, y: number) { show(x: number | undefined, y: number | undefined) {
super.show(x, y, new LatLng(0, 0)); super.show(x, y, new LatLng(0, 0));
this.#aircraftSpawnMenu.setAirbase(undefined); this.#aircraftSpawnMenu.setAirbase(undefined);

View File

@@ -19,15 +19,15 @@ export class ContextMenu {
/** Show the contextmenu on top of the map, usually at the location where the user has clicked on it. /** Show the contextmenu on top of the map, usually at the location where the user has clicked on it.
* *
* @param x X screen coordinate of the top left corner of the context menu * @param x X screen coordinate of the top left corner of the context menu. If undefined, use the old value
* @param y Y screen coordinate of the top left corner of the context menu * @param y Y screen coordinate of the top left corner of the context menu. If undefined, use the old value
* @param latlng Leaflet latlng object of the mouse click * @param latlng Leaflet latlng object of the mouse click. If undefined, use the old value
*/ */
show(x: number, y: number, latlng: LatLng) { show(x: number | undefined = undefined, y: number | undefined = undefined, latlng: LatLng | undefined = undefined) {
this.#latlng = latlng; this.#latlng = latlng ?? this.#latlng;
this.#container?.classList.toggle("hide", false); this.#container?.classList.toggle("hide", false);
this.#x = x; this.#x = x ?? this.#x;
this.#y = y; this.#y = y ?? this.#y;
this.clip(); this.clip();
this.getContainer()?.dispatchEvent(new Event("show")); this.getContainer()?.dispatchEvent(new Event("show"));
} }

View File

@@ -1,4 +1,4 @@
import { deg2rad, ftToM } from "../other/utils"; import { ContextActionSet } from "../unit/contextactionset";
import { ContextMenu } from "./contextmenu"; import { ContextMenu } from "./contextmenu";
/** The UnitContextMenu is shown when the user rightclicks on a unit. It dynamically presents the user with possible actions to perform on the unit. */ /** The UnitContextMenu is shown when the user rightclicks on a unit. It dynamically presents the user with possible actions to perform on the unit. */
@@ -16,15 +16,19 @@ export class UnitContextMenu extends ContextMenu {
* @param options Dictionary element containing the text and tooltip of the options shown in the menu * @param options Dictionary element containing the text and tooltip of the options shown in the menu
* @param callback Callback that will be called when the user clicks on one of the options * @param callback Callback that will be called when the user clicks on one of the options
*/ */
setOptions(options: { [key: string]: {text: string, tooltip: string }}, callback: CallableFunction) { setContextActions(contextActionSet: ContextActionSet) {
this.getContainer()?.replaceChildren(...Object.keys(options).map((key: string, idx: number) => { this.getContainer()?.replaceChildren(...Object.keys(contextActionSet.getContextActions()).map((key: string, idx: number) => {
const option = options[key]; const contextAction = contextActionSet.getContextActions()[key];
var button = document.createElement("button"); var button = document.createElement("button");
var el = document.createElement("div"); var el = document.createElement("div");
el.title = option.tooltip; el.title = contextAction.getDescription();
el.innerText = option.text; el.innerText = contextAction.getLabel();
el.id = key; el.id = key;
button.addEventListener("click", () => callback(key)); button.addEventListener("click", () => {
contextAction.executeCallback();
if (contextAction.getHideContextAfterExecution())
this.hide();
});
button.appendChild(el); button.appendChild(el);
return (button); return (button);
})); }));

View File

@@ -12,16 +12,16 @@ import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker";
import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker";
import { ClickableMiniMap } from "./clickableminimap"; import { ClickableMiniMap } from "./clickableminimap";
import { SVGInjector } from '@tanem/svg-injector' import { SVGInjector } from '@tanem/svg-injector'
import { mapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, visibilityControls, visibilityControlsTooltips, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, visibilityControlsTypes, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS } from "../constants/constants"; import { mapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS } from "../constants/constants";
import { TargetMarker } from "./markers/targetmarker"; import { TargetMarker } from "./markers/targetmarker";
import { CoalitionArea } from "./coalitionarea/coalitionarea"; import { CoalitionArea } from "./coalitionarea/coalitionarea";
import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu"; import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu";
import { DrawingCursor } from "./coalitionarea/drawingcursor"; import { DrawingCursor } from "./coalitionarea/drawingcursor";
import { AirbaseSpawnContextMenu } from "../contextmenus/airbasespawnmenu"; import { AirbaseSpawnContextMenu } from "../contextmenus/airbasespawnmenu";
import { Popup } from "../popups/popup";
import { GestureHandling } from "leaflet-gesture-handling"; import { GestureHandling } from "leaflet-gesture-handling";
import { TouchBoxSelect } from "./touchboxselect"; import { TouchBoxSelect } from "./touchboxselect";
import { DestinationPreviewHandle } from "./markers/destinationpreviewHandle"; import { DestinationPreviewHandle } from "./markers/destinationpreviewHandle";
import { ContextActionSet } from "../unit/contextactionset";
var hasTouchScreen = false; var hasTouchScreen = false;
//if ("maxTouchPoints" in navigator) //if ("maxTouchPoints" in navigator)
@@ -322,7 +322,7 @@ export class Map extends L.Map {
return this.#mapContextMenu; return this.#mapContextMenu;
} }
showUnitContextMenu(x: number, y: number, latlng: L.LatLng) { showUnitContextMenu(x: number | undefined = undefined, y: number | undefined = undefined, latlng: L.LatLng | undefined = undefined) {
this.hideAllContextMenus(); this.hideAllContextMenus();
this.#unitContextMenu.show(x, y, latlng); this.#unitContextMenu.show(x, y, latlng);
} }
@@ -335,7 +335,7 @@ export class Map extends L.Map {
this.#unitContextMenu.hide(); this.#unitContextMenu.hide();
} }
showAirbaseContextMenu(x: number, y: number, latlng: L.LatLng, airbase: Airbase) { showAirbaseContextMenu(airbase: Airbase, x: number | undefined = undefined, y: number | undefined = undefined, latlng: L.LatLng | undefined = undefined) {
this.hideAllContextMenus(); this.hideAllContextMenus();
this.#airbaseContextMenu.show(x, y, latlng); this.#airbaseContextMenu.show(x, y, latlng);
this.#airbaseContextMenu.setAirbase(airbase); this.#airbaseContextMenu.setAirbase(airbase);
@@ -349,7 +349,7 @@ export class Map extends L.Map {
this.#airbaseContextMenu.hide(); this.#airbaseContextMenu.hide();
} }
showAirbaseSpawnMenu(x: number, y: number, latlng: L.LatLng, airbase: Airbase) { showAirbaseSpawnMenu(airbase: Airbase, x: number | undefined = undefined, y: number | undefined = undefined, latlng: L.LatLng | undefined = undefined) {
this.hideAllContextMenus(); this.hideAllContextMenus();
this.#airbaseSpawnMenu.show(x, y); this.#airbaseSpawnMenu.show(x, y);
this.#airbaseSpawnMenu.setAirbase(airbase); this.#airbaseSpawnMenu.setAirbase(airbase);
@@ -561,9 +561,9 @@ export class Map extends L.Map {
} }
else if (this.#state === MOVE_UNIT) { else if (this.#state === MOVE_UNIT) {
if (!e.originalEvent.ctrlKey) { if (!e.originalEvent.ctrlKey) {
getApp().getUnitsManager().selectedUnitsClearDestinations(); getApp().getUnitsManager().clearDestinations();
} }
getApp().getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, this.#shiftKey, this.#destinationGroupRotation) getApp().getUnitsManager().addDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, this.#shiftKey, this.#destinationGroupRotation)
this.#destinationGroupRotation = 0; this.#destinationGroupRotation = 0;
this.#destinationRotationCenter = null; this.#destinationRotationCenter = null;
@@ -611,59 +611,15 @@ export class Map extends L.Map {
if (e.originalEvent.button != 2 || e.originalEvent.ctrlKey || e.originalEvent.shiftKey) if (e.originalEvent.button != 2 || e.originalEvent.ctrlKey || e.originalEvent.shiftKey)
return; return;
var options: { [key: string]: { text: string, tooltip: string } } = {}; var contextActionSet = new ContextActionSet();
const selectedUnits = getApp().getUnitsManager().getSelectedUnits(); var units = getApp().getUnitsManager().getSelectedUnits();
const selectedUnitTypes = getApp().getUnitsManager().getSelectedUnitsCategories(); units.forEach((unit: Unit) => {
unit.appendContextActions(contextActionSet, null, e.latlng);
if (selectedUnitTypes.length === 1 && ["Aircraft", "Helicopter"].includes(selectedUnitTypes[0])) { })
if (selectedUnits.every((unit: Unit) => { return unit.canLandAtPoint()}))
options["land-at-point"] = { text: "Land here", tooltip: "Land at this precise location" }; if (Object.keys(contextActionSet.getContextActions()).length > 0) {
getApp().getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng);
if (selectedUnits.every((unit: Unit) => { return unit.canTargetPoint()})) { getApp().getMap().getUnitContextMenu().setContextActions(contextActionSet);
options["bomb"] = { text: "Precision bombing", tooltip: "Precision bombing of a specific point" };
options["carpet-bomb"] = { text: "Carpet bombing", tooltip: "Carpet bombing close to a point" };
}
if (Object.keys(options).length === 0)
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Selected units can not perform point actions.`);
}
else if (selectedUnitTypes.length === 1 && ["GroundUnit", "NavyUnit"].includes(selectedUnitTypes[0])) {
if (selectedUnits.every((unit: Unit) => { return unit.canTargetPoint() })) {
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.`);
}
else if(selectedUnitTypes.length > 1) {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Multiple unit types selected, no common actions available.`);
}
if (Object.keys(options).length > 0) {
this.showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng);
this.getUnitContextMenu().setOptions(options, (option: string) => {
this.hideUnitContextMenu();
if (option === "bomb") {
getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE);
getApp().getUnitsManager().selectedUnitsBombPoint(this.getMouseCoordinates());
}
else if (option === "carpet-bomb") {
getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE);
getApp().getUnitsManager().selectedUnitsCarpetBomb(this.getMouseCoordinates());
}
else if (option === "fire-at-area") {
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());
}
else if (option === "land-at-point") {
getApp().getUnitsManager().getSelectedUnits().length > 0 ? this.setState(MOVE_UNIT) : this.setState(IDLE);
getApp().getUnitsManager().selectedUnitsLandAtPoint(this.getMouseCoordinates());
}
});
} }
}, 150); }, 150);
this.#longPressHandled = false; this.#longPressHandled = false;
@@ -742,6 +698,8 @@ export class Map extends L.Map {
const toggles = `["${control.toggles.join('","')}"]`; const toggles = `["${control.toggles.join('","')}"]`;
const div = document.createElement("div"); const div = document.createElement("div");
div.className = control.protectable === true ? "protectable" : ""; div.className = control.protectable === true ? "protectable" : "";
// TODO: for consistency let's avoid using innerHTML. Let's create elements.
div.innerHTML = ` div.innerHTML = `
<button data-on-click="toggleMarkerVisibility" title="${control.tooltip}" data-on-click-params='{"types":${toggles}}'> <button data-on-click="toggleMarkerVisibility" title="${control.tooltip}" data-on-click-params='{"types":${toggles}}'>
<img src="/resources/theme/images/buttons/${control.image}" /> <img src="/resources/theme/images/buttons/${control.image}" />
@@ -842,7 +800,7 @@ export class Map extends L.Map {
if (this.#destinationPreviewCursors.length == 1) if (this.#destinationPreviewCursors.length == 1)
this.#destinationPreviewCursors[0].setLatLng(this.getMouseCoordinates()); this.#destinationPreviewCursors[0].setLatLng(this.getMouseCoordinates());
else { else {
Object.values(getApp().getUnitsManager().selectedUnitsComputeGroupDestination(groupLatLng, this.#destinationGroupRotation)).forEach((latlng: L.LatLng, idx: number) => { Object.values(getApp().getUnitsManager().computeGroupDestination(groupLatLng, this.#destinationGroupRotation)).forEach((latlng: L.LatLng, idx: number) => {
if (idx < this.#destinationPreviewCursors.length) if (idx < this.#destinationPreviewCursors.length)
this.#destinationPreviewCursors[idx].setLatLng(this.#shiftKey ? latlng : this.getMouseCoordinates()); this.#destinationPreviewCursors[idx].setLatLng(this.#shiftKey ? latlng : this.getMouseCoordinates());
}) })

View File

@@ -242,7 +242,7 @@ export class MissionManager {
} }
#onAirbaseClick(e: any) { #onAirbaseClick(e: any) {
getApp().getMap().showAirbaseContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng, e.sourceTarget); getApp().getMap().showAirbaseContextMenu(e.sourceTarget, e.originalEvent.x, e.originalEvent.y, e.latlng);
} }
#loadAirbaseChartData(callsign: string) { #loadAirbaseChartData(callsign: string) {

View File

@@ -380,9 +380,9 @@ export class OlympusApp {
if (ev.ctrlKey && ev.shiftKey) if (ev.ctrlKey && ev.shiftKey)
this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false); // "Select hotgroup X in addition to any units already selected" this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5)), false); // "Select hotgroup X in addition to any units already selected"
else if (ev.ctrlKey && !ev.shiftKey) else if (ev.ctrlKey && !ev.shiftKey)
this.getUnitsManager().selectedUnitsSetHotgroup(parseInt(ev.code.substring(5))); // "These selected units are hotgroup X (forget any previous membership)" this.getUnitsManager().setHotgroup(parseInt(ev.code.substring(5))); // "These selected units are hotgroup X (forget any previous membership)"
else if (!ev.ctrlKey && ev.shiftKey) else if (!ev.ctrlKey && ev.shiftKey)
this.getUnitsManager().selectedUnitsAddToHotgroup(parseInt(ev.code.substring(5))); // "Add (append) these units to hotgroup X (in addition to any existing members)" this.getUnitsManager().addToHotgroup(parseInt(ev.code.substring(5))); // "Add (append) these units to hotgroup X (in addition to any existing members)"
else else
this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it." this.getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5))); // "Select hotgroup X, deselect any units not in it."
}, },

View File

@@ -10,6 +10,8 @@ import { ROEDescriptions, ROEs, altitudeIncrements, emissionsCountermeasures, em
import { ftToM, knotsToMs, mToFt, msToKnots } from "../other/utils"; import { ftToM, knotsToMs, mToFt, msToKnots } from "../other/utils";
import { GeneralSettings, Radio, TACAN } from "../interfaces"; import { GeneralSettings, Radio, TACAN } from "../interfaces";
import { PrimaryToolbar } from "../toolbars/primarytoolbar"; import { PrimaryToolbar } from "../toolbars/primarytoolbar";
import { ContextActionSet } from "../unit/contextactionset";
import { ContextAction } from "../unit/contextaction";
export class UnitControlPanel extends Panel { export class UnitControlPanel extends Panel {
#altitudeSlider: Slider; #altitudeSlider: Slider;
@@ -38,32 +40,32 @@ export class UnitControlPanel extends Panel {
super(ID); super(ID);
/* Unit control sliders */ /* Unit control sliders */
this.#altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => { getApp().getUnitsManager().selectedUnitsSetAltitude(ftToM(value)); }); this.#altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => { getApp().getUnitsManager().setAltitude(ftToM(value)); });
this.#altitudeTypeSwitch = new Switch("altitude-type-switch", (value: boolean) => { getApp().getUnitsManager().selectedUnitsSetAltitudeType(value? "ASL": "AGL"); }); this.#altitudeTypeSwitch = new Switch("altitude-type-switch", (value: boolean) => { getApp().getUnitsManager().setAltitudeType(value? "ASL": "AGL"); });
this.#speedSlider = new Slider("speed-slider", 0, 100, "kts", (value: number) => { getApp().getUnitsManager().selectedUnitsSetSpeed(knotsToMs(value)); }); this.#speedSlider = new Slider("speed-slider", 0, 100, "kts", (value: number) => { getApp().getUnitsManager().setSpeed(knotsToMs(value)); });
this.#speedTypeSwitch = new Switch("speed-type-switch", (value: boolean) => { getApp().getUnitsManager().selectedUnitsSetSpeedType(value? "CAS": "GS"); }); this.#speedTypeSwitch = new Switch("speed-type-switch", (value: boolean) => { getApp().getUnitsManager().setSpeedType(value? "CAS": "GS"); });
/* Option buttons */ /* Option buttons */
// Reversing the ROEs so that the least "aggressive" option is always on the left // Reversing the ROEs so that the least "aggressive" option is always on the left
this.#optionButtons["ROE"] = ROEs.slice(0).reverse().map((option: string, index: number) => { this.#optionButtons["ROE"] = ROEs.slice(0).reverse().map((option: string, index: number) => {
return this.#createOptionButton(option, `roe/${option.toLowerCase()}.svg`, ROEDescriptions.slice(0).reverse()[index], () => { getApp().getUnitsManager().selectedUnitsSetROE(option); }); return this.#createOptionButton(option, `roe/${option.toLowerCase()}.svg`, ROEDescriptions.slice(0).reverse()[index], () => { getApp().getUnitsManager().setROE(option); });
}).filter((button: HTMLButtonElement, index: number) => {return ROEs[index] !== "";}); }).filter((button: HTMLButtonElement, index: number) => {return ROEs[index] !== "";});
this.#optionButtons["reactionToThreat"] = reactionsToThreat.map((option: string, index: number) => { this.#optionButtons["reactionToThreat"] = reactionsToThreat.map((option: string, index: number) => {
return this.#createOptionButton(option, `threat/${option.toLowerCase()}.svg`, reactionsToThreatDescriptions[index],() => { getApp().getUnitsManager().selectedUnitsSetReactionToThreat(option); }); return this.#createOptionButton(option, `threat/${option.toLowerCase()}.svg`, reactionsToThreatDescriptions[index],() => { getApp().getUnitsManager().setReactionToThreat(option); });
}); });
this.#optionButtons["emissionsCountermeasures"] = emissionsCountermeasures.map((option: string, index: number) => { this.#optionButtons["emissionsCountermeasures"] = emissionsCountermeasures.map((option: string, index: number) => {
return this.#createOptionButton(option, `emissions/${option.toLowerCase()}.svg`, emissionsCountermeasuresDescriptions[index],() => { getApp().getUnitsManager().selectedUnitsSetEmissionsCountermeasures(option); }); return this.#createOptionButton(option, `emissions/${option.toLowerCase()}.svg`, emissionsCountermeasuresDescriptions[index],() => { getApp().getUnitsManager().setEmissionsCountermeasures(option); });
}); });
this.#optionButtons["shotsScatter"] = [1, 2, 3].map((option: number, index: number) => { this.#optionButtons["shotsScatter"] = [1, 2, 3].map((option: number, index: number) => {
return this.#createOptionButton(option.toString(), `scatter/${option.toString().toLowerCase()}.svg`, shotsScatterDescriptions[index],() => { getApp().getUnitsManager().selectedUnitsSetShotsScatter(option); }); return this.#createOptionButton(option.toString(), `scatter/${option.toString().toLowerCase()}.svg`, shotsScatterDescriptions[index],() => { getApp().getUnitsManager().setShotsScatter(option); });
}); });
this.#optionButtons["shotsIntensity"] = [1, 2, 3].map((option: number, index: number) => { this.#optionButtons["shotsIntensity"] = [1, 2, 3].map((option: number, index: number) => {
return this.#createOptionButton(option.toString(), `intensity/${option.toString().toLowerCase()}.svg`, shotsIntensityDescriptions[index],() => { getApp().getUnitsManager().selectedUnitsSetShotsIntensity(option); }); return this.#createOptionButton(option.toString(), `intensity/${option.toString().toLowerCase()}.svg`, shotsIntensityDescriptions[index],() => { getApp().getUnitsManager().setShotsIntensity(option); });
}); });
this.getElement().querySelector("#roe-buttons-container")?.append(...this.#optionButtons["ROE"]); this.getElement().querySelector("#roe-buttons-container")?.append(...this.#optionButtons["ROE"]);
@@ -92,17 +94,17 @@ export class UnitControlPanel extends Panel {
/* On off switch */ /* On off switch */
this.#onOffSwitch = new Switch("on-off-switch", (value: boolean) => { this.#onOffSwitch = new Switch("on-off-switch", (value: boolean) => {
getApp().getUnitsManager().selectedUnitsSetOnOff(value); getApp().getUnitsManager().setOnOff(value);
}); });
/* Follow roads switch */ /* Follow roads switch */
this.#followRoadsSwitch = new Switch("follow-roads-switch", (value: boolean) => { this.#followRoadsSwitch = new Switch("follow-roads-switch", (value: boolean) => {
getApp().getUnitsManager().selectedUnitsSetFollowRoads(value); getApp().getUnitsManager().setFollowRoads(value);
}); });
/* Operate as */ /* Operate as */
this.#operateAsSwitch = new Switch("operate-as-switch", (value: boolean) => { this.#operateAsSwitch = new Switch("operate-as-switch", (value: boolean) => {
getApp().getUnitsManager().selectedUnitsSetOperateAs(value); getApp().getUnitsManager().setOperateAs(value);
}); });
/* Advanced settings dialog */ /* Advanced settings dialog */
@@ -300,54 +302,32 @@ export class UnitControlPanel extends Panel {
} }
#updateRapidControls() { #updateRapidControls() {
var options: { [key: string]: { text: string, tooltip: string, type: string } } | null = null; var contextActionSet = new ContextActionSet();
var units = getApp().getUnitsManager().getSelectedUnits();
var selectedUnits = getApp().getUnitsManager().getSelectedUnits(); var showAltitudeChange = units.some((unit: Unit) => {return ["Aircraft", "Helicopter"].includes(unit.getCategory());});
var showAltitudeChange = selectedUnits.some((unit: Unit) => {return ["Aircraft", "Helicopter"].includes(unit.getCategory());});
this.getElement().querySelector("#climb")?.classList.toggle("hide", !showAltitudeChange); this.getElement().querySelector("#climb")?.classList.toggle("hide", !showAltitudeChange);
this.getElement().querySelector("#descend")?.classList.toggle("hide", !showAltitudeChange); this.getElement().querySelector("#descend")?.classList.toggle("hide", !showAltitudeChange);
/* Keep only the common "and" options, unless a single unit is selected */ units.forEach((unit: Unit) => {
selectedUnits.forEach((unit: Unit) => { unit.appendContextActions(contextActionSet, null, null);
var unitOptions = unit.getActions(); })
if (options === null) {
options = unitOptions;
} else {
/* Delete all the "or" type options */
for (let optionKey in options) {
if (options[optionKey].type == "or") {
delete options[optionKey];
}
}
/* Options of "and" type get shown if ALL units have it */
for (let optionKey in options) {
if (!(optionKey in unitOptions)) {
delete options[optionKey];
}
}
}
});
options = options ?? {};
const rapidControlsContainer = this.getElement().querySelector("#rapid-controls") as HTMLElement; const rapidControlsContainer = this.getElement().querySelector("#rapid-controls") as HTMLElement;
const unitActionButtons = rapidControlsContainer.querySelectorAll(".unit-action-button"); const unitActionButtons = rapidControlsContainer.querySelectorAll(".unit-action-button");
for (let button of unitActionButtons) { for (let button of unitActionButtons) {
rapidControlsContainer.removeChild(button); rapidControlsContainer.removeChild(button);
} }
for (let option in options) { for (let key in contextActionSet.getContextActions()) {
const contextAction = contextActionSet.getContextActions()[key];
let button = document.createElement("button"); let button = document.createElement("button");
button.title = options[option].tooltip; button.title = contextAction.getDescription();
button.classList.add("ol-button", "unit-action-button"); button.classList.add("ol-button", "unit-action-button");
button.id = option; button.id = key;
rapidControlsContainer.appendChild(button); rapidControlsContainer.appendChild(button);
button.onclick = () => { button.onclick = () => {
/* Since only common actions are shown in the rapid controls, we execute it only on the first unit */ contextAction.executeCallback();
if (selectedUnits.length > 0)
selectedUnits[0].executeAction(null, option);
} }
} }
} }

View File

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

View File

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

View File

@@ -13,6 +13,8 @@ import { Weapon } from '../weapon/weapon';
import { Ammo, Contact, GeneralSettings, LoadoutBlueprint, ObjectIconOptions, Offset, Radio, TACAN, UnitData } from '../interfaces'; import { Ammo, Contact, GeneralSettings, LoadoutBlueprint, ObjectIconOptions, Offset, Radio, TACAN, UnitData } from '../interfaces';
import { RangeCircle } from "../map/rangecircle"; import { RangeCircle } from "../map/rangecircle";
import { Group } from './group'; import { Group } from './group';
import { ContextActionSet } from './contextactionset';
import { ContextAction } from './contextaction';
var pathIcon = new Icon({ var pathIcon = new Icon({
iconUrl: '/resources/theme/images/markers/marker-icon.png', iconUrl: '/resources/theme/images/markers/marker-icon.png',
@@ -212,7 +214,7 @@ export abstract class Unit extends CustomMarker {
}); });
} }
/********************** Abstract methods *************************/ /********************** Abstract methods *************************/
/** Get the unit category string /** Get the unit category string
* *
* @returns string The unit category * @returns string The unit category
@@ -228,9 +230,8 @@ export abstract class Unit extends CustomMarker {
/** Get the actions that this unit can perform /** Get the actions that this unit can perform
* *
* @returns Object containing the available actions
*/ */
abstract getActions(): {[key: string]: { text: string, tooltip: string, type: string}}; abstract appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null): void;
/********************** Unit data *************************/ /********************** Unit data *************************/
/** This function is called by the units manager to update all the data coming from the backend. It reads the binary raw data using a DataExtractor /** This function is called by the units manager to update all the data coming from the backend. It reads the binary raw data using a DataExtractor
@@ -240,7 +241,7 @@ export abstract class Unit extends CustomMarker {
setData(dataExtractor: DataExtractor) { setData(dataExtractor: DataExtractor) {
/* This variable controls if the marker must be updated. This is not always true since not all variables have an effect on the marker */ /* This variable controls if the marker must be updated. This is not always true since not all variables have an effect on the marker */
var updateMarker = !getApp().getMap().hasLayer(this); var updateMarker = !getApp().getMap().hasLayer(this);
var oldIsLeader = this.#isLeader; var oldIsLeader = this.#isLeader;
var datumIndex = 0; var datumIndex = 0;
while (datumIndex != DataIndexes.endOfData) { while (datumIndex != DataIndexes.endOfData) {
@@ -489,7 +490,7 @@ export abstract class Unit extends CustomMarker {
* @returns Unit[] * @returns Unit[]
*/ */
getGroupMembers() { getGroupMembers() {
if (this.#group !== null) if (this.#group !== null)
return this.#group.getMembers().filter((unit: Unit) => { return unit != this; }) return this.#group.getMembers().filter((unit: Unit) => { return unit != this; })
return []; return [];
} }
@@ -499,7 +500,7 @@ export abstract class Unit extends CustomMarker {
* @returns Unit The leader of the group * @returns Unit The leader of the group
*/ */
getGroupLeader() { getGroupLeader() {
if (this.#group !== null) if (this.#group !== null)
return this.#group.getLeader(); return this.#group.getLeader();
return null; return null;
} }
@@ -563,7 +564,7 @@ export abstract class Unit extends CustomMarker {
var iconOptions = this.getIconOptions(); var iconOptions = this.getIconOptions();
/* Generate and append elements depending on active options */ /* Generate and append elements depending on active options */
/* Velocity vector */ /* Velocity vector */
if (iconOptions.showVvi) { if (iconOptions.showVvi) {
var vvi = document.createElement("div"); var vvi = document.createElement("div");
@@ -682,12 +683,13 @@ export abstract class Unit extends CustomMarker {
(!this.belongsToCommandedCoalition() && (this.#detectionMethods.length == 0 || (this.#detectionMethods.length == 1 && this.#detectionMethods[0] === RWR))) || (!this.belongsToCommandedCoalition() && (this.#detectionMethods.length == 0 || (this.#detectionMethods.length == 1 && this.#detectionMethods[0] === RWR))) ||
/* Hide the unit if grouping is activated, the unit is not the group leader, it is not selected, and the zoom is higher than the grouping threshold */ /* Hide the unit if grouping is activated, the unit is not the group leader, it is not selected, and the zoom is higher than the grouping threshold */
(getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] && !this.#isLeader && this.getCategory() == "GroundUnit" && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION && (getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] && !this.#isLeader && this.getCategory() == "GroundUnit" && getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION &&
(this.belongsToCommandedCoalition() || (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0)))) && (this.belongsToCommandedCoalition() || (!this.belongsToCommandedCoalition() && this.#detectionMethods.length == 0))));
!(this.getSelected()
);
/* Force dead units to be hidden */ /* Force dead units to be hidden */
this.setHidden(hidden || !this.#alive); this.setHidden(hidden || !this.getAlive());
/* Force hidden units to be unselected */
this.setSelected(this.getSelected() && !this.getHidden());
} }
setHidden(hidden: boolean) { setHidden(hidden: boolean) {
@@ -767,10 +769,6 @@ export abstract class Unit extends CustomMarker {
return this.getDatabaseEntry()?.canRearm === true; return this.getDatabaseEntry()?.canRearm === true;
} }
canLandAtPoint() {
return this.getCategory() === "Helicopter";
}
canAAA() { canAAA() {
return this.getDatabaseEntry()?.canAAA === true; return this.getDatabaseEntry()?.canAAA === true;
} }
@@ -964,24 +962,6 @@ export abstract class Unit extends CustomMarker {
getApp().getServerManager().setShotsIntensity(this.ID, shotsIntensity); getApp().getServerManager().setShotsIntensity(this.ID, shotsIntensity);
} }
/***********************************************/
executeAction(e: any, action: string) {
if (action === "center-map")
getApp().getMap().centerOnUnit(this.ID);
if (action === "attack")
getApp().getUnitsManager().selectedUnitsAttackUnit(this.ID);
else if (action === "refuel")
getApp().getUnitsManager().selectedUnitsRefuel();
else if (action === "group-ground" || action === "group-navy")
getApp().getUnitsManager().selectedUnitsCreateGroup();
else if (action === "scenic-aaa")
getApp().getUnitsManager().selectedUnitsScenicAAA();
else if (action === "miss-aaa")
getApp().getUnitsManager().selectedUnitsMissOnPurpose();
else if (action === "follow")
this.#showFollowOptions(e);
}
/***********************************************/ /***********************************************/
onAdd(map: Map): this { onAdd(map: Map): this {
super.onAdd(map); super.onAdd(map);
@@ -992,6 +972,56 @@ export abstract class Unit extends CustomMarker {
this.#redrawMarker(); this.#redrawMarker();
} }
showFollowOptions(units: Unit[]) {
var contextActionSet = new ContextActionSet();
contextActionSet.addContextAction(this, 'trail', "Trail", "Follow unit in trail formation", () => this.applyFollowOptions('trail', units));
contextActionSet.addContextAction(this, 'echelon-lh', "Echelon (LH)", "Follow unit in echelon left formation", () => this.applyFollowOptions('echelon-lh', units));
contextActionSet.addContextAction(this, 'echelon-rh', "Echelon (RH)", "Follow unit in echelon right formation", () => this.applyFollowOptions('echelon-rh', units));
contextActionSet.addContextAction(this, 'line-abreast-lh', "Line abreast (LH)", "Follow unit in line abreast left formation", () => this.applyFollowOptions('line-abreast-lh', units));
contextActionSet.addContextAction(this, 'line-abreast-rh', "Line abreast (RH)", "Follow unit in line abreast right formation", () => this.applyFollowOptions('line-abreast-rh', units));
contextActionSet.addContextAction(this, 'front', "Front", "Fly in front of unit", () => this.applyFollowOptions('front', units));
contextActionSet.addContextAction(this, 'diamond', "Diamond", "Follow unit in diamond formation", () => this.applyFollowOptions('diamond', units));
contextActionSet.addContextAction(this, 'custom', "Custom", "Set a custom formation position", () => this.applyFollowOptions('custom', units));
getApp().getMap().getUnitContextMenu().setContextActions(contextActionSet);
getApp().getMap().showUnitContextMenu();
}
applyFollowOptions(formation: string, units: Unit[]) {
if (formation === "custom") {
document.getElementById("custom-formation-dialog")?.classList.remove("hide");
document.addEventListener("applyCustomFormation", () => {
var dialog = document.getElementById("custom-formation-dialog");
if (dialog) {
dialog.classList.add("hide");
var clock = 1;
while (clock < 8) {
if ((<HTMLInputElement>dialog.querySelector(`#formation-${clock}`)).checked)
break
clock++;
}
var angleDeg = 360 - (clock - 1) * 45;
var angleRad = deg2rad(angleDeg);
var distance = ftToM(parseInt((<HTMLInputElement>dialog.querySelector(`#distance`)?.querySelector("input")).value));
var upDown = ftToM(parseInt((<HTMLInputElement>dialog.querySelector(`#up-down`)?.querySelector("input")).value));
// X: front-rear, positive front
// Y: top-bottom, positive top
// Z: left-right, positive right
var x = distance * Math.cos(angleRad);
var y = upDown;
var z = distance * Math.sin(angleRad);
getApp().getUnitsManager().followUnit(this.ID, { "x": x, "y": y, "z": z }, undefined, units);
}
});
}
else {
getApp().getUnitsManager().followUnit(this.ID, undefined, formation, units);
}
}
/***********************************************/ /***********************************************/
#onClick(e: any) { #onClick(e: any) {
/* Exit if we were waiting for a doubleclick */ /* Exit if we were waiting for a doubleclick */
@@ -1030,102 +1060,20 @@ export abstract class Unit extends CustomMarker {
}); });
} }
getActionOptions() { #onContextMenu(e: any) {
var options: { [key: string]: { text: string, tooltip: string, type: string } } | null = null; var contextActionSet = new ContextActionSet();
var units = getApp().getUnitsManager().getSelectedUnits(); var units = getApp().getUnitsManager().getSelectedUnits();
units.push(this); if (!units.includes(this))
units.push(this);
/* Keep only the common "or" options or any "and" option */
units.forEach((unit: Unit) => { units.forEach((unit: Unit) => {
var unitOptions = unit.getActions(); unit.appendContextActions(contextActionSet, this, null);
if (options === null) { })
options = unitOptions;
} else {
/* Options of "or" type get shown if any one unit has it*/
for (let optionKey in unitOptions) {
if (unitOptions[optionKey].type == "or") {
options[optionKey] = unitOptions[optionKey];
}
}
/* Options of "and" type get shown if ALL units have it */ if (Object.keys(contextActionSet.getContextActions()).length > 0) {
for (let optionKey in options) {
if (!(optionKey in unitOptions)) {
delete options[optionKey];
}
}
}
});
return options ?? {};
}
#onContextMenu(e: any) {
var options = this.getActionOptions();
if (Object.keys(options).length > 0) {
getApp().getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng); getApp().getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng);
getApp().getMap().getUnitContextMenu().setOptions(options, (option: string) => { getApp().getMap().getUnitContextMenu().setContextActions(contextActionSet);
getApp().getMap().hideUnitContextMenu();
this.executeAction(e, option);
});
}
}
#showFollowOptions(e: any) {
var options: { [key: string]: { text: string, tooltip: string } } = {};
options = {
'trail': { text: "Trail", tooltip: "Follow unit in trail formation" },
'echelon-lh': { text: "Echelon (LH)", tooltip: "Follow unit in echelon left formation" },
'echelon-rh': { text: "Echelon (RH)", tooltip: "Follow unit in echelon right formation" },
'line-abreast-lh': { text: "Line abreast (LH)", tooltip: "Follow unit in line abreast left formation" },
'line-abreast-rh': { text: "Line abreast (RH)", tooltip: "Follow unit in line abreast right formation" },
'front': { text: "Front", tooltip: "Fly in front of unit" },
'diamond': { text: "Diamond", tooltip: "Follow unit in diamond formation" },
'custom': { text: "Custom", tooltip: "Set a custom formation position" },
}
getApp().getMap().getUnitContextMenu().setOptions(options, (option: string) => {
getApp().getMap().hideUnitContextMenu();
this.#applyFollowOptions(option);
});
getApp().getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng);
}
#applyFollowOptions(action: string) {
if (action === "custom") {
document.getElementById("custom-formation-dialog")?.classList.remove("hide");
document.addEventListener("applyCustomFormation", () => {
var dialog = document.getElementById("custom-formation-dialog");
if (dialog) {
dialog.classList.add("hide");
var clock = 1;
while (clock < 8) {
if ((<HTMLInputElement>dialog.querySelector(`#formation-${clock}`)).checked)
break
clock++;
}
var angleDeg = 360 - (clock - 1) * 45;
var angleRad = deg2rad(angleDeg);
var distance = ftToM(parseInt((<HTMLInputElement>dialog.querySelector(`#distance`)?.querySelector("input")).value));
var upDown = ftToM(parseInt((<HTMLInputElement>dialog.querySelector(`#up-down`)?.querySelector("input")).value));
// X: front-rear, positive front
// Y: top-bottom, positive top
// Z: left-right, positive right
var x = distance * Math.cos(angleRad);
var y = upDown;
var z = distance * Math.sin(angleRad);
getApp().getUnitsManager().selectedUnitsFollowUnit(this.ID, { "x": x, "y": y, "z": z });
}
});
}
else {
getApp().getUnitsManager().selectedUnitsFollowUnit(this.ID, undefined, action);
} }
} }
@@ -1483,7 +1431,7 @@ export abstract class Unit extends CustomMarker {
} }
#onZoom(e: any) { #onZoom(e: any) {
if (this.checkZoomRedraw()) if (this.checkZoomRedraw())
this.#redrawMarker(); this.#redrawMarker();
this.#updateMarker(); this.#updateMarker();
} }
@@ -1507,35 +1455,24 @@ export abstract class AirUnit extends Unit {
}; };
} }
getActions() { appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) {
var options: { [key: string]: { text: string, tooltip: string, type: string } } = {}; if (targetUnit !== null) {
if (targetUnit != this) {
/* Options if this unit is not selected */ contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", (units: Unit[]) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) });
if (!this.getSelected()) { contextActionSet.addContextAction(this, "follow", "Follow unit", "Follow this unit in formation", (units: Unit[]) => { targetUnit.showFollowOptions(units); }, false); // Don't hide the context menu after the execution (to show the follow options)
/* Someone else is selected */ }
if (getApp().getUnitsManager().getSelectedUnits().length > 0) { if (targetUnit.getSelected()) {
options["attack"] = { text: "Attack", tooltip: "Attack the unit using A/A or A/G weapons", type: "or" }; contextActionSet.addContextAction(this, "refuel", "Refuel", "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB", (units: Unit[]) => { getApp().getUnitsManager().refuel(units) });
options["follow"] = { text: "Follow", tooltip: "Follow the unit at a user defined distance and position", type: "or" }; }
} else { if (getApp().getUnitsManager().getSelectedUnits().length == 1 && targetUnit === this) {
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" }; contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", () => { getApp().getMap().centerOnUnit(this.ID); });
} }
} }
/* Options if this unit is selected*/
else if (this.getSelected()) {
/* This is the only selected unit */
if (getApp().getUnitsManager().getSelectedUnits().length == 1) {
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" };
} else {
options["follow"] = { text: "Follow", tooltip: "Follow the unit at a user defined distance and position", type: "or" };
}
options["refuel"] = { text: "Air to air refuel", tooltip: "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB.", type: "and" }; // TODO Add some way of knowing which aircraft can AAR if (targetPosition !== null) {
contextActionSet.addContextAction(this, "bomb", "Precision bombing", "Precision bombing of a specific point", (units: Unit[]) => { getApp().getUnitsManager().bombPoint(targetPosition, units) });
contextActionSet.addContextAction(this, "carpet-bomb", "Carpet bombing", "Carpet bombing close to a point", (units: Unit[]) => { getApp().getUnitsManager().carpetBomb(targetPosition, units) });
} }
/* All other options */
else {
/* Provision */
}
return options;
} }
} }
@@ -1547,6 +1484,14 @@ export class Aircraft extends AirUnit {
getCategory() { getCategory() {
return "Aircraft"; return "Aircraft";
} }
appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) {
super.appendContextActions(contextActionSet, targetUnit, targetPosition);
if (targetPosition === null && this.getSelected()) {
contextActionSet.addContextAction(this, "refuel", "Refuel", "Refuel units at the nearest AAR Tanker. If no tanker is available the unit will RTB", (units: Unit[]) => { getApp().getUnitsManager().refuel(units) });
}
}
} }
export class Helicopter extends AirUnit { export class Helicopter extends AirUnit {
@@ -1557,6 +1502,13 @@ export class Helicopter extends AirUnit {
getCategory() { getCategory() {
return "Helicopter"; return "Helicopter";
} }
appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) {
super.appendContextActions(contextActionSet, targetUnit, targetPosition);
if (targetPosition !== null)
contextActionSet.addContextAction(this, "land-at-point", "Land here", "land at this precise location", (units: Unit[]) => { getApp().getUnitsManager().landAtPoint(targetPosition, units) });
}
} }
export class GroundUnit extends Unit { export class GroundUnit extends Unit {
@@ -1581,37 +1533,31 @@ export class GroundUnit extends Unit {
}; };
} }
getActions() { appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) {
var options: { [key: string]: { text: string, tooltip: string, type: string } } = {}; contextActionSet.addContextAction(this, "group-ground", "Group ground units", "Create a group of ground units", (units: Unit[]) => { getApp().getUnitsManager().createGroup(units) });
/* Options if this unit is not selected */ if (targetUnit !== null) {
if (!this.getSelected()) { if (targetUnit != this) {
/* Someone else is selected */ contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", (units: Unit[]) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) });
if (getApp().getUnitsManager().getSelectedUnits().length > 0) {
options["attack"] = { text: "Attack", tooltip: "Attack the unit using A/A or A/G weapons", type: "or" };
} else {
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" };
}
}
/* Options if this unit is selected*/
else if (this.getSelected()) {
/* This is the only selected unit */
if (getApp().getUnitsManager().getSelectedUnits().length == 1) {
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" };
} else {
options["group-ground"] = { text: "Create group", tooltip: "Create a group from the selected units", type: "and" };
} }
if (this.canAAA()) { if (getApp().getUnitsManager().getSelectedUnits().length == 1 && targetUnit === this) {
options["scenic-aaa"] = { text: "Scenic AAA", tooltip: "Shoot AAA in the air without aiming at any target, when a enemy unit gets close enough. WARNING: works correctly only on neutral units, blue or red units will aim", type: "and" }; contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", () => { getApp().getMap().centerOnUnit(this.ID); });
options["miss-aaa"] = { text: "Miss on purpose AAA", tooltip: "Shoot AAA towards the closest enemy unit, but don't aim precisely. WARNING: works correctly only on neutral units, blue or red units will aim", type: "and" }; }
}
if (targetPosition !== null) {
if (this.canTargetPoint()) {
contextActionSet.addContextAction(this, "fire-at-area", "Fire at area", "Fire at a specific area on the ground", (units: Unit[]) => { getApp().getUnitsManager().fireAtArea(targetPosition, units) });
contextActionSet.addContextAction(this, "simulate-fire-fight", "Simulate fire fight", "Simulate a fire fight by shooting randomly in a certain large area. WARNING: works correctly only on neutral units, blue or red units will aim", (units: Unit[]) => { getApp().getUnitsManager().fireAtArea(targetPosition, units) });
} }
} }
/* All other options */
else { else {
/* Provision */ if (this.canAAA()) {
contextActionSet.addContextAction(this, "scenic-aaa", "Scenic AAA", "Shoot AAA in the air without aiming at any target, when a enemy unit gets close enough. WARNING: works correctly only on neutral units, blue or red units will aim", (units: Unit[]) => { getApp().getUnitsManager().scenicAAA(units) });
contextActionSet.addContextAction(this, "miss-aaa", "Misson on purpose", "Shoot AAA towards the closest enemy unit, but don't aim precisely. WARNING: works correctly only on neutral units, blue or red units will aim", (units: Unit[]) => { getApp().getUnitsManager().missOnPurpose(units) });
}
} }
return options;
} }
getCategory() { getCategory() {
@@ -1671,32 +1617,21 @@ export class NavyUnit extends Unit {
}; };
} }
getActions() { appendContextActions(contextActionSet: ContextActionSet, targetUnit: Unit | null, targetPosition: LatLng | null) {
var options: { [key: string]: { text: string, tooltip: string, type: string } } = {}; contextActionSet.addContextAction(this, "group-navy", "Group navy units", "Create a group of navy units", (units: Unit[]) => { getApp().getUnitsManager().createGroup(units) });
/* Options if this unit is not selected */ if (targetUnit !== null) {
if (!this.getSelected()) { if (targetUnit != this) {
/* Someone else is selected */ contextActionSet.addContextAction(this, "attack", "Attack unit", "Attack the unit using A/A or A/G weapons", (units: Unit[]) => { getApp().getUnitsManager().attackUnit(targetUnit.ID, units) });
if (getApp().getUnitsManager().getSelectedUnits().length > 0) { }
options["attack"] = { text: "Attack", tooltip: "Attack the unit using A/A or A/G weapons", type: "or" }; if (getApp().getUnitsManager().getSelectedUnits().length == 1 && targetUnit === this) {
} else { contextActionSet.addContextAction(this, "center-map", "Center map", "Center the map on the unit and follow it", () => { getApp().getMap().centerOnUnit(this.ID); });
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" };
} }
} }
/* Options if this unit is selected */
else if (this.getSelected()) { if (targetPosition !== null) {
/* This is the only selected unit */ contextActionSet.addContextAction(this, "fire-at-area", "Fire at area", "Fire at a specific area on the ground", (units: Unit[]) => { getApp().getUnitsManager().fireAtArea(targetPosition, units) });
if (getApp().getUnitsManager().getSelectedUnits().length == 1) {
options["center-map"] = { text: "Center map", tooltip: "Center the map on the unit and follow it", type: "and" };
} else {
options["group-navy"] = { text: "Create group", tooltip: "Create a group from the selected units", type: "and" };
}
} }
/* All other options */
else {
/* Provision */
}
return options;
} }
getMarkerCategory() { getMarkerCategory() {

View File

@@ -36,15 +36,15 @@ export class UnitsManager {
document.addEventListener('commandModeOptionsChanged', () => { Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility()) }); document.addEventListener('commandModeOptionsChanged', () => { Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility()) });
document.addEventListener('contactsUpdated', (e: CustomEvent) => { this.#requestDetectionUpdate = true }); document.addEventListener('contactsUpdated', (e: CustomEvent) => { this.#requestDetectionUpdate = true });
document.addEventListener('copy', () => this.selectedUnitsCopy()); document.addEventListener('copy', () => this.copy());
document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete()); document.addEventListener('deleteSelectedUnits', () => this.delete());
document.addEventListener('explodeSelectedUnits', (e: any) => this.selectedUnitsDelete(true, e.detail.type)); document.addEventListener('explodeSelectedUnits', (e: any) => this.delete(true, e.detail.type));
document.addEventListener('exportToFile', () => this.exportToFile()); document.addEventListener('exportToFile', () => this.exportToFile());
document.addEventListener('importFromFile', () => this.importFromFile()); document.addEventListener('importFromFile', () => this.importFromFile());
document.addEventListener('keyup', (event) => this.#onKeyUp(event)); document.addEventListener('keyup', (event) => this.#onKeyUp(event));
document.addEventListener('paste', () => this.pasteUnits()); document.addEventListener('paste', () => this.paste());
document.addEventListener('selectedUnitsChangeAltitude', (e: any) => { this.selectedUnitsChangeAltitude(e.detail.type) }); document.addEventListener('selectedUnitsChangeAltitude', (e: any) => { this.changeAltitude(e.detail.type) });
document.addEventListener('selectedUnitsChangeSpeed', (e: any) => { this.selectedUnitsChangeSpeed(e.detail.type) }); document.addEventListener('selectedUnitsChangeSpeed', (e: any) => { this.changeSpeed(e.detail.type) });
document.addEventListener('unitDeselection', (e: CustomEvent) => this.#onUnitDeselection(e.detail)); document.addEventListener('unitDeselection', (e: CustomEvent) => this.#onUnitDeselection(e.detail));
document.addEventListener('unitSelection', (e: CustomEvent) => this.#onUnitSelection(e.detail)); document.addEventListener('unitSelection', (e: CustomEvent) => this.#onUnitSelection(e.detail));
document.addEventListener("toggleMarkerProtection", (ev: CustomEventInit) => { this.#showNumberOfSelectedProtectedUnits() }); document.addEventListener("toggleMarkerProtection", (ev: CustomEventInit) => { this.#showNumberOfSelectedProtectedUnits() });
@@ -130,8 +130,8 @@ export class UnitsManager {
this.#units[ID]?.setData(dataExtractor); this.#units[ID]?.setData(dataExtractor);
} }
/* Update the unit groups */ /* Update the unit groups */
for (let ID in this.#units) { for (let ID in this.#units) {
const unit = this.#units[ID]; const unit = this.#units[ID];
const groupName = unit.getGroupName(); const groupName = unit.getGroupName();
@@ -141,7 +141,7 @@ export class UnitsManager {
this.#groups[groupName] = new Group(groupName); this.#groups[groupName] = new Group(groupName);
/* If the unit was not assigned to a group yet, assign it */ /* If the unit was not assigned to a group yet, assign it */
if (unit.getGroup() === null) if (unit.getGroup() === null)
this.#groups[groupName].addMember(unit); this.#groups[groupName].addMember(unit);
} }
} }
@@ -260,6 +260,10 @@ export class UnitsManager {
if (options.showProtectionReminder === true && numProtectedUnits > selectedUnits.length && selectedUnits.length === 0) { if (options.showProtectionReminder === true && numProtectedUnits > selectedUnits.length && selectedUnits.length === 0) {
const messageText = (numProtectedUnits === 1) ? `Unit is protected` : `All selected units are protected`; const messageText = (numProtectedUnits === 1) ? `Unit is protected` : `All selected units are protected`;
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(messageText); (getApp().getPopupsManager().get("infoPopup") as Popup).setText(messageText);
// Cheap way for now until we use more locks
let lock = <HTMLElement>document.querySelector("#unit-visibility-control button.lock");
lock.classList.add("prompt");
setTimeout(() => lock.classList.remove("prompt"), 4000);
} }
if (options.onlyOnePerGroup) { if (options.onlyOnePerGroup) {
@@ -350,22 +354,21 @@ export class UnitsManager {
* @param mantainRelativePosition If true, the selected units will mantain their relative positions when reaching the target. This is useful to maintain a formation for groun/navy units * @param mantainRelativePosition If true, the selected units will mantain their relative positions when reaching the target. This is useful to maintain a formation for groun/navy units
* @param rotation Rotation in radians by which the formation will be rigidly rotated. E.g. a ( V ) formation will look like this ( < ) if rotated pi/4 radians (90 degrees) * @param rotation Rotation in radians by which the formation will be rigidly rotated. E.g. a ( V ) formation will look like this ( < ) if rotated pi/4 radians (90 degrees)
*/ */
selectedUnitsAddDestination(latlng: L.LatLng, mantainRelativePosition: boolean, rotation: number) { addDestination(latlng: L.LatLng, mantainRelativePosition: boolean, rotation: number, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (selectedUnits.length === 0) if (units.length === 0)
return; return;
/* Compute the destination for each unit. If mantainRelativePosition is true, compute the destination so to hold the relative positions */ /* Compute the destination for each unit. If mantainRelativePosition is true, compute the destination so to hold the relative positions */
var unitDestinations: { [key: number]: LatLng } = {}; var unitDestinations: { [key: number]: LatLng } = {};
if (mantainRelativePosition) if (mantainRelativePosition)
unitDestinations = this.selectedUnitsComputeGroupDestination(latlng, rotation); unitDestinations = this.computeGroupDestination(latlng, rotation);
else else
selectedUnits.forEach((unit: Unit) => { unitDestinations[unit.ID] = latlng; }); units.forEach((unit: Unit) => { unitDestinations[unit.ID] = latlng; });
for (let idx in selectedUnits) {
const unit = selectedUnits[idx];
units.forEach((unit: Unit) => {
/* If a unit is following another unit, and that unit is also selected, send the command to the followed ("leader") unit */ /* If a unit is following another unit, and that unit is also selected, send the command to the followed ("leader") unit */
if (unit.getState() === "follow") { if (unit.getState() === "follow") {
const leader = this.getUnitByID(unit.getLeaderID()) const leader = this.getUnitByID(unit.getLeaderID())
@@ -378,18 +381,22 @@ export class UnitsManager {
if (unit.ID in unitDestinations) if (unit.ID in unitDestinations)
unit.addDestination(unitDestinations[unit.ID]); unit.addDestination(unitDestinations[unit.ID]);
} }
});
} this.#showActionMessage(units, " new destination added");
this.#showActionMessage(selectedUnits, " new destination added");
} }
/** Clear the destinations of all the selected units /** Clear the destinations of all the selected units
* *
*/ */
selectedUnitsClearDestinations() { clearDestinations(units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: false }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: false });
const unit = selectedUnits[idx];
if (units.length === 0)
return;
for (let idx in units) {
const unit = units[idx];
if (unit.getState() === "follow") { if (unit.getState() === "follow") {
const leader = this.getUnitByID(unit.getLeaderID()) const leader = this.getUnitByID(unit.getLeaderID())
if (leader && leader.getSelected()) if (leader && leader.getSelected())
@@ -406,178 +413,224 @@ export class UnitsManager {
* *
* @param latlng Location where to land at * @param latlng Location where to land at
*/ */
selectedUnitsLandAt(latlng: LatLng) { landAt(latlng: LatLng, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].landAt(latlng);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, " landing"); return;
units.forEach((unit: Unit) => unit.landAt(latlng));
this.#showActionMessage(units, " landing");
} }
/** Instruct all the selected units to change their speed /** Instruct all the selected units to change their speed
* *
* @param speedChange Speed change, either "stop", "slow", or "fast". The specific value depends on the unit category * @param speedChange Speed change, either "stop", "slow", or "fast". The specific value depends on the unit category
*/ */
selectedUnitsChangeSpeed(speedChange: string) { changeSpeed(speedChange: string, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].changeSpeed(speedChange);
} if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.changeSpeed(speedChange));
} }
/** Instruct all the selected units to change their altitude /** Instruct all the selected units to change their altitude
* *
* @param altitudeChange Altitude change, either "climb" or "descend". The specific value depends on the unit category * @param altitudeChange Altitude change, either "climb" or "descend". The specific value depends on the unit category
*/ */
selectedUnitsChangeAltitude(altitudeChange: string) { changeAltitude(altitudeChange: string, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].changeAltitude(altitudeChange);
} if (units.length === 0)
return;
units.forEach((unit: Unit) => unit.changeAltitude(altitudeChange));
} }
/** Set a specific speed to all the selected units /** Set a specific speed to all the selected units
* *
* @param speed Value to set, in m/s * @param speed Value to set, in m/s
*/ */
selectedUnitsSetSpeed(speed: number) { setSpeed(speed: number, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].setSpeed(speed);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `setting speed to ${msToKnots(speed)} kts`); return;
units.forEach((unit: Unit) => unit.setSpeed(speed));
this.#showActionMessage(units, `setting speed to ${msToKnots(speed)} kts`);
} }
/** Set a specific speed type to all the selected units /** Set a specific speed type to all the selected units
* *
* @param speedType Value to set, either "CAS" or "GS". If "CAS" is selected, the unit will try to maintain the selected Calibrated Air Speed, but DCS will still only maintain a Ground Speed value so errors may arise depending on wind. * @param speedType Value to set, either "CAS" or "GS". If "CAS" is selected, the unit will try to maintain the selected Calibrated Air Speed, but DCS will still only maintain a Ground Speed value so errors may arise depending on wind.
*/ */
selectedUnitsSetSpeedType(speedType: string) { setSpeedType(speedType: string, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].setSpeedType(speedType);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `setting speed type to ${speedType}`); return;
units.forEach((unit: Unit) => unit.setSpeedType(speedType));
this.#showActionMessage(units, `setting speed type to ${speedType}`);
} }
/** Set a specific altitude to all the selected units /** Set a specific altitude to all the selected units
* *
* @param altitude Value to set, in m * @param altitude Value to set, in m
*/ */
selectedUnitsSetAltitude(altitude: number) { setAltitude(altitude: number, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].setAltitude(altitude);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `setting altitude to ${mToFt(altitude)} ft`); return;
units.forEach((unit: Unit) => unit.setAltitude(altitude));
this.#showActionMessage(units, `setting altitude to ${mToFt(altitude)} ft`);
} }
/** Set a specific altitude type to all the selected units /** Set a specific altitude type to all the selected units
* *
* @param altitudeType Value to set, either "ASL" or "AGL". If "AGL" is selected, the unit will try to maintain the selected Above Ground Level altitude. Due to a DCS bug, this will only be true at the final position. * @param altitudeType Value to set, either "ASL" or "AGL". If "AGL" is selected, the unit will try to maintain the selected Above Ground Level altitude. Due to a DCS bug, this will only be true at the final position.
*/ */
selectedUnitsSetAltitudeType(altitudeType: string) { setAltitudeType(altitudeType: string, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].setAltitudeType(altitudeType);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `setting altitude type to ${altitudeType}`); return;
units.forEach((unit: Unit) => unit.setAltitudeType(altitudeType));
this.#showActionMessage(units, `setting altitude type to ${altitudeType}`);
} }
/** Set a specific ROE to all the selected units /** Set a specific ROE to all the selected units
* *
* @param ROE Value to set, see constants for acceptable values * @param ROE Value to set, see constants for acceptable values
*/ */
selectedUnitsSetROE(ROE: string) { setROE(ROE: string, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].setROE(ROE);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `ROE set to ${ROE}`); return;
units.forEach((unit: Unit) => unit.setROE(ROE));
this.#showActionMessage(units, `ROE set to ${ROE}`);
} }
/** Set a specific reaction to threat to all the selected units /** Set a specific reaction to threat to all the selected units
* *
* @param reactionToThreat Value to set, see constants for acceptable values * @param reactionToThreat Value to set, see constants for acceptable values
*/ */
selectedUnitsSetReactionToThreat(reactionToThreat: string) { setReactionToThreat(reactionToThreat: string, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].setReactionToThreat(reactionToThreat);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `reaction to threat set to ${reactionToThreat}`); return;
units.forEach((unit: Unit) => unit.setReactionToThreat(reactionToThreat));
this.#showActionMessage(units, `reaction to threat set to ${reactionToThreat}`);
} }
/** Set a specific emissions & countermeasures to all the selected units /** Set a specific emissions & countermeasures to all the selected units
* *
* @param emissionCountermeasure Value to set, see constants for acceptable values * @param emissionCountermeasure Value to set, see constants for acceptable values
*/ */
selectedUnitsSetEmissionsCountermeasures(emissionCountermeasure: string) { setEmissionsCountermeasures(emissionCountermeasure: string, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].setEmissionsCountermeasures(emissionCountermeasure);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `emissions & countermeasures set to ${emissionCountermeasure}`); return;
units.forEach((unit: Unit) => unit.setEmissionsCountermeasures(emissionCountermeasure));
this.#showActionMessage(units, `emissions & countermeasures set to ${emissionCountermeasure}`);
} }
/** Turn selected units on or off, only works on ground and navy units /** Turn selected units on or off, only works on ground and navy units
* *
* @param onOff If true, the unit will be turned on * @param onOff If true, the unit will be turned on
*/ */
selectedUnitsSetOnOff(onOff: boolean) { setOnOff(onOff: boolean, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].setOnOff(onOff);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `unit active set to ${onOff}`); return;
units.forEach((unit: Unit) => unit.setOnOff(onOff));
this.#showActionMessage(units, `unit active set to ${onOff}`);
} }
/** Instruct the selected units to follow roads, only works on ground units /** Instruct the selected units to follow roads, only works on ground units
* *
* @param followRoads If true, units will follow roads * @param followRoads If true, units will follow roads
*/ */
selectedUnitsSetFollowRoads(followRoads: boolean) { setFollowRoads(followRoads: boolean, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].setFollowRoads(followRoads);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `follow roads set to ${followRoads}`); return;
units.forEach((unit: Unit) => unit.setFollowRoads(followRoads));
this.#showActionMessage(units, `follow roads set to ${followRoads}`);
} }
/** Instruct selected units to operate as a certain coalition /** Instruct selected units to operate as a certain coalition
* *
* @param operateAsBool If true, units will operate as blue * @param operateAsBool If true, units will operate as blue
*/ */
selectedUnitsSetOperateAs(operateAsBool: boolean) { setOperateAs(operateAsBool: boolean, units: Unit[] | null = null) {
var operateAs = operateAsBool ? "blue" : "red"; var operateAs = operateAsBool ? "blue" : "red";
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].setOperateAs(operateAs);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `operate as set to ${operateAs}`); return;
units.forEach((unit: Unit) => unit.setOperateAs(operateAs));
this.#showActionMessage(units, `operate as set to ${operateAs}`);
} }
/** Instruct units to attack a specific unit /** Instruct units to attack a specific unit
* *
* @param ID ID of the unit to attack * @param ID ID of the unit to attack
*/ */
selectedUnitsAttackUnit(ID: number) { attackUnit(ID: number, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].attackUnit(ID);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `attacking unit ${this.getUnitByID(ID)?.getUnitName()}`); return;
units.forEach((unit: Unit) => unit.attackUnit(ID));
this.#showActionMessage(units, `attacking unit ${this.getUnitByID(ID)?.getUnitName()}`);
} }
/** Instruct units to refuel at the nearest tanker, if possible. Else units will RTB /** Instruct units to refuel at the nearest tanker, if possible. Else units will RTB
* *
*/ */
selectedUnitsRefuel() { refuel(units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].refuel();
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `sent to nearest tanker`); return;
units.forEach((unit: Unit) => unit.refuel());
this.#showActionMessage(units, `sent to nearest tanker`);
} }
/** Instruct the selected units to follow another unit in a formation. Only works for aircrafts and helicopters. /** Instruct the selected units to follow another unit in a formation. Only works for aircrafts and helicopters.
@@ -586,7 +639,13 @@ export class UnitsManager {
* @param offset Optional parameter, defines a static offset. X: front-rear, positive front, Y: top-bottom, positive top, Z: left-right, positive right * @param offset Optional parameter, defines a static offset. X: front-rear, positive front, Y: top-bottom, positive top, Z: left-right, positive right
* @param formation Optional parameter, defines a predefined formation type. Values are: "trail", "echelon-lh", "echelon-rh", "line-abreast-lh", "line-abreast-rh", "front", "diamond" * @param formation Optional parameter, defines a predefined formation type. Values are: "trail", "echelon-lh", "echelon-rh", "line-abreast-lh", "line-abreast-rh", "front", "diamond"
*/ */
selectedUnitsFollowUnit(ID: number, offset?: { "x": number, "y": number, "z": number }, formation?: string) { followUnit(ID: number, offset?: { "x": number, "y": number, "z": number }, formation?: string, units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
if (offset == undefined) { if (offset == undefined) {
/* Simple formations with fixed offsets */ /* Simple formations with fixed offsets */
offset = { "x": 0, "y": 0, "z": 0 }; offset = { "x": 0, "y": 0, "z": 0 };
@@ -599,16 +658,11 @@ export class UnitsManager {
else offset = undefined; else offset = undefined;
} }
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (selectedUnits.length === 0)
return;
var count = 1; var count = 1;
var xr = 0; var yr = 1; var zr = -1; var xr = 0; var yr = 1; var zr = -1;
var layer = 1; var layer = 1;
for (let idx in selectedUnits) { for (let idx in units) {
var unit = selectedUnits[idx]; var unit = units[idx];
if (unit.ID !== ID) { if (unit.ID !== ID) {
if (offset != undefined) if (offset != undefined)
/* Offset is set, apply it */ /* Offset is set, apply it */
@@ -630,51 +684,65 @@ export class UnitsManager {
count++; count++;
} }
} }
this.#showActionMessage(selectedUnits, `following unit ${this.getUnitByID(ID)?.getUnitName()}`); this.#showActionMessage(units, `following unit ${this.getUnitByID(ID)?.getUnitName()}`);
} }
/** Instruct the selected units to perform precision bombing of specific coordinates /** Instruct the selected units to perform precision bombing of specific coordinates
* *
* @param latlng Location to bomb * @param latlng Location to bomb
*/ */
selectedUnitsBombPoint(latlng: LatLng) { bombPoint(latlng: LatLng, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].bombPoint(latlng);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `unit bombing point`); return;
units.forEach((unit: Unit) => unit.bombPoint(latlng));
this.#showActionMessage(units, `unit bombing point`);
} }
/** Instruct the selected units to perform carpet bombing of specific coordinates /** Instruct the selected units to perform carpet bombing of specific coordinates
* *
* @param latlng Location to bomb * @param latlng Location to bomb
*/ */
selectedUnitsCarpetBomb(latlng: LatLng) { carpetBomb(latlng: LatLng, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].carpetBomb(latlng);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `unit carpet bombing point`); return;
units.forEach((unit: Unit) => unit.carpetBomb(latlng));
this.#showActionMessage(units, `unit carpet bombing point`);
} }
/** Instruct the selected units to fire at specific coordinates /** Instruct the selected units to fire at specific coordinates
* *
* @param latlng Location to fire at * @param latlng Location to fire at
*/ */
selectedUnitsFireAtArea(latlng: LatLng) { fireAtArea(latlng: LatLng, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].fireAtArea(latlng);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `unit firing at area`); return;
units.forEach((unit: Unit) => unit.fireAtArea(latlng));
this.#showActionMessage(units, `unit firing at area`);
} }
/** Instruct the selected units to simulate a fire fight at specific coordinates /** Instruct the selected units to simulate a fire fight at specific coordinates
* *
* @param latlng Location to fire at * @param latlng Location to fire at
*/ */
selectedUnitsSimulateFireFight(latlng: LatLng) { simulateFireFight(latlng: LatLng, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
if (units.length === 0)
return;
getGroundElevation(latlng, (response: string) => { getGroundElevation(latlng, (response: string) => {
var groundElevation: number | null = null; var groundElevation: number | null = null;
try { try {
@@ -682,70 +750,83 @@ export class UnitsManager {
} catch { } catch {
console.warn("Simulate fire fight: could not retrieve ground elevation") console.warn("Simulate fire fight: could not retrieve ground elevation")
} }
for (let idx in selectedUnits) { units?.forEach((unit: Unit) => unit.simulateFireFight(latlng, groundElevation));
selectedUnits[idx].simulateFireFight(latlng, groundElevation);
}
}); });
this.#showActionMessage(selectedUnits, `unit simulating fire fight`); this.#showActionMessage(units, `unit simulating fire fight`);
} }
/** Instruct units to enter into scenic AAA mode. Units will shoot in the air without aiming /** Instruct units to enter into scenic AAA mode. Units will shoot in the air without aiming
* *
*/ */
selectedUnitsScenicAAA() { scenicAAA(units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].scenicAAA();
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `unit set to perform scenic AAA`); return;
units.forEach((unit: Unit) => unit.scenicAAA());
this.#showActionMessage(units, `unit set to perform scenic AAA`);
} }
/** Instruct units to enter into miss on purpose mode. Units will aim to the nearest enemy unit but not precisely. /** Instruct units to enter into miss on purpose mode. Units will aim to the nearest enemy unit but not precisely.
* *
*/ */
selectedUnitsMissOnPurpose() { missOnPurpose(units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
selectedUnits[idx].missOnPurpose();
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `unit set to perform miss-on-purpose AAA`); return;
units.forEach((unit: Unit) => unit.missOnPurpose());
this.#showActionMessage(units, `unit set to perform miss-on-purpose AAA`);
} }
/** Instruct units to land at specific point /** Instruct units to land at specific point
* *
* @param latlng Point where to land * @param latlng Point where to land
*/ */
selectedUnitsLandAtPoint(latlng: LatLng) { landAtPoint(latlng: LatLng, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true }); if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true, showProtectionReminder: true });
for (let idx in selectedUnits) {
selectedUnits[idx].landAtPoint(latlng); if (units.length === 0)
} return;
this.#showActionMessage(selectedUnits, `unit landing at point`);
units.forEach((unit: Unit) => unit.landAtPoint(latlng));
this.#showActionMessage(units, `unit landing at point`);
} }
/** Set a specific shots scatter to all the selected units /** Set a specific shots scatter to all the selected units
* *
* @param shotsScatter Value to set * @param shotsScatter Value to set
*/ */
selectedUnitsSetShotsScatter(shotsScatter: number) { setShotsScatter(shotsScatter: number, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true });
selectedUnits[idx].setShotsScatter(shotsScatter);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `shots scatter set to ${shotsScatter}`); return;
units.forEach((unit: Unit) => unit.setShotsScatter(shotsScatter));
this.#showActionMessage(units, `shots scatter set to ${shotsScatter}`);
} }
/** Set a specific shots intensity to all the selected units /** Set a specific shots intensity to all the selected units
* *
* @param shotsScatter Value to set * @param shotsScatter Value to set
*/ */
selectedUnitsSetShotsIntensity(shotsIntensity: number) { setShotsIntensity(shotsIntensity: number, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true });
selectedUnits[idx].setShotsIntensity(shotsIntensity);
} if (units.length === 0)
this.#showActionMessage(selectedUnits, `shots intensity set to ${shotsIntensity}`); return;
units.forEach((unit: Unit) => unit.setShotsIntensity(shotsIntensity));
this.#showActionMessage(units, `shots intensity set to ${shotsIntensity}`);
} }
/*********************** Control operations on selected units ************************/ /*********************** Control operations on selected units ************************/
@@ -769,15 +850,18 @@ export class UnitsManager {
/** Groups the selected units in a single (DCS) group, if all the units have the same category /** Groups the selected units in a single (DCS) group, if all the units have the same category
* *
*/ */
selectedUnitsCreateGroup() { createGroup(units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: false, showProtectionReminder: true }); if (units === null)
if (this.getUnitsCategories(selectedUnits).length == 1) { units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: false, showProtectionReminder: true });
var units: { ID: number, location: LatLng }[] = [];
for (let idx in selectedUnits) { if (units.length === 0)
var unit = selectedUnits[idx]; return;
units.push({ ID: unit.ID, location: unit.getPosition() });
} if (this.getUnitsCategories(units).length == 1) {
getApp().getServerManager().cloneUnits(units, true, 0 /* No spawn points, we delete the original units */); var unitsData: { ID: number, location: LatLng }[] = [];
units.forEach((unit: Unit) => unitsData.push({ ID: unit.ID, location: unit.getPosition() }));
getApp().getServerManager().cloneUnits(unitsData, true, 0 /* No spawn points, we delete the original units */);
this.#showActionMessage(units, `created a group`);
} else { } else {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Groups can only be created from units of the same category`); (getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Groups can only be created from units of the same category`);
} }
@@ -787,21 +871,20 @@ export class UnitsManager {
* *
* @param hotgroup Hotgroup number * @param hotgroup Hotgroup number
*/ */
selectedUnitsSetHotgroup(hotgroup: number) { setHotgroup(hotgroup: number, units: Unit[] | null = null) {
this.getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setHotgroup(null)); this.getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setHotgroup(null));
this.selectedUnitsAddToHotgroup(hotgroup); this.addToHotgroup(hotgroup);
} }
/** Add the selected units to a hotgroup. Units can be in multiple hotgroups at the same type /** Add the selected units to a hotgroup. Units can be in multiple hotgroups at the same type
* *
* @param hotgroup Hotgroup number * @param hotgroup Hotgroup number
*/ */
selectedUnitsAddToHotgroup(hotgroup: number) { addToHotgroup(hotgroup: number, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits(); if (units === null)
for (let idx in selectedUnits) { units = this.getSelectedUnits();
selectedUnits[idx].setHotgroup(hotgroup); units.forEach((unit: Unit) => unit.setHotgroup(hotgroup));
} this.#showActionMessage(units, `added to hotgroup ${hotgroup}`);
this.#showActionMessage(selectedUnits, `added to hotgroup ${hotgroup}`);
(getApp().getPanelsManager().get("hotgroup") as HotgroupPanel).refreshHotgroups(); (getApp().getPanelsManager().get("hotgroup") as HotgroupPanel).refreshHotgroups();
} }
@@ -810,9 +893,14 @@ export class UnitsManager {
* @param explosion If true, the unit will be deleted using an explosion * @param explosion If true, the unit will be deleted using an explosion
* @returns * @returns
*/ */
selectedUnitsDelete(explosion: boolean = false, explosionType: string = "") { delete(explosion: boolean = false, explosionType: string = "", units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeProtected: true }); /* Can be applied to humans too */ if (units === null)
const selectionContainsAHuman = selectedUnits.some((unit: Unit) => { units = this.getSelectedUnits({ excludeProtected: true, showProtectionReminder: true }); /* Can be applied to humans too */
if (units.length === 0)
return;
const selectionContainsAHuman = units.some((unit: Unit) => {
return unit.getHuman() === true; return unit.getHuman() === true;
}); });
@@ -821,14 +909,12 @@ export class UnitsManager {
} }
const doDelete = (explosion = false, explosionType = "", immediate = false) => { const doDelete = (explosion = false, explosionType = "", immediate = false) => {
for (let idx in selectedUnits) { units?.forEach((unit: Unit) => unit.delete(explosion, explosionType, immediate));
selectedUnits[idx].delete(explosion, explosionType, immediate); this.#showActionMessage(units as Unit[], `deleted`);
}
this.#showActionMessage(selectedUnits, `deleted`);
} }
if (selectedUnits.length >= DELETE_SLOW_THRESHOLD) if (units.length >= DELETE_SLOW_THRESHOLD)
this.#showSlowDeleteDialog(selectedUnits).then((action: any) => { this.#showSlowDeleteDialog(units).then((action: any) => {
if (action === "delete-slow") if (action === "delete-slow")
doDelete(explosion, explosionType, false); doDelete(explosion, explosionType, false);
else if (action === "delete-immediate") else if (action === "delete-immediate")
@@ -845,19 +931,25 @@ export class UnitsManager {
* @param rotation Rotation of the group, in radians * @param rotation Rotation of the group, in radians
* @returns Array of positions for each unit, in order * @returns Array of positions for each unit, in order
*/ */
selectedUnitsComputeGroupDestination(latlng: LatLng, rotation: number) { computeGroupDestination(latlng: LatLng, rotation: number, units: Unit[] | null = null) {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true }); if (units === null)
units = this.getSelectedUnits({ excludeHumans: true, excludeProtected: true, onlyOnePerGroup: true });
if (units.length === 0)
return {};
/* Compute the center of the group */ /* Compute the center of the group */
var len = units.length;
var center = { x: 0, y: 0 }; var center = { x: 0, y: 0 };
selectedUnits.forEach((unit: Unit) => { units.forEach((unit: Unit) => {
var mercator = latLngToMercator(unit.getPosition().lat, unit.getPosition().lng); var mercator = latLngToMercator(unit.getPosition().lat, unit.getPosition().lng);
center.x += mercator.x / selectedUnits.length; center.x += mercator.x / len;
center.y += mercator.y / selectedUnits.length; center.y += mercator.y / len;
}); });
/* Compute the distances from the center of the group */ /* Compute the distances from the center of the group */
var unitDestinations: { [key: number]: LatLng } = {}; var unitDestinations: { [key: number]: LatLng } = {};
selectedUnits.forEach((unit: Unit) => { units.forEach((unit: Unit) => {
var mercator = latLngToMercator(unit.getPosition().lat, unit.getPosition().lng); var mercator = latLngToMercator(unit.getPosition().lat, unit.getPosition().lng);
var distancesFromCenter = { dx: mercator.x - center.x, dy: mercator.y - center.y }; var distancesFromCenter = { dx: mercator.x - center.x, dy: mercator.y - center.y };
@@ -879,9 +971,15 @@ export class UnitsManager {
/** Copy the selected units and store their properties in memory /** Copy the selected units and store their properties in memory
* *
*/ */
selectedUnitsCopy() { copy(units: Unit[] | null = null) {
if (units === null)
units = this.getSelectedUnits({ excludeHumans: true });
if (units.length === 0)
return;
/* A JSON is used to deepcopy the units, creating a "snapshot" of their properties at the time of the copy */ /* A JSON is used to deepcopy the units, creating a "snapshot" of their properties at the time of the copy */
this.#copiedUnits = JSON.parse(JSON.stringify(this.getSelectedUnits().map((unit: Unit) => { return unit.getData() }))); /* Can be applied to humans too */ this.#copiedUnits = JSON.parse(JSON.stringify(units.map((unit: Unit) => { return unit.getData() }))); /* Can be applied to humans too */
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${this.#copiedUnits.length} units copied`); (getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${this.#copiedUnits.length} units copied`);
} }
@@ -890,7 +988,7 @@ export class UnitsManager {
* *
* @returns True if units were pasted successfully * @returns True if units were pasted successfully
*/ */
pasteUnits() { paste() {
let spawnPoints = 0; let spawnPoints = 0;
/* If spawns are restricted, check that the user has the necessary spawn points */ /* If spawns are restricted, check that the user has the necessary spawn points */
@@ -1111,7 +1209,7 @@ export class UnitsManager {
#onKeyUp(event: KeyboardEvent) { #onKeyUp(event: KeyboardEvent) {
if (!keyEventWasInInput(event)) { if (!keyEventWasInInput(event)) {
if (event.key === "Delete") if (event.key === "Delete")
this.selectedUnitsDelete(); this.delete();
else if (event.key === "a" && event.ctrlKey) else if (event.key === "a" && event.ctrlKey)
Object.values(this.getUnits()).filter((unit: Unit) => { return !unit.getHidden() }).forEach((unit: Unit) => unit.setSelected(true)); Object.values(this.getUnits()).filter((unit: Unit) => { return !unit.getHidden() }).forEach((unit: Unit) => unit.setSelected(true));
} }
@@ -1160,9 +1258,9 @@ export class UnitsManager {
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${units[0].getUnitName()} and ${units.length - 1} other units ${message}`); (getApp().getPopupsManager().get("infoPopup") as Popup).setText(`${units[0].getUnitName()} and ${units.length - 1} other units ${message}`);
} }
#showSlowDeleteDialog(selectedUnits: Unit[]) { #showSlowDeleteDialog(units: Unit[]) {
let button: HTMLButtonElement | null = null; let button: HTMLButtonElement | null = null;
const deletionTime = Math.round(selectedUnits.length * DELETE_CYCLE_TIME).toString(); const deletionTime = Math.round(units.length * DELETE_CYCLE_TIME).toString();
const dialog = this.#slowDeleteDialog; const dialog = this.#slowDeleteDialog;
const element = dialog.getElement(); const element = dialog.getElement();
const listener = (ev: MouseEvent) => { const listener = (ev: MouseEvent) => {
@@ -1170,7 +1268,7 @@ export class UnitsManager {
button = ev.target; button = ev.target;
} }
element.querySelectorAll(".deletion-count").forEach(el => el.innerHTML = selectedUnits.length.toString()); element.querySelectorAll(".deletion-count").forEach(el => el.innerHTML = units.length.toString());
element.querySelectorAll(".deletion-time").forEach(el => el.innerHTML = deletionTime); element.querySelectorAll(".deletion-time").forEach(el => el.innerHTML = deletionTime);
dialog.show(); dialog.show();
@@ -1190,9 +1288,9 @@ export class UnitsManager {
#showNumberOfSelectedProtectedUnits() { #showNumberOfSelectedProtectedUnits() {
const map = getApp().getMap(); const map = getApp().getMap();
const selectedUnits = this.getSelectedUnits(); const units = this.getSelectedUnits();
const numSelectedUnits = selectedUnits.length; const numSelectedUnits = units.length;
const numProtectedUnits = selectedUnits.filter((unit: Unit) => map.unitIsProtected(unit)).length; const numProtectedUnits = units.filter((unit: Unit) => map.unitIsProtected(unit)).length;
if (numProtectedUnits === 1 && numSelectedUnits === numProtectedUnits) if (numProtectedUnits === 1 && numSelectedUnits === numProtectedUnits)
(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Notice: unit is protected`); (getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Notice: unit is protected`);

View File

@@ -3,14 +3,18 @@
<head> <head>
<title>Olympus client</title> <title>Olympus client</title>
<link rel="stylesheet" type="text/css" href="stylesheets/olympus.css" /> <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.css" />
<link rel="stylesheet" type="text/css" href="stylesheets/leaflet/leaflet-gesture-handling.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 --> <link rel="stylesheet" type="text/css" href="/resources/theme/theme.css" /> <!-- Theme specifc css, autorouted to point to active theme -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;600;700;800&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;600;700;800&display=swap" />
<link rel="icon" href="/images/favicons/favicon.ico" />
<link rel="apple-touch-icon" href="images/favicons/apple-touch-icon.png"/>
<link rel="manifest" href="/images/favicons/site.webmanifest" />
</head> </head>
<body> <body>

View File

@@ -3,12 +3,12 @@
<div id="app-summary"> <div id="app-summary">
<h2>DCS Olympus</h2> <h2>DCS Olympus</h2>
<h4>Dynamic Unit Command</h4> <h4>Dynamic Unit Command</h4>
<div class="app-version">Version <span class="app-version-number">v0.4.6-alpha</span></div> <div class="app-version">Version <span class="app-version-number">v0.4.7-alpha</span></div>
</div> </div>
<form id="authentication-form"> <form id="authentication-form">
<div><h5>Username</h5> <input type="text" id="username" name="username" required autocomplete="username" placeholder="Enter username..."></div> <div><h5>Name</h5> <input type="text" id="username" name="username" required autocomplete="username" placeholder="Enter username..."></div>
<div><h5>Password</h5> <input type="password" id="password" name="password" minlength="8" required autocomplete="current-password" placeholder="Enter password..."></div> <div><h5>Server password</h5> <input type="password" id="password" name="password" minlength="8" required autocomplete="current-password" placeholder="Enter password..."></div>
<button type="submit" id="connection-button" class="ol-button-apply">Connect</button> <button type="submit" id="connection-button" class="ol-button-apply">Connect</button>
</form> </form>

View File

@@ -6,7 +6,7 @@
<div class="ol-select-options"> <div class="ol-select-options">
<div id="toolbar-summary"> <div id="toolbar-summary">
<h3>DCS Olympus</h3> <h3>DCS Olympus</h3>
<div class="accent-green app-version-number">version v0.4.6-alpha</div> <div class="accent-green app-version-number">version v0.4.7-alpha</div>
</div> </div>
<div> <div>
<a href="https://www.discord.com" target="_blank">Discord</a> <a href="https://www.discord.com" target="_blank">Discord</a>

View File

@@ -1,5 +1,5 @@
#define nwjsFolder "C:\Users\dpass\Documents\nwjs\" #define nwjsFolder "C:\Users\dpass\Documents\nwjs\"
#define version "v0.4.6-alpha" #define version "v0.4.7-alpha"
[Setup] [Setup]
AppName=DCS Olympus AppName=DCS Olympus

View File

@@ -15,7 +15,7 @@ declare_plugin(self_ID,
shortName = "Olympus", shortName = "Olympus",
fileMenuName = "Olympus", fileMenuName = "Olympus",
version = "v0.4.6-alpha", version = "v0.4.7-alpha",
state = "installed", state = "installed",
developerName= "DCS Refugees 767 squadron", developerName= "DCS Refugees 767 squadron",
info = _("DCS Olympus is a mod for DCS World. It allows users to spawn, control, task, group, and remove units from a DCS World server using a real-time map interface, similarly to Real Time Strategy games. The user interface also provides useful informations units, like loadouts, fuel, tasking, and so on. In the future, more features for DCS World GCI and JTAC will be available."), info = _("DCS Olympus is a mod for DCS World. It allows users to spawn, control, task, group, and remove units from a DCS World server using a real-time map interface, similarly to Real Time Strategy games. The user interface also provides useful informations units, like loadouts, fuel, tasking, and so on. In the future, more features for DCS World GCI and JTAC will be available."),

View File

@@ -1,4 +1,4 @@
local version = "v0.4.6-alpha" local version = "v0.4.7-alpha"
local debug = false -- True enables debug printing using DCS messages local debug = false -- True enables debug printing using DCS messages
@@ -180,7 +180,7 @@ function Olympus.buildTask(groupName, options)
if options ['altitudeType'] then if options ['altitudeType'] then
if options ['altitudeType'] == "AGL" then if options ['altitudeType'] == "AGL" then
local groundHeight = 0 local groundHeight = 0
if group then if group ~= nil then
local groupPos = mist.getLeadPos(group) local groupPos = mist.getLeadPos(group)
groundHeight = land.getHeight({x = groupPos.x, y = groupPos.z}) groundHeight = land.getHeight({x = groupPos.x, y = groupPos.z})
end end
@@ -287,7 +287,7 @@ end
function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedType, category, taskOptions) function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedType, category, taskOptions)
Olympus.debug("Olympus.move " .. groupName .. " (" .. lat .. ", " .. lng ..") " .. altitude .. "m " .. altitudeType .. " ".. speed .. "m/s " .. category .. " " .. Olympus.serializeTable(taskOptions), 2) Olympus.debug("Olympus.move " .. groupName .. " (" .. lat .. ", " .. lng ..") " .. altitude .. "m " .. altitudeType .. " ".. speed .. "m/s " .. category .. " " .. Olympus.serializeTable(taskOptions), 2)
local group = Group.getByName(groupName) local group = Group.getByName(groupName)
if group then if group ~= nil then
if category == "Aircraft" then if category == "Aircraft" then
local startPoint = mist.getLeadPos(group) local startPoint = mist.getLeadPos(group)
local endPoint = coord.LLtoLO(lat, lng, 0) local endPoint = coord.LLtoLO(lat, lng, 0)
@@ -765,9 +765,9 @@ end
-- Find a database entry by ID -- Find a database entry by ID
function Olympus.findInDatabase(ID) function Olympus.findInDatabase(ID)
for idx, unit in pairs(Olympus.cloneDatabase) do for idx, unitRecord in pairs(Olympus.cloneDatabase) do
if unit ~= nil and unit["ID"] == ID then if unitRecord ~= nil and unitRecord["ID"] == ID then
return unit return unitRecord
end end
end end
return nil return nil
@@ -789,11 +789,11 @@ function Olympus.clone(cloneTable, deleteOriginal)
-- All the units in the table will be cloned in a single group -- All the units in the table will be cloned in a single group
for idx, cloneData in pairs(cloneTable) do for idx, cloneData in pairs(cloneTable) do
local ID = cloneData.ID local ID = cloneData.ID
local unit = Olympus.findInDatabase(ID) local unitRecord = Olympus.findInDatabase(ID)
if unit ~= nil then if unitRecord ~= nil then
-- Update the data of the cloned unit -- Update the data of the cloned unit
local unitTable = mist.utils.deepCopy(unit) local unitTable = mist.utils.deepCopy(unitRecord)
local point = coord.LLtoLO(cloneData['lat'], cloneData['lng'], 0) local point = coord.LLtoLO(cloneData['lat'], cloneData['lng'], 0)
if unitTable then if unitTable then
@@ -803,8 +803,8 @@ function Olympus.clone(cloneTable, deleteOriginal)
end end
if countryID == nil and category == nil then if countryID == nil and category == nil then
countryID = unit["country"] countryID = unitRecord["country"]
if unit["category"] == Unit.Category.AIRPLANE then if unitRecord["category"] == Unit.Category.AIRPLANE then
category = 'plane' category = 'plane'
route = { route = {
["points"] = ["points"] =
@@ -823,7 +823,7 @@ function Olympus.clone(cloneTable, deleteOriginal)
}, },
}, },
} }
elseif unit["category"] == Unit.Category.HELICOPTER then elseif unitRecord["category"] == Unit.Category.HELICOPTER then
category = 'helicopter' category = 'helicopter'
route = { route = {
["points"] = ["points"] =
@@ -842,9 +842,9 @@ function Olympus.clone(cloneTable, deleteOriginal)
}, },
}, },
} }
elseif unit["category"] == Unit.Category.GROUND_UNIT then elseif unitRecord["category"] == Unit.Category.GROUND_UNIT then
category = 'vehicle' category = 'vehicle'
elseif unit["category"] == Unit.Category.SHIP then elseif unitRecord["category"] == Unit.Category.SHIP then
category = 'ship' category = 'ship'
end end
end end
@@ -884,7 +884,7 @@ end
function Olympus.delete(ID, explosion, explosionType) function Olympus.delete(ID, explosion, explosionType)
Olympus.debug("Olympus.delete " .. ID .. " " .. tostring(explosion), 2) Olympus.debug("Olympus.delete " .. ID .. " " .. tostring(explosion), 2)
local unit = Olympus.getUnitByID(ID) local unit = Olympus.getUnitByID(ID)
if unit then if unit ~= nil and unit:isExist() then
if unit:getPlayerName() or explosion then if unit:getPlayerName() or explosion then
if explosionType == nil then if explosionType == nil then
explosionType = "normal" explosionType = "normal"
@@ -903,7 +903,7 @@ end
function Olympus.setTask(groupName, taskOptions) function Olympus.setTask(groupName, taskOptions)
Olympus.debug("Olympus.setTask " .. groupName .. " " .. Olympus.serializeTable(taskOptions), 2) Olympus.debug("Olympus.setTask " .. groupName .. " " .. Olympus.serializeTable(taskOptions), 2)
local group = Group.getByName(groupName) local group = Group.getByName(groupName)
if group then if group ~= nil then
local task = Olympus.buildTask(groupName, taskOptions); local task = Olympus.buildTask(groupName, taskOptions);
Olympus.debug("Olympus.setTask " .. Olympus.serializeTable(task), 20) Olympus.debug("Olympus.setTask " .. Olympus.serializeTable(task), 20)
if task then if task then
@@ -917,7 +917,7 @@ end
function Olympus.resetTask(groupName) function Olympus.resetTask(groupName)
Olympus.debug("Olympus.resetTask " .. groupName, 2) Olympus.debug("Olympus.resetTask " .. groupName, 2)
local group = Group.getByName(groupName) local group = Group.getByName(groupName)
if group then if group ~= nil then
group:getController():resetTask() group:getController():resetTask()
Olympus.debug("Olympus.resetTask completed successfully", 2) Olympus.debug("Olympus.resetTask completed successfully", 2)
end end
@@ -927,7 +927,7 @@ end
function Olympus.setCommand(groupName, command) function Olympus.setCommand(groupName, command)
Olympus.debug("Olympus.setCommand " .. groupName .. " " .. Olympus.serializeTable(command), 2) Olympus.debug("Olympus.setCommand " .. groupName .. " " .. Olympus.serializeTable(command), 2)
local group = Group.getByName(groupName) local group = Group.getByName(groupName)
if group then if group ~= nil then
group:getController():setCommand(command) group:getController():setCommand(command)
Olympus.debug("Olympus.setCommand completed successfully", 2) Olympus.debug("Olympus.setCommand completed successfully", 2)
end end
@@ -937,7 +937,7 @@ end
function Olympus.setOption(groupName, optionID, optionValue) function Olympus.setOption(groupName, optionID, optionValue)
Olympus.debug("Olympus.setOption " .. groupName .. " " .. optionID .. " " .. tostring(optionValue), 2) Olympus.debug("Olympus.setOption " .. groupName .. " " .. optionID .. " " .. tostring(optionValue), 2)
local group = Group.getByName(groupName) local group = Group.getByName(groupName)
if group then if group ~= nil then
group:getController():setOption(optionID, optionValue) group:getController():setOption(optionID, optionValue)
Olympus.debug("Olympus.setOption completed successfully", 2) Olympus.debug("Olympus.setOption completed successfully", 2)
end end
@@ -947,7 +947,7 @@ end
function Olympus.setOnOff(groupName, onOff) function Olympus.setOnOff(groupName, onOff)
Olympus.debug("Olympus.setOnOff " .. groupName .. " " .. tostring(onOff), 2) Olympus.debug("Olympus.setOnOff " .. groupName .. " " .. tostring(onOff), 2)
local group = Group.getByName(groupName) local group = Group.getByName(groupName)
if group then if group ~= nil then
group:getController():setOnOff(onOff) group:getController():setOnOff(onOff)
Olympus.debug("Olympus.setOnOff completed successfully", 2) Olympus.debug("Olympus.setOnOff completed successfully", 2)
end end
@@ -965,11 +965,11 @@ function Olympus.setUnitsData(arg, time)
index = index + 1 index = index + 1
-- Only the indexes between startIndex and endIndex are handled. This is a simple way to spread the update load over many cycles -- Only the indexes between startIndex and endIndex are handled. This is a simple way to spread the update load over many cycles
if index > startIndex then if index > startIndex then
if unit ~= nil then if unit ~= nil and unit:isExist() then
local table = {} local table = {}
-- Get the object category in Olympus name -- Get the object category in Olympus name
local objectCategory = unit:getCategory() local objectCategory = Object.getCategory(unit)
if objectCategory == Object.Category.UNIT then if objectCategory == Object.Category.UNIT then
if unit:getDesc().category == Unit.Category.AIRPLANE then if unit:getDesc().category == Unit.Category.AIRPLANE then
table["category"] = "Aircraft" table["category"] = "Aircraft"
@@ -1091,11 +1091,11 @@ function Olympus.setWeaponsData(arg, time)
-- Only the indexes between startIndex and endIndex are handled. This is a simple way to spread the update load over many cycles -- Only the indexes between startIndex and endIndex are handled. This is a simple way to spread the update load over many cycles
if index > startIndex then if index > startIndex then
if weapon ~= nil then if weapon ~= nil and weapon:isExist() then
local table = {} local table = {}
-- Get the object category in Olympus name -- Get the object category in Olympus name
local objectCategory = weapon:getCategory() local objectCategory = Object.getCategory(weapon)
if objectCategory == Object.Category.WEAPON then if objectCategory == Object.Category.WEAPON then
if weapon:getDesc().category == Weapon.Category.MISSILE then if weapon:getDesc().category == Weapon.Category.MISSILE then
table["category"] = "Missile" table["category"] = "Missile"
@@ -1216,7 +1216,7 @@ function Olympus.initializeUnits()
if mist and mist.DBs and mist.DBs.MEunitsById then if mist and mist.DBs and mist.DBs.MEunitsById then
for id, unitsTable in pairs(mist.DBs.MEunitsById) do for id, unitsTable in pairs(mist.DBs.MEunitsById) do
local unit = Unit.getByName(unitsTable["unitName"]) local unit = Unit.getByName(unitsTable["unitName"])
if unit then if unit ~= nil and unit:isExist() then
Olympus.units[unit["id_"]] = unit Olympus.units[unit["id_"]] = unit
end end
end end

View File

@@ -1,4 +1,4 @@
local version = 'v0.4.6-alpha' local version = 'v0.4.7-alpha'
Olympus = {} Olympus = {}
Olympus.OlympusDLL = nil Olympus.OlympusDLL = nil

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#define VERSION "v0.4.6-alpha" #define VERSION "v0.4.7-alpha"
#define LOG_NAME "Olympus_log.txt" #define LOG_NAME "Olympus_log.txt"
#define REST_ADDRESS "http://localhost:30000" #define REST_ADDRESS "http://localhost:30000"
#define REST_URI "olympus" #define REST_URI "olympus"