diff --git a/client/demo.js b/client/demo.js index 9e560bee..ca65e0c8 100644 --- a/client/demo.js +++ b/client/demo.js @@ -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 t = Object.keys(databases).length; @@ -114,6 +114,39 @@ class DemoDataGenerator { DEMO_UNIT_DATA[idx].position.lat += idx / 100; DEMO_UNIT_DATA[idx].category = "GroundUnit"; 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(); } diff --git a/client/package-lock.json b/client/package-lock.json index 446de9f4..a50305c8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "DCSOlympus", - "version": "v0.4.6-alpha", + "version": "v0.4.7-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "DCSOlympus", - "version": "v0.4.6-alpha", + "version": "v0.4.7-alpha", "dependencies": { "@turf/turf": "^6.5.0", "body-parser": "^1.20.2", diff --git a/client/package.json b/client/package.json index 8a6e718a..1238d38a 100644 --- a/client/package.json +++ b/client/package.json @@ -2,7 +2,7 @@ "name": "DCSOlympus", "node-main": "./bin/www", "main": "http://localhost:3000", - "version": "v0.4.6-alpha", + "version": "v0.4.7-alpha", "private": true, "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", diff --git a/client/public/images/favicons/android-chrome-192x192.png b/client/public/images/favicons/android-chrome-192x192.png new file mode 100644 index 00000000..94156476 Binary files /dev/null and b/client/public/images/favicons/android-chrome-192x192.png differ diff --git a/client/public/images/favicons/android-chrome-512x512.png b/client/public/images/favicons/android-chrome-512x512.png new file mode 100644 index 00000000..1b1a4c17 Binary files /dev/null and b/client/public/images/favicons/android-chrome-512x512.png differ diff --git a/client/public/images/favicons/apple-touch-icon.png b/client/public/images/favicons/apple-touch-icon.png new file mode 100644 index 00000000..8aa1651e Binary files /dev/null and b/client/public/images/favicons/apple-touch-icon.png differ diff --git a/client/public/images/favicons/favicon-16x16.png b/client/public/images/favicons/favicon-16x16.png new file mode 100644 index 00000000..01d279f2 Binary files /dev/null and b/client/public/images/favicons/favicon-16x16.png differ diff --git a/client/public/images/favicons/favicon-32x32.png b/client/public/images/favicons/favicon-32x32.png new file mode 100644 index 00000000..63c55bd2 Binary files /dev/null and b/client/public/images/favicons/favicon-32x32.png differ diff --git a/client/public/images/favicons/favicon.ico b/client/public/images/favicons/favicon.ico new file mode 100644 index 00000000..5bb88640 Binary files /dev/null and b/client/public/images/favicons/favicon.ico differ diff --git a/client/public/images/favicons/site.webmanifest b/client/public/images/favicons/site.webmanifest new file mode 100644 index 00000000..44c84eb7 --- /dev/null +++ b/client/public/images/favicons/site.webmanifest @@ -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" +} \ No newline at end of file diff --git a/client/public/stylesheets/style/style.css b/client/public/stylesheets/style/style.css index 3117df23..9909d690 100644 --- a/client/public/stylesheets/style/style.css +++ b/client/public/stylesheets/style/style.css @@ -687,6 +687,19 @@ nav.ol-panel> :last-child { 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 * { fill:white !important; } diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index 7ec229c5..c58e3073 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -15,11 +15,11 @@ export const BLUE_COMMANDER = "Blue commander"; export const RED_COMMANDER = "Red commander"; export const VISUAL = 1; -export const OPTIC = 2; -export const RADAR = 4; -export const IRST = 8; -export const RWR = 16; -export const DLINK = 32; +export const OPTIC = 2; +export const RADAR = 4; +export const IRST = 8; +export const RWR = 16; +export const DLINK = 32; export const states: string[] = ["none", "idle", "reach-destination", "attack", "follow", "land", "refuel", "AWACS", "tanker", "bomb-point", "carpet-bomb", "bomb-building", "fire-at-area", "simulate-fire-fight", "scenic-aaa", "miss-on-purpose", "land-at-point"]; export const ROEs: string[] = ["free", "designated", "", "return", "hold"]; @@ -35,16 +35,16 @@ export const ROEDescriptions: string[] = [ ]; export const reactionsToThreatDescriptions: string[] = [ - "None (No reaction)", - "Manoeuvre (no countermeasures)", - "Passive (Countermeasures only, no manoeuvre)", + "None (No reaction)", + "Manoeuvre (no countermeasures)", + "Passive (Countermeasures only, no manoeuvre)", "Evade (Countermeasures and manoeuvers)" ]; export const emissionsCountermeasuresDescriptions: string[] = [ - "Silent (Radar OFF, no ECM)", - "Attack (Radar only for targeting, ECM only if locked)", - "Defend (Radar for searching, ECM if locked)", + "Silent (Radar OFF, no ECM)", + "Attack (Radar only for targeting, ECM only if locked)", + "Defend (Radar for searching, ECM if locked)", "Always on (Radar and ECM always on)" ]; @@ -118,25 +118,25 @@ export const mapLayers = { "ArcGIS Satellite": { urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", 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" }, "USGS Topo": { urlTemplate: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', minZoom: 1, - maxZoom: 18, + maxZoom: 14, attribution: 'Tiles courtesy of the U.S. Geological Survey' }, "OpenStreetMap Mapnik": { urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', minZoom: 1, - maxZoom: 18, + maxZoom: 20, attribution: '© OpenStreetMap contributors' }, "OPENVKarte": { urlTemplate: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', minZoom: 1, - maxZoom: 18, + maxZoom: 20, attribution: 'Map memomaps.de CC-BY-SA, map data © OpenStreetMap contributors' }, "Esri.DeLorme": { @@ -148,7 +148,7 @@ export const mapLayers = { "CyclOSM": { urlTemplate: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', minZoom: 1, - maxZoom: 18, + maxZoom: 20, attribution: 'CyclOSM | Map data: © OpenStreetMap 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 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 MAP_MARKER_CONTROLS:MapMarkerControl[] = [{ - "name":"Human", +export const MAP_MARKER_CONTROLS: MapMarkerControl[] = [{ + "name": "Human", "image": "visibility/human.svg", - "toggles": [ "human" ], + "toggles": ["human"], "tooltip": "Toggle human players' visibility" }, { "image": "visibility/dcs.svg", "isProtected": true, - "name":"DCS", + "name": "DCS", "protectable": true, - "toggles": [ "dcs" ], + "toggles": ["dcs"], "tooltip": "Toggle DCS-controlled units' visibility" }, { "image": "visibility/aircraft.svg", - "name":"Aircraft", - "toggles": [ "aircraft" ], + "name": "Aircraft", + "toggles": ["aircraft"], "tooltip": "Toggle aircraft's visibility" }, { "image": "visibility/helicopter.svg", - "name":"Helicopter", - "toggles": [ "helicopter" ], + "name": "Helicopter", + "toggles": ["helicopter"], "tooltip": "Toggle helicopters' visibility" }, { "image": "visibility/groundunit-sam.svg", - "name":"Air defence", - "toggles": [ "groundunit-sam" ], + "name": "Air defence", + "toggles": ["groundunit-sam"], "tooltip": "Toggle air defence units' visibility" }, { "image": "visibility/groundunit-other.svg", - "name":"Ground units", - "toggles": [ "groundunit-other" ], + "name": "Ground units", + "toggles": ["groundunit-other"], "tooltip": "Toggle ground units' visibility" }, { "image": "visibility/navyunit.svg", - "name":"Naval", - "toggles": [ "navyunit" ], + "name": "Naval", + "toggles": ["navyunit"], "tooltip": "Toggle naval units' visibility" }, { "image": "visibility/airbase.svg", - "name":"Airbase", - "toggles": [ "airbase" ], + "name": "Airbase", + "toggles": ["airbase"], "tooltip": "Toggle airbase' visibility" }]; 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 SHOW_UNIT_LABELS = "Show unit labels (L)"; -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 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 SHOW_UNIT_CONTACTS = "Show selected units contact lines"; -export const SHOW_UNIT_PATHS = "Show selected unit paths"; -export const SHOW_UNIT_TARGETS = "Show selected unit targets"; +export const HIDE_GROUP_MEMBERS = "Hide group members when zoomed out"; +export const SHOW_UNIT_LABELS = "Show unit labels (L)"; +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 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 SHOW_UNIT_CONTACTS = "Show selected units contact lines"; +export const SHOW_UNIT_PATHS = "Show selected unit paths"; +export const SHOW_UNIT_TARGETS = "Show selected unit targets"; export enum DataIndexes { startOfData = 0, @@ -267,12 +267,12 @@ export enum DataIndexes { }; 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_10M = 5; -export const MGRS_PRECISION_1M = 6; +export const MGRS_PRECISION_10M = 5; +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 GROUPING_ZOOM_TRANSITION = 13; \ No newline at end of file diff --git a/client/src/contextmenus/airbasecontextmenu.ts b/client/src/contextmenus/airbasecontextmenu.ts index b6b753ac..85738197 100644 --- a/client/src/contextmenus/airbasecontextmenu.ts +++ b/client/src/contextmenus/airbasecontextmenu.ts @@ -24,7 +24,7 @@ export class AirbaseContextMenu extends ContextMenu { document.addEventListener("contextMenuLandAirbase", (e: any) => { if (this.#airbase) - getApp().getUnitsManager().selectedUnitsLandAt(this.#airbase.getLatLng()); + getApp().getUnitsManager().landAt(this.#airbase.getLatLng()); this.hide(); }) } @@ -111,7 +111,7 @@ export class AirbaseContextMenu extends ContextMenu { #showSpawnMenu() { if (this.#airbase != null) { 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()); } } diff --git a/client/src/contextmenus/airbasespawnmenu.ts b/client/src/contextmenus/airbasespawnmenu.ts index a5c40e3c..ff753c6e 100644 --- a/client/src/contextmenus/airbasespawnmenu.ts +++ b/client/src/contextmenus/airbasespawnmenu.ts @@ -46,7 +46,7 @@ export class AirbaseSpawnContextMenu extends ContextMenu { * @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 */ - show(x: number, y: number) { + show(x: number | undefined, y: number | undefined) { super.show(x, y, new LatLng(0, 0)); this.#aircraftSpawnMenu.setAirbase(undefined); diff --git a/client/src/contextmenus/contextmenu.ts b/client/src/contextmenus/contextmenu.ts index fb923e10..78d91713 100644 --- a/client/src/contextmenus/contextmenu.ts +++ b/client/src/contextmenus/contextmenu.ts @@ -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. * - * @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 latlng Leaflet latlng object of the mouse click + * @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. If undefined, use the old value + * @param latlng Leaflet latlng object of the mouse click. If undefined, use the old value */ - show(x: number, y: number, latlng: LatLng) { - this.#latlng = latlng; + show(x: number | undefined = undefined, y: number | undefined = undefined, latlng: LatLng | undefined = undefined) { + this.#latlng = latlng ?? this.#latlng; this.#container?.classList.toggle("hide", false); - this.#x = x; - this.#y = y; + this.#x = x ?? this.#x; + this.#y = y ?? this.#y; this.clip(); this.getContainer()?.dispatchEvent(new Event("show")); } diff --git a/client/src/contextmenus/unitcontextmenu.ts b/client/src/contextmenus/unitcontextmenu.ts index fe1ee735..57502abb 100644 --- a/client/src/contextmenus/unitcontextmenu.ts +++ b/client/src/contextmenus/unitcontextmenu.ts @@ -1,4 +1,4 @@ -import { deg2rad, ftToM } from "../other/utils"; +import { ContextActionSet } from "../unit/contextactionset"; 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. */ @@ -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 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) { - this.getContainer()?.replaceChildren(...Object.keys(options).map((key: string, idx: number) => { - const option = options[key]; + setContextActions(contextActionSet: ContextActionSet) { + this.getContainer()?.replaceChildren(...Object.keys(contextActionSet.getContextActions()).map((key: string, idx: number) => { + const contextAction = contextActionSet.getContextActions()[key]; var button = document.createElement("button"); var el = document.createElement("div"); - el.title = option.tooltip; - el.innerText = option.text; + el.title = contextAction.getDescription(); + el.innerText = contextAction.getLabel(); el.id = key; - button.addEventListener("click", () => callback(key)); + button.addEventListener("click", () => { + contextAction.executeCallback(); + if (contextAction.getHideContextAfterExecution()) + this.hide(); + }); button.appendChild(el); return (button); })); diff --git a/client/src/map/map.ts b/client/src/map/map.ts index b176e137..f4df7ecd 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -12,16 +12,16 @@ import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; 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 { CoalitionArea } from "./coalitionarea/coalitionarea"; import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu"; import { DrawingCursor } from "./coalitionarea/drawingcursor"; import { AirbaseSpawnContextMenu } from "../contextmenus/airbasespawnmenu"; -import { Popup } from "../popups/popup"; import { GestureHandling } from "leaflet-gesture-handling"; import { TouchBoxSelect } from "./touchboxselect"; import { DestinationPreviewHandle } from "./markers/destinationpreviewHandle"; +import { ContextActionSet } from "../unit/contextactionset"; var hasTouchScreen = false; //if ("maxTouchPoints" in navigator) @@ -322,7 +322,7 @@ export class Map extends L.Map { 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.#unitContextMenu.show(x, y, latlng); } @@ -335,7 +335,7 @@ export class Map extends L.Map { 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.#airbaseContextMenu.show(x, y, latlng); this.#airbaseContextMenu.setAirbase(airbase); @@ -349,7 +349,7 @@ export class Map extends L.Map { 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.#airbaseSpawnMenu.show(x, y); this.#airbaseSpawnMenu.setAirbase(airbase); @@ -561,9 +561,9 @@ export class Map extends L.Map { } else if (this.#state === MOVE_UNIT) { 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.#destinationRotationCenter = null; @@ -611,59 +611,15 @@ export class Map extends L.Map { if (e.originalEvent.button != 2 || e.originalEvent.ctrlKey || e.originalEvent.shiftKey) return; - var options: { [key: string]: { text: string, tooltip: string } } = {}; - const selectedUnits = getApp().getUnitsManager().getSelectedUnits(); - const selectedUnitTypes = getApp().getUnitsManager().getSelectedUnitsCategories(); - - 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 (selectedUnits.every((unit: Unit) => { return unit.canTargetPoint()})) { - 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()); - } - }); + var contextActionSet = new ContextActionSet(); + var units = getApp().getUnitsManager().getSelectedUnits(); + units.forEach((unit: Unit) => { + unit.appendContextActions(contextActionSet, null, e.latlng); + }) + + if (Object.keys(contextActionSet.getContextActions()).length > 0) { + getApp().getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng); + getApp().getMap().getUnitContextMenu().setContextActions(contextActionSet); } }, 150); this.#longPressHandled = false; @@ -742,6 +698,8 @@ export class Map extends L.Map { const toggles = `["${control.toggles.join('","')}"]`; const div = document.createElement("div"); div.className = control.protectable === true ? "protectable" : ""; + + // TODO: for consistency let's avoid using innerHTML. Let's create elements. div.innerHTML = ` diff --git a/client/views/toolbars/primary.ejs b/client/views/toolbars/primary.ejs index 439b35ad..f7cf4c0b 100644 --- a/client/views/toolbars/primary.ejs +++ b/client/views/toolbars/primary.ejs @@ -6,7 +6,7 @@

DCS Olympus

-
version v0.4.6-alpha
+
version v0.4.7-alpha
Discord diff --git a/installer/olympus.iss b/installer/olympus.iss index fc4e6f82..04904a3b 100644 --- a/installer/olympus.iss +++ b/installer/olympus.iss @@ -1,5 +1,5 @@ #define nwjsFolder "C:\Users\dpass\Documents\nwjs\" -#define version "v0.4.6-alpha" +#define version "v0.4.7-alpha" [Setup] AppName=DCS Olympus diff --git a/mod/entry.lua b/mod/entry.lua index 6dea1156..2a0ecb73 100644 --- a/mod/entry.lua +++ b/mod/entry.lua @@ -15,7 +15,7 @@ declare_plugin(self_ID, shortName = "Olympus", fileMenuName = "Olympus", - version = "v0.4.6-alpha", + version = "v0.4.7-alpha", state = "installed", 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."), diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index f05d7cf3..291b139b 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -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 @@ -180,7 +180,7 @@ function Olympus.buildTask(groupName, options) if options ['altitudeType'] then if options ['altitudeType'] == "AGL" then local groundHeight = 0 - if group then + if group ~= nil then local groupPos = mist.getLeadPos(group) groundHeight = land.getHeight({x = groupPos.x, y = groupPos.z}) end @@ -287,7 +287,7 @@ end 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) local group = Group.getByName(groupName) - if group then + if group ~= nil then if category == "Aircraft" then local startPoint = mist.getLeadPos(group) local endPoint = coord.LLtoLO(lat, lng, 0) @@ -765,9 +765,9 @@ end -- Find a database entry by ID function Olympus.findInDatabase(ID) - for idx, unit in pairs(Olympus.cloneDatabase) do - if unit ~= nil and unit["ID"] == ID then - return unit + for idx, unitRecord in pairs(Olympus.cloneDatabase) do + if unitRecord ~= nil and unitRecord["ID"] == ID then + return unitRecord end end return nil @@ -789,11 +789,11 @@ function Olympus.clone(cloneTable, deleteOriginal) -- All the units in the table will be cloned in a single group for idx, cloneData in pairs(cloneTable) do 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 - local unitTable = mist.utils.deepCopy(unit) + local unitTable = mist.utils.deepCopy(unitRecord) local point = coord.LLtoLO(cloneData['lat'], cloneData['lng'], 0) if unitTable then @@ -803,8 +803,8 @@ function Olympus.clone(cloneTable, deleteOriginal) end if countryID == nil and category == nil then - countryID = unit["country"] - if unit["category"] == Unit.Category.AIRPLANE then + countryID = unitRecord["country"] + if unitRecord["category"] == Unit.Category.AIRPLANE then category = 'plane' route = { ["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' route = { ["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' - elseif unit["category"] == Unit.Category.SHIP then + elseif unitRecord["category"] == Unit.Category.SHIP then category = 'ship' end end @@ -884,7 +884,7 @@ end function Olympus.delete(ID, explosion, explosionType) Olympus.debug("Olympus.delete " .. ID .. " " .. tostring(explosion), 2) local unit = Olympus.getUnitByID(ID) - if unit then + if unit ~= nil and unit:isExist() then if unit:getPlayerName() or explosion then if explosionType == nil then explosionType = "normal" @@ -903,7 +903,7 @@ end function Olympus.setTask(groupName, taskOptions) Olympus.debug("Olympus.setTask " .. groupName .. " " .. Olympus.serializeTable(taskOptions), 2) local group = Group.getByName(groupName) - if group then + if group ~= nil then local task = Olympus.buildTask(groupName, taskOptions); Olympus.debug("Olympus.setTask " .. Olympus.serializeTable(task), 20) if task then @@ -917,7 +917,7 @@ end function Olympus.resetTask(groupName) Olympus.debug("Olympus.resetTask " .. groupName, 2) local group = Group.getByName(groupName) - if group then + if group ~= nil then group:getController():resetTask() Olympus.debug("Olympus.resetTask completed successfully", 2) end @@ -927,7 +927,7 @@ end function Olympus.setCommand(groupName, command) Olympus.debug("Olympus.setCommand " .. groupName .. " " .. Olympus.serializeTable(command), 2) local group = Group.getByName(groupName) - if group then + if group ~= nil then group:getController():setCommand(command) Olympus.debug("Olympus.setCommand completed successfully", 2) end @@ -937,7 +937,7 @@ end function Olympus.setOption(groupName, optionID, optionValue) Olympus.debug("Olympus.setOption " .. groupName .. " " .. optionID .. " " .. tostring(optionValue), 2) local group = Group.getByName(groupName) - if group then + if group ~= nil then group:getController():setOption(optionID, optionValue) Olympus.debug("Olympus.setOption completed successfully", 2) end @@ -947,7 +947,7 @@ end function Olympus.setOnOff(groupName, onOff) Olympus.debug("Olympus.setOnOff " .. groupName .. " " .. tostring(onOff), 2) local group = Group.getByName(groupName) - if group then + if group ~= nil then group:getController():setOnOff(onOff) Olympus.debug("Olympus.setOnOff completed successfully", 2) end @@ -965,11 +965,11 @@ function Olympus.setUnitsData(arg, time) 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 if index > startIndex then - if unit ~= nil then + if unit ~= nil and unit:isExist() then local table = {} -- Get the object category in Olympus name - local objectCategory = unit:getCategory() + local objectCategory = Object.getCategory(unit) if objectCategory == Object.Category.UNIT then if unit:getDesc().category == Unit.Category.AIRPLANE then 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 if index > startIndex then - if weapon ~= nil then + if weapon ~= nil and weapon:isExist() then local table = {} -- Get the object category in Olympus name - local objectCategory = weapon:getCategory() + local objectCategory = Object.getCategory(weapon) if objectCategory == Object.Category.WEAPON then if weapon:getDesc().category == Weapon.Category.MISSILE then table["category"] = "Missile" @@ -1216,7 +1216,7 @@ function Olympus.initializeUnits() if mist and mist.DBs and mist.DBs.MEunitsById then for id, unitsTable in pairs(mist.DBs.MEunitsById) do local unit = Unit.getByName(unitsTable["unitName"]) - if unit then + if unit ~= nil and unit:isExist() then Olympus.units[unit["id_"]] = unit end end diff --git a/scripts/OlympusHook.lua b/scripts/OlympusHook.lua index a34af85c..261023eb 100644 --- a/scripts/OlympusHook.lua +++ b/scripts/OlympusHook.lua @@ -1,4 +1,4 @@ -local version = 'v0.4.6-alpha' +local version = 'v0.4.7-alpha' Olympus = {} Olympus.OlympusDLL = nil diff --git a/src/shared/include/defines.h b/src/shared/include/defines.h index d695109e..1506bfd2 100644 --- a/src/shared/include/defines.h +++ b/src/shared/include/defines.h @@ -1,6 +1,6 @@ #pragma once -#define VERSION "v0.4.6-alpha" +#define VERSION "v0.4.7-alpha" #define LOG_NAME "Olympus_log.txt" #define REST_ADDRESS "http://localhost:30000" #define REST_URI "olympus"