diff --git a/client/demo.js b/client/demo.js index 4dc6a2b4..63e7940c 100644 --- a/client/demo.js +++ b/client/demo.js @@ -107,7 +107,7 @@ const DEMO_UNIT_DATA = { ["3"]:{ baseData: { AI: true, - name: "2S6 Tunguska", + name: "M-60", unitName: "Olympus 1-3", groupName: "Group 4", alive: true, diff --git a/client/public/stylesheets/aic/aic.css b/client/public/stylesheets/aic/aic.css index 16022448..ea972d71 100644 --- a/client/public/stylesheets/aic/aic.css +++ b/client/public/stylesheets/aic/aic.css @@ -130,7 +130,7 @@ padding: 10px; position: absolute; width: fit-content; - z-index: 1000; + z-index: 9999; } .aic-enabled #aic-teleprompt { diff --git a/client/public/stylesheets/layout/layout.css b/client/public/stylesheets/layout/layout.css index f6fa464c..7ae3c7b5 100644 --- a/client/public/stylesheets/layout/layout.css +++ b/client/public/stylesheets/layout/layout.css @@ -11,7 +11,7 @@ left: 10px; position: absolute; top: 10px; - z-index: 1000; + z-index: 9999; } #app-icon>.ol-select-options { @@ -39,7 +39,7 @@ position: absolute; right: 10px; width: 180px; - z-index: 1000; + z-index: 9999; } #mouse-info-panel { @@ -51,7 +51,7 @@ right: 10px; row-gap: 10px; width: 180px; - z-index: 1000; + z-index: 9999; } #unit-control-panel { @@ -60,7 +60,7 @@ position: absolute; top: 80px; width: 320px; - z-index: 1000; + z-index: 9999; } #unit-info-panel { @@ -69,7 +69,7 @@ left: 10px; position: absolute; width: fit-content; - z-index: 1000; + z-index: 9999; padding: 24px 30px; } diff --git a/client/public/stylesheets/markers/units.css b/client/public/stylesheets/markers/units.css index 8447f4d1..65ec1d4b 100644 --- a/client/public/stylesheets/markers/units.css +++ b/client/public/stylesheets/markers/units.css @@ -260,7 +260,14 @@ background-image: url("/resources/theme/images/states/idle.svg"); } -[data-object|="unit"][data-state="attack"] .unit-state { +[data-object*="groundunit"][data-state="idle"] .unit-state { + background-image: url(""); /* To avoid clutter, dont show the idle state for non flying units */ +} + +[data-object|="unit"][data-state="attack"] .unit-state, +[data-object|="unit"][data-state="bombing point"] .unit-state, +[data-object|="unit"][data-state="carpet bombing"] .unit-state, +[data-object|="unit"][data-state="firing at area"] .unit-state { background-image: url("/resources/theme/images/states/attack.svg"); } @@ -280,6 +287,10 @@ background-image: url("/resources/theme/images/states/dcs.svg"); } +[data-object|="unit"][data-state="no-task"] .unit-state { + background-image: url("/resources/theme/images/states/no-task.svg"); +} + /*** Dead unit ***/ [data-object|="unit-aircraft"][data-is-dead] .unit-selected-spotlight, [data-object|="unit-aircraft"][data-is-dead] .unit-short-label, diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 7fe17ff9..d4afca30 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -34,6 +34,10 @@ body { width: 100%; } +.hidden-cursor { + cursor: none !important; +} + a { text-decoration: none; } @@ -203,7 +207,7 @@ form>div { max-height: 0; overflow: hidden; position: absolute; - z-index: 1000; + z-index: 9999; } .ol-select-options.scrollbar-visible { @@ -709,7 +713,6 @@ nav.ol-panel> :last-child { position: relative; row-gap: 10px; width: 50%; - z-index: 10; } #splash-content::after { @@ -860,16 +863,18 @@ nav.ol-panel> :last-child { } .ol-destination-preview-icon { - background-color: var(--secondary-yellow); - border-radius: 999px; - cursor: grab; + background-image: url("/resources/theme/images/markers/move.svg"); height: 52px; pointer-events: none; width: 52px; } -.ol-destination-preview { +.ol-target-icon { + background-image: url("/resources/theme/images/markers/target.svg"); + height: 52px; pointer-events: none; + width: 52px; + z-index: 9999; } dl.ol-data-grid { @@ -933,7 +938,7 @@ dl.ol-data-grid dd { color: white; justify-self: center; position: absolute; - z-index: 1000; + z-index: 9999; } .ol-panel.ol-dialog { diff --git a/client/public/stylesheets/other/contextmenus.css b/client/public/stylesheets/other/contextmenus.css index 57fc1341..9f50a4d4 100644 --- a/client/public/stylesheets/other/contextmenus.css +++ b/client/public/stylesheets/other/contextmenus.css @@ -271,7 +271,7 @@ position: absolute; row-gap: 5px; width: fit-content; - z-index: 1000; + z-index: 9999; } #unit-contextmenu button { @@ -307,6 +307,18 @@ content: url("/resources/theme/images/icons/sword.svg"); } +#bomb::before { + content: url("/resources/theme/images/icons/crosshairs-solid.svg"); +} + +#carpet-bomb::before { + content: url("/resources/theme/images/icons/explosion-solid.svg"); +} + +#fire-at-area::before { + content: url("/resources/theme/images/icons/crosshairs-solid.svg"); +} + #follow::before { content: url("/resources/theme/images/icons/follow.svg"); } @@ -393,5 +405,5 @@ position: absolute; row-gap: 5px; width: 180px; - z-index: 1000; + z-index: 9999; } diff --git a/client/public/themes/olympus/images/icons/triangle-exclamation-solid.svg b/client/public/themes/olympus/images/icons/triangle-exclamation-solid.svg new file mode 100644 index 00000000..bb69b55f --- /dev/null +++ b/client/public/themes/olympus/images/icons/triangle-exclamation-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/markers/move.svg b/client/public/themes/olympus/images/markers/move.svg new file mode 100644 index 00000000..4af0aa8e --- /dev/null +++ b/client/public/themes/olympus/images/markers/move.svg @@ -0,0 +1,116 @@ + + + + diff --git a/client/public/themes/olympus/images/markers/target.svg b/client/public/themes/olympus/images/markers/target.svg new file mode 100644 index 00000000..7afbf612 --- /dev/null +++ b/client/public/themes/olympus/images/markers/target.svg @@ -0,0 +1,101 @@ + + + + diff --git a/client/public/themes/olympus/images/states/no-task.svg b/client/public/themes/olympus/images/states/no-task.svg new file mode 100644 index 00000000..2e1e906d --- /dev/null +++ b/client/public/themes/olympus/images/states/no-task.svg @@ -0,0 +1,51 @@ + + diff --git a/client/src/@types/unit.d.ts b/client/src/@types/unit.d.ts index d6163f68..9f70f291 100644 --- a/client/src/@types/unit.d.ts +++ b/client/src/@types/unit.d.ts @@ -40,10 +40,12 @@ interface TaskData { targetSpeedType: string; targetAltitude: number; targetAltitudeType: string; + targetLocation: any; isTanker: boolean; isAWACS: boolean; onOff: boolean; followRoads: boolean; + targetID: number; } interface OptionsData { diff --git a/client/src/map/map.ts b/client/src/map/map.ts index eaf0951d..83339a7a 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -13,6 +13,7 @@ import { TemporaryUnitMarker } from "./temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; import { SVGInjector } from '@tanem/svg-injector' import { layers as mapLayers, mapBounds, minimapBoundaries } from "../constants/constants"; +import { TargetMarker } from "./targetmarker"; L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect); @@ -20,12 +21,16 @@ L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect); require("../../public/javascripts/leaflet.nauticscale.js") /* Map constants */ -export const IDLE = "IDLE"; -export const UNIT_SELECTED = "MOVE_UNIT"; +export const IDLE = "Idle"; +export const MOVE_UNIT = "Move unit"; +export const BOMBING = "Bombing"; +export const CARPET_BOMBING = "Carpet bombing"; +export const FIRE_AT_AREA = "Fire at area"; export const visibilityControls: string[] = ["human", "dcs", "aircraft", "groundunit-sam", "groundunit-other", "navyunit", "airbase"]; export const visibilityControlsTootlips: string[] = ["Toggle human players visibility", "Toggle DCS controlled units visibility", "Toggle aircrafts visibility", "Toggle SAM units visibility", "Toggle ground units (not SAM) visibility", "Toggle navy units visibility", "Toggle airbases visibility"]; export class Map extends L.Map { + #ID: string; #state: string; #layer: L.TileLayer | null = null; #preventLeftClick: boolean = false; @@ -40,8 +45,9 @@ export class Map extends L.Map { #centerUnit: Unit | null = null; #miniMap: ClickableMiniMap | null = null; #miniMapLayerGroup: L.LayerGroup; - #temporaryMarkers: L.Marker[] = []; - #destinationPreviewMarkers: L.Marker[] = []; + #temporaryMarkers: TemporaryUnitMarker[] = []; + #destinationPreviewMarkers: DestinationPreviewMarker[] = []; + #targetMarker: TargetMarker; #destinationGroupRotation: number = 0; #computeDestinationRotation: boolean = false; #destinationRotationCenter: L.LatLng | null = null; @@ -55,10 +61,13 @@ export class Map extends L.Map { constructor(ID: string) { /* Init the leaflet map */ + //@ts-ignore super(ID, { doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true, zoomAnimation: true, maxBoundsViscosity: 1.0, minZoom: 7, keyboard: true, keyboardPanDelta: 0 }); this.setView([37.23, -115.8], 10); + this.#ID = ID; + this.setLayer(Object.keys(mapLayers)[0]); /* Minimap */ @@ -122,6 +131,9 @@ export class Map extends L.Map { return this.#createOptionButton(option, `visibility/${option.toLowerCase()}.svg`, visibilityControlsTootlips[index], "toggleUnitVisibility", `{"type": "${option}"}`); }); document.querySelector("#unit-visibility-control")?.append(...this.#optionButtons["visibility"]); + + /* Markers */ + this.#targetMarker = new TargetMarker(new L.LatLng(0, 0), {interactive: false}); } setLayer(layerName: string) { @@ -149,31 +161,20 @@ export class Map extends L.Map { setState(state: string) { this.#state = state; if (this.#state === IDLE) { - /* Remove all the destination preview markers */ - this.#destinationPreviewMarkers.forEach((marker: L.Marker) => { - this.removeLayer(marker); - }) - this.#destinationPreviewMarkers = []; - - this.#destinationGroupRotation = 0; - this.#computeDestinationRotation = false; - this.#destinationRotationCenter = null; + this.#resetDestinationMarkers(); + this.#resetTargetMarker(); + this.#showCursor(); } - else if (this.#state === UNIT_SELECTED) { - /* Remove all the exising destination preview markers */ - this.#destinationPreviewMarkers.forEach((marker: L.Marker) => { - this.removeLayer(marker); - }) - this.#destinationPreviewMarkers = []; - - if (getUnitsManager().getSelectedUnits({ excludeHumans: true }).length > 1 && getUnitsManager().getSelectedUnits({ excludeHumans: true }).length < 20) { - /* Create the unit destination preview markers */ - this.#destinationPreviewMarkers = getUnitsManager().getSelectedUnits({ excludeHumans: true }).map((unit: Unit) => { - var marker = new DestinationPreviewMarker(this.getMouseCoordinates(), {interactive: false}); - marker.addTo(this); - return marker; - }) - } + else if (this.#state === MOVE_UNIT) { + this.#resetTargetMarker(); + this.#createDestinationMarkers(); + if (this.#destinationPreviewMarkers.length > 0) + this.#hideCursor(); + } + else if ([BOMBING, CARPET_BOMBING, FIRE_AT_AREA].includes(this.#state)) { + this.#resetDestinationMarkers(); + this.#createTargetMarker(); + this.#hideCursor(); } document.dispatchEvent(new CustomEvent("mapStateChanged")); } @@ -363,7 +364,7 @@ export class Map extends L.Map { if (this.#state === IDLE) { } - else if (this.#state === UNIT_SELECTED) { + else { this.setState(IDLE); getUnitsManager().deselectAllUnits(); } @@ -381,38 +382,26 @@ export class Map extends L.Map { this.showMapContextMenu(e); } } - else if (this.#state === UNIT_SELECTED) { - if (e.originalEvent.shiftKey) { - var options: {[key: string]: {text: string, tooltip: string}} = {}; - var selectedUnitTypes = getUnitsManager().getSelectedUnitsTypes(); - - if (selectedUnitTypes.length === 1 && ["Aircraft"].includes(selectedUnitTypes[0])) - { - options["bomb"] = {text: "Bomb here", tooltip: "Precision bombing of this specific point"}; - options["carpet-bomb"] = {text: "Carpet bomb", tooltip: "Carpet bombing around this point"}; - options["building-bomb"] = {text: "Bomb building", tooltip: "Precision bombing of the building closest to this point"}; - } - - if (selectedUnitTypes.length === 1 && ["GroundUnit"].includes(selectedUnitTypes[0])) - options["fire-at-area"] = {text: "Fire at area", tooltip: "Fire at this point"}; - - if (Object.keys(options).length > 0) { - this.showUnitContextMenu(e); - this.getUnitContextMenu().setOptions(options, (option: string) => { - this.hideUnitContextMenu(); - this.#executeAction(e, option); - }); - } - - } else { - if (!e.originalEvent.ctrlKey) { - getUnitsManager().selectedUnitsClearDestinations(); - } - getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, e.originalEvent.shiftKey, this.#destinationGroupRotation) - this.#destinationGroupRotation = 0; - this.#destinationRotationCenter = null; - this.#computeDestinationRotation = false; + else if (this.#state === MOVE_UNIT) { + if (!e.originalEvent.ctrlKey) { + getUnitsManager().selectedUnitsClearDestinations(); } + getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, e.originalEvent.shiftKey, this.#destinationGroupRotation) + this.#destinationGroupRotation = 0; + this.#destinationRotationCenter = null; + this.#computeDestinationRotation = false; + } + else if (this.#state === BOMBING) { + getUnitsManager().getSelectedUnits().length > 0? this.setState(MOVE_UNIT): this.setState(IDLE); + getUnitsManager().selectedUnitsBombPoint(this.getMouseCoordinates()); + } + else if (this.#state === CARPET_BOMBING) { + getUnitsManager().getSelectedUnits().length > 0? this.setState(MOVE_UNIT): this.setState(IDLE); + getUnitsManager().selectedUnitsCarpetBomb(this.getMouseCoordinates()); + } + else if (this.#state === FIRE_AT_AREA) { + getUnitsManager().getSelectedUnits().length > 0? this.setState(MOVE_UNIT): this.setState(IDLE); + getUnitsManager().selectedUnitsFireAtArea(this.getMouseCoordinates()); } } @@ -439,7 +428,7 @@ export class Map extends L.Map { #onMouseDown(e: any) { this.hideAllContextMenus(); - if (this.#state == UNIT_SELECTED) { + if (this.#state == MOVE_UNIT) { this.#destinationGroupRotation = 0; this.#destinationRotationCenter = null; this.#computeDestinationRotation = false; @@ -457,10 +446,14 @@ export class Map extends L.Map { this.#lastMousePosition.x = e.originalEvent.x; this.#lastMousePosition.y = e.originalEvent.y; - if (this.#computeDestinationRotation && this.#destinationRotationCenter != null) - this.#destinationGroupRotation = -bearing(this.#destinationRotationCenter.lat, this.#destinationRotationCenter.lng, this.getMouseCoordinates().lat, this.getMouseCoordinates().lng); - - this.#updateDestinationPreview(e); + if (this.#state === MOVE_UNIT){ + if (this.#computeDestinationRotation && this.#destinationRotationCenter != null) + this.#destinationGroupRotation = -bearing(this.#destinationRotationCenter.lat, this.#destinationRotationCenter.lng, this.getMouseCoordinates().lat, this.getMouseCoordinates().lng); + this.#updateDestinationPreview(e); + } + else if ([BOMBING, CARPET_BOMBING, FIRE_AT_AREA].includes(this.#state)) { + this.#targetMarker.setLatLng(this.getMouseCoordinates()); + } } #onZoom(e: any) { @@ -497,5 +490,48 @@ export class Map extends L.Map { button.setAttribute("data-on-click-params", argument); return button; } + + #createDestinationMarkers() { + this.#resetDestinationMarkers(); + + if (getUnitsManager().getSelectedUnits({ excludeHumans: true }).length > 0 && getUnitsManager().getSelectedUnits({ excludeHumans: true }).length < 20) { + /* Create the unit destination preview markers */ + this.#destinationPreviewMarkers = getUnitsManager().getSelectedUnits({ excludeHumans: true }).map((unit: Unit) => { + var marker = new DestinationPreviewMarker(this.getMouseCoordinates(), {interactive: false}); + marker.addTo(this); + return marker; + }) + } + } + + #resetDestinationMarkers() { + /* Remove all the destination preview markers */ + this.#destinationPreviewMarkers.forEach((marker: L.Marker) => { + this.removeLayer(marker); + }) + this.#destinationPreviewMarkers = []; + + this.#destinationGroupRotation = 0; + this.#computeDestinationRotation = false; + this.#destinationRotationCenter = null; + } + + #createTargetMarker(){ + this.#resetTargetMarker(); + this.#targetMarker.addTo(this); + } + + #resetTargetMarker() { + this.#targetMarker.setLatLng(new L.LatLng(0, 0)); + this.removeLayer(this.#targetMarker); + } + + #showCursor() { + document.getElementById(this.#ID)?.classList.remove("hidden-cursor"); + } + + #hideCursor() { + document.getElementById(this.#ID)?.classList.add("hidden-cursor"); + } } diff --git a/client/src/map/targetmarker.ts b/client/src/map/targetmarker.ts new file mode 100644 index 00000000..30232dc1 --- /dev/null +++ b/client/src/map/targetmarker.ts @@ -0,0 +1,18 @@ +import { DivIcon } from "leaflet"; +import { CustomMarker } from "./custommarker"; + +export class TargetMarker extends CustomMarker { + #interactive: boolean = false; + + createIcon() { + this.setIcon(new DivIcon({ + iconSize: [52, 52], + iconAnchor: [26, 26], + className: "leaflet-target-marker", + })); + var el = document.createElement("div"); + el.classList.add("ol-target-icon"); + el.classList.toggle("ol-target-icon-interactive", this.#interactive) + this.getElement()?.appendChild(el); + } +} diff --git a/client/src/server/server.ts b/client/src/server/server.ts index f92a1204..01317c18 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -127,8 +127,8 @@ export function spawnSmoke(color: string, latlng: LatLng) { POST(data, () => { }); } -export function spawnExplosion(strength: number, latlng: LatLng) { - var command = { "strength": strength, "location": latlng }; +export function spawnExplosion(intensity: number, latlng: LatLng) { + var command = { "intensity": intensity, "location": latlng }; var data = { "explosion": command } POST(data, () => { }); } diff --git a/client/src/units/aircraftdatabase.ts b/client/src/units/aircraftdatabase.ts index f2448468..845f5aba 100644 --- a/client/src/units/aircraftdatabase.ts +++ b/client/src/units/aircraftdatabase.ts @@ -2603,7 +2603,7 @@ export class AircraftDatabase extends UnitDatabase { } ], "roles": [ - "Recon" + "Reconnaissance" ], "code": "R-60M*2", "name": "Heavy / Fox 2 / Long Range" diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index fb4ab994..0fd1d761 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -7,6 +7,8 @@ import { groundUnitsDatabase } from './groundunitsdatabase'; import { CustomMarker } from '../map/custommarker'; import { SVGInjector } from '@tanem/svg-injector'; import { UnitDatabase } from './unitdatabase'; +import { BOMBING, CARPET_BOMBING, FIRE_AT_AREA, IDLE, MOVE_UNIT } from '../map/map'; +import { TargetMarker } from '../map/targetmarker'; var pathIcon = new Icon({ iconUrl: '/resources/theme/images/markers/marker-icon.png', @@ -52,10 +54,12 @@ export class Unit extends CustomMarker { targetSpeedType: "GS", targetAltitude: 0, targetAltitudeType: "AGL", + targetLocation: {}, isTanker: false, isAWACS: false, onOff: true, - followRoads: false + followRoads: false, + targetID: 0 }, optionsData: { ROE: "", @@ -78,6 +82,8 @@ export class Unit extends CustomMarker { #pathPolyline: Polyline; #targetsPolylines: Polyline[]; #miniMapMarker: CircleMarker | null = null; + #targetLocationMarker: TargetMarker; + #targetLocationPolyline: Polyline; #timer: number = 0; @@ -109,6 +115,9 @@ export class Unit extends CustomMarker { this.#pathPolyline.addTo(getMap()); this.#targetsPolylines = []; + this.#targetLocationMarker = new TargetMarker(new LatLng(0, 0)); + this.#targetLocationPolyline = new Polyline([], { color: '#FF0000', weight: 3, opacity: 0.5, smoothFactor: 1 }); + /* Deselect units if they are hidden */ document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => { window.setTimeout(() => { this.setSelected(this.getSelected() && !this.getHidden()) }, 300); @@ -152,11 +161,16 @@ export class Unit extends CustomMarker { if ((this.getBaseData().alive || !selected) && this.getSelectable() && this.getSelected() != selected) { this.#selected = selected; this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-selected", selected); - if (selected) + if (selected) { document.dispatchEvent(new CustomEvent("unitSelection", { detail: this })); - else + this.#updateMarker(); + } + else { document.dispatchEvent(new CustomEvent("unitDeselection", { detail: this })); - this.getGroupMembers().forEach((unit: Unit) => unit.setSelected(selected)); + this.#clearDetectedUnits(); + this.#clearPath(); + this.#clearTarget(); + } } } @@ -228,13 +242,17 @@ export class Unit extends CustomMarker { if (updateMarker) this.#updateMarker(); - this.#clearTargets(); + this.#clearDetectedUnits(); if (this.getSelected()) { this.#drawPath(); - this.#drawTargets(); + this.#drawDetectedUnits(); + this.#drawTarget(); } - else + else { this.#clearPath(); + this.#clearTarget(); + } + document.dispatchEvent(new CustomEvent("unitUpdated", { detail: this })); } @@ -404,6 +422,15 @@ export class Unit extends CustomMarker { return getUnitsManager().getUnitByID(this.getFormationData().leaderID); } + canRole(roles: string | string[]) { + if (typeof(roles) === "string") + roles = [roles]; + + return this.getDatabase()?.getByName(this.getBaseData().name)?.loadouts.some((loadout: LoadoutBlueprint) => { + return (roles as string[]).some((role: string) => {return loadout.roles.includes(role)}); + }); + } + /********************** Unit commands *************************/ addDestination(latlng: L.LatLng) { if (!this.getMissionData().flags.Human) { @@ -543,10 +570,9 @@ export class Unit extends CustomMarker { /***********************************************/ #onClick(e: any) { if (!this.#preventClick) { - if (getMap().getState() === 'IDLE' || getMap().getState() === 'MOVE_UNIT' || e.originalEvent.ctrlKey) { - if (!e.originalEvent.ctrlKey) { + if (getMap().getState() === IDLE || getMap().getState() === MOVE_UNIT || e.originalEvent.ctrlKey) { + if (!e.originalEvent.ctrlKey) getUnitsManager().deselectAllUnits(); - } this.setSelected(!this.getSelected()); } } @@ -563,20 +589,35 @@ export class Unit extends CustomMarker { #onContextMenu(e: any) { var options: {[key: string]: {text: string, tooltip: string}} = {}; + const selectedUnits = getUnitsManager().getSelectedUnits(); + const selectedUnitTypes = getUnitsManager().getSelectedUnitsTypes(); options["center-map"] = {text: "Center map", tooltip: "Center the map on the unit and follow it"}; - if (getUnitsManager().getSelectedUnits().length > 0 && !(getUnitsManager().getSelectedUnits().length == 1 && (getUnitsManager().getSelectedUnits().includes(this)))) { + if (selectedUnits.length > 0 && !(selectedUnits.length == 1 && (selectedUnits.includes(this)))) { options["attack"] = {text: "Attack", tooltip: "Attack the unit using A/A or A/G weapons"}; if (getUnitsManager().getSelectedUnitsTypes().length == 1 && getUnitsManager().getSelectedUnitsTypes()[0] === "Aircraft") options["follow"] = {text: "Follow", tooltip: "Follow the unit at a user defined distance and position"};; } - else if ((getUnitsManager().getSelectedUnits().length > 0 && (getUnitsManager().getSelectedUnits().includes(this))) || getUnitsManager().getSelectedUnits().length == 0) { + else if ((selectedUnits.length > 0 && (selectedUnits.includes(this))) || selectedUnits.length == 0) { if (this.getBaseData().category == "Aircraft") { options["refuel"] = {text: "Air to air refuel", tooltip: "Refuel unit at the nearest AAR Tanker. If no tanker is available the unit will RTB."}; // TODO Add some way of knowing which aircraft can AAR } } + if ((selectedUnits.length === 0 && this.getBaseData().category == "Aircraft") || (selectedUnitTypes.length === 1 && ["Aircraft"].includes(selectedUnitTypes[0]))) + { + if (selectedUnits.concat([this]).every((unit: Unit) => {return unit.canRole(["CAS", "Strike"])})) { + 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 ((selectedUnits.length === 0 && this.getBaseData().category == "GroundUnit") || selectedUnitTypes.length === 1 && ["GroundUnit"].includes(selectedUnitTypes[0])) { + if (selectedUnits.concat([this]).every((unit: Unit) => {return unit.canRole(["Gun Artillery", "Rocket Artillery", "Infantry", "IFV", "Tank"])})) + options["fire-at-area"] = {text: "Fire at area", tooltip: "Fire at a large area"}; + } + if (Object.keys(options).length > 0) { getMap().showUnitContextMenu(e); getMap().getUnitContextMenu().setOptions(options, (option: string) => { @@ -595,6 +636,12 @@ export class Unit extends CustomMarker { getUnitsManager().selectedUnitsRefuel(); else if (action === "follow") this.#showFollowOptions(e); + else if (action === "bomb") + getMap().setState(BOMBING); + else if (action === "carpet-bomb") + getMap().setState(CARPET_BOMBING); + else if (action === "fire-at-area") + getMap().setState(FIRE_AT_AREA); } #showFollowOptions(e: any) { @@ -679,6 +726,8 @@ export class Unit extends CustomMarker { element.querySelector(".unit")?.setAttribute("data-state", "human"); else if (!this.getBaseData().AI) // Unit is under DCS control (not Olympus) element.querySelector(".unit")?.setAttribute("data-state", "dcs"); + else if ((this.getBaseData().category == "Aircraft" || this.getBaseData().category == "Helicopter") && !this.getMissionData().hasTask) + element.querySelector(".unit")?.setAttribute("data-state", "no-task"); else // Unit is under Olympus control element.querySelector(".unit")?.setAttribute("data-state", this.getTaskData().currentState.toLowerCase()); @@ -777,7 +826,7 @@ export class Unit extends CustomMarker { this.#pathPolyline.setLatLngs([]); } - #drawTargets() { + #drawDetectedUnits() { for (let index in this.getMissionData().targets) { var targetData = this.getMissionData().targets[index]; if (targetData.object != undefined){ @@ -795,7 +844,7 @@ export class Unit extends CustomMarker { color = "#00FF00"; else color = "#FFFFFF"; - var targetPolyline = new Polyline([startLatLng, endLatLng], { color: color, weight: 3, opacity: 0.4, smoothFactor: 1 }); + var targetPolyline = new Polyline([startLatLng, endLatLng], { color: color, weight: 3, opacity: 0.4, smoothFactor: 1, dashArray: "4, 8" }); targetPolyline.addTo(getMap()); this.#targetsPolylines.push(targetPolyline) } @@ -803,11 +852,48 @@ export class Unit extends CustomMarker { } } - #clearTargets() { + #clearDetectedUnits() { for (let index in this.#targetsPolylines) { getMap().removeLayer(this.#targetsPolylines[index]) } } + + #drawTarget() { + const targetLocation = this.getTaskData().targetLocation; + + if (targetLocation.latitude && targetLocation.longitude && targetLocation.latitude != 0 && targetLocation.longitude != 0) { + const lat = targetLocation.latitude; + const lng = targetLocation.longitude; + if (lat && lng) + this.#drawTargetLocation(new LatLng(lat, lng)); + } + else if (this.getTaskData().targetID != 0 && getUnitsManager().getUnitByID(this.getTaskData().targetID)) { + const flightData = getUnitsManager().getUnitByID(this.getTaskData().targetID)?.getFlightData(); + const lat = flightData?.latitude; + const lng = flightData?.longitude; + if (lat && lng) + this.#drawTargetLocation(new LatLng(lat, lng)); + } + else + this.#clearTarget(); + } + + #drawTargetLocation(targetLocation: LatLng) { + if (!getMap().hasLayer(this.#targetLocationMarker)) + this.#targetLocationMarker.addTo(getMap()); + if (!getMap().hasLayer(this.#targetLocationPolyline)) + this.#targetLocationPolyline.addTo(getMap()); + this.#targetLocationMarker.setLatLng(new LatLng(targetLocation.lat, targetLocation.lng)); + this.#targetLocationPolyline.setLatLngs([new LatLng(this.getFlightData().latitude, this.getFlightData().longitude), new LatLng(targetLocation.lat, targetLocation.lng)]) + } + + #clearTarget() { + if (getMap().hasLayer(this.#targetLocationMarker)) + this.#targetLocationMarker.removeFrom(getMap()); + + if (getMap().hasLayer(this.#targetLocationPolyline)) + this.#targetLocationPolyline.removeFrom(getMap()); + } } export class AirUnit extends Unit { diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index 05f839ee..dc3fd58b 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -3,7 +3,7 @@ import { getHotgroupPanel, getInfoPopup, getMap, getUnitDataTable } from ".."; import { Unit } from "./unit"; import { cloneUnit } from "../server/server"; import { deg2rad, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots } from "../other/utils"; -import { IDLE, UNIT_SELECTED } from "../map/map"; +import { IDLE, MOVE_UNIT } from "../map/map"; export class UnitsManager { #units: { [ID: number]: Unit }; @@ -52,10 +52,12 @@ export class UnitsManager { } addUnit(ID: number, data: UnitData) { + if (data.baseData && data.baseData.category){ /* The name of the unit category is exactly the same as the constructor name */ - var constructor = Unit.getConstructor(data.baseData.category); - if (constructor != undefined) { - this.#units[ID] = new constructor(ID, data); + var constructor = Unit.getConstructor(data.baseData.category); + if (constructor != undefined) { + this.#units[ID] = new constructor(ID, data); + } } } @@ -498,7 +500,7 @@ export class UnitsManager { #onUnitSelection(unit: Unit) { if (this.getSelectedUnits().length > 0) { - getMap().setState(UNIT_SELECTED); + getMap().setState(MOVE_UNIT); /* Disable the firing of the selection event for a certain amount of time. This avoids firing many events if many units are selected */ if (!this.#selectionEventDisabled) { window.setTimeout(() => { diff --git a/client/views/other/contextmenus.ejs b/client/views/other/contextmenus.ejs index 77a6006b..0940f076 100644 --- a/client/views/other/contextmenus.ejs +++ b/client/views/other/contextmenus.ejs @@ -84,10 +84,10 @@
diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index bde0dc53..71007d8d 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -140,50 +140,57 @@ function Olympus.buildTask(options) } } elseif options['id'] == 'Bombing' and options['lat'] and options['lng'] then + local point = coord.LLtoLO(options['lat'], options['lng'], 0) task = { id = 'Bombing', params = { - point = coord.LLtoLO(options['lat'], options['lng'], 0), + point = {x = point.x, y = point.z}, attackQty = 1 } } elseif options['id'] == 'CarpetBombing' and options['lat'] and options['lng'] then + local point = coord.LLtoLO(options['lat'], options['lng'], 0) task = { id = 'CarpetBombing', params = { - point = coord.LLtoLO(options['lat'], options['lng'], 0), - attackQty = 1, + x = point.x, + y = point.z, carpetLength = 1000, - attackType = 'Carpet' + attackType = 'Carpet', + expend = "All", + attackQty = 1, + attackQtyLimit = true } } elseif options['id'] == 'AttackMapObject' and options['lat'] and options['lng'] then + local point = coord.LLtoLO(options['lat'], options['lng'], 0) task = { id = 'AttackMapObject', params = { - point = coord.LLtoLO(options['lat'], options['lng'], 0), + point = {x = point.x, y = point.z} } } - end elseif options['id'] == 'FireAtPoint' and options['lat'] and options['lng'] and options['radius'] then + local point = coord.LLtoLO(options['lat'], options['lng'], 0) task = { - id = 'AttackMapObject', + id = 'FireAtPoint', params = { - point = coord.LLtoLO(options['lat'], options['lng'], 0), + point = {x = point.x, y = point.z}, radius = options['radius'] } } end + end return task end -- Move a unit. Since many tasks in DCS are Enroute tasks, this function is an important way to control the unit AI -function Olympus.move(ID, lat, lng, altitude, altitudeType, speed, speedType, category, taskOptions) - Olympus.debug("Olympus.move " .. ID .. " (" .. lat .. ", " .. lng ..") " .. altitude .. "m " .. altitudeType .. " ".. speed .. "m/s " .. category .. " " .. Olympus.serializeTable(taskOptions), 2) - local unit = Olympus.getUnitByID(ID) - if unit then +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 category == "Aircraft" then - local startPoint = mist.getLeadPos(unit:getGroup()) + local startPoint = mist.getLeadPos(group) local endPoint = coord.LLtoLO(lat, lng, 0) if altitudeType == "AGL" then @@ -221,7 +228,6 @@ function Olympus.move(ID, lat, lng, altitude, altitudeType, speed, speedType, ca }, }, } - group = unit:getGroup() local groupCon = group:getController() if groupCon then groupCon:setTask(missionTask) @@ -230,7 +236,7 @@ function Olympus.move(ID, lat, lng, altitude, altitudeType, speed, speedType, ca elseif category == "GroundUnit" then vars = { - group = unit:getGroup(), + group = group, point = coord.LLtoLO(lat, lng, 0), heading = 0, speed = speed @@ -249,7 +255,7 @@ function Olympus.move(ID, lat, lng, altitude, altitudeType, speed, speedType, ca Olympus.debug("Olympus.move not implemented yet for " .. category, 2) end else - Olympus.debug("Error in Olympus.move " .. ID, 2) + Olympus.debug("Error in Olympus.move " .. groupName, 2) end end @@ -408,8 +414,61 @@ function Olympus.spawnAircraft(coalition, unitType, lat, lng, alt, spawnOptions) }, } end + else + route = { + ["points"] = + { + [1] = + { + ["alt"] = alt, + ["alt_type"] = "BARO", + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + [1] = + { + ["number"] = 1, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "EPLRS", + ["params"] = + { + ["value"] = true + }, + }, + }, + }, + [2] = + { + ["number"] = 2, + ["auto"] = false, + ["id"] = "Orbit", + ["enabled"] = true, + ["params"] = + { + ["pattern"] = "Circle" + }, + }, + }, + }, + }, + ["type"] = "Turning Point", + ["x"] = spawnLocation.x, + ["y"] = spawnLocation.z, + }, -- end of [1] + }, -- end of ["points"] + } -- end of ["route"] end - + local vars = { units = unitTable, @@ -461,51 +520,51 @@ function Olympus.delete(ID, explosion) end end -function Olympus.setTask(ID, taskOptions) - Olympus.debug("Olympus.setTask " .. ID .. " " .. Olympus.serializeTable(taskOptions), 2) - local unit = Olympus.getUnitByID(ID) - if unit then +function Olympus.setTask(groupName, taskOptions) + Olympus.debug("Olympus.setTask " .. groupName .. " " .. Olympus.serializeTable(taskOptions), 2) + local group = Group.getByName(groupName) + if group then local task = Olympus.buildTask(taskOptions); Olympus.debug("Olympus.setTask " .. Olympus.serializeTable(task), 20) if task then - unit:getGroup():getController():setTask(task) + group:getController():setTask(task) Olympus.debug("Olympus.setTask completed successfully", 2) end end end -function Olympus.resetTask(ID) - Olympus.debug("Olympus.resetTask " .. ID, 2) - local unit = Olympus.getUnitByID(ID) - if unit then - unit:getGroup():getController():resetTask() +function Olympus.resetTask(groupName) + Olympus.debug("Olympus.resetTask " .. groupName, 2) + local group = Group.getByName(groupName) + if group then + group:getController():resetTask() Olympus.debug("Olympus.resetTask completed successfully", 2) end end -function Olympus.setCommand(ID, command) - Olympus.debug("Olympus.setCommand " .. ID .. " " .. Olympus.serializeTable(command), 2) - local unit = Olympus.getUnitByID(ID) - if unit then - unit:getGroup():getController():setCommand(command) +function Olympus.setCommand(groupName, command) + Olympus.debug("Olympus.setCommand " .. groupName .. " " .. Olympus.serializeTable(command), 2) + local group = Group.getByName(groupName) + if group then + group:getController():setCommand(command) Olympus.debug("Olympus.setCommand completed successfully", 2) end end -function Olympus.setOption(ID, optionID, optionValue) - Olympus.debug("Olympus.setOption " .. ID .. " " .. optionID .. " " .. tostring(optionValue), 2) - local unit = Olympus.getUnitByID(ID) - if unit then - unit:getGroup():getController():setOption(optionID, optionValue) +function Olympus.setOption(groupName, optionID, optionValue) + Olympus.debug("Olympus.setOption " .. groupName .. " " .. optionID .. " " .. tostring(optionValue), 2) + local group = Group.getByName(groupName) + if group then + group:getController():setOption(optionID, optionValue) Olympus.debug("Olympus.setOption completed successfully", 2) end end -function Olympus.setOnOff(ID, onOff) - Olympus.debug("Olympus.setOnOff " .. ID .. " " .. tostring(onOff), 2) - local unit = Olympus.getUnitByID(ID) - if unit then - unit:getGroup():getController():setOnOff(onOff) +function Olympus.setOnOff(groupName, onOff) + Olympus.debug("Olympus.setOnOff " .. groupName .. " " .. tostring(onOff), 2) + local group = Group.getByName(groupName) + if group then + group:getController():setOnOff(onOff) Olympus.debug("Olympus.setOnOff completed successfully", 2) end end diff --git a/src/core/include/commands.h b/src/core/include/commands.h index b244bc58..d4e0bab3 100644 --- a/src/core/include/commands.h +++ b/src/core/include/commands.h @@ -97,14 +97,15 @@ protected: class Move : public Command { public: - Move(int ID, Coords destination, double speed, wstring speedType, double altitude, wstring altitudeType, wstring taskOptions): - ID(ID), + Move(wstring groupName, Coords destination, double speed, wstring speedType, double altitude, wstring altitudeType, wstring taskOptions, wstring category): + groupName(groupName), destination(destination), speed(speed), speedType(speedType), altitude(altitude), altitudeType(altitudeType), - taskOptions(taskOptions) + taskOptions(taskOptions), + category(category) { priority = CommandPriority::HIGH; }; @@ -112,13 +113,14 @@ public: virtual int getLoad() { return 5; } private: - const int ID; + const wstring groupName; const Coords destination; const double speed; const wstring speedType; const double altitude; const wstring altitudeType; const wstring taskOptions; + const wstring category; }; /* Smoke command */ @@ -223,8 +225,8 @@ private: class SetTask : public Command { public: - SetTask(int ID, wstring task) : - ID(ID), + SetTask(wstring groupName, wstring task) : + groupName(groupName), task(task) { priority = CommandPriority::MEDIUM; @@ -233,7 +235,7 @@ public: virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; const wstring task; }; @@ -241,8 +243,8 @@ private: class ResetTask : public Command { public: - ResetTask(int ID) : - ID(ID) + ResetTask(wstring groupName) : + groupName(groupName) { priority = CommandPriority::HIGH; }; @@ -250,15 +252,15 @@ public: virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; }; /* Set command */ class SetCommand : public Command { public: - SetCommand(int ID, wstring command) : - ID(ID), + SetCommand(wstring groupName, wstring command) : + groupName(groupName), command(command) { priority = CommandPriority::HIGH; @@ -267,7 +269,7 @@ public: virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; const wstring command; }; @@ -275,8 +277,8 @@ private: class SetOption : public Command { public: - SetOption(int ID, int optionID, int optionValue) : - ID(ID), + SetOption(wstring groupName, int optionID, int optionValue) : + groupName(groupName), optionID(optionID), optionValue(optionValue), optionBool(false), @@ -285,8 +287,8 @@ public: priority = CommandPriority::HIGH; }; - SetOption(int ID, int optionID, bool optionBool) : - ID(ID), + SetOption(wstring groupName, int optionID, bool optionBool) : + groupName(groupName), optionID(optionID), optionValue(0), optionBool(optionBool), @@ -298,7 +300,7 @@ public: virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; const int optionID; const int optionValue; const bool optionBool; @@ -309,8 +311,8 @@ private: class SetOnOff : public Command { public: - SetOnOff(int ID, bool onOff) : - ID(ID), + SetOnOff(wstring groupName, bool onOff) : + groupName(groupName), onOff(onOff) { priority = CommandPriority::HIGH; @@ -319,7 +321,7 @@ public: virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; const bool onOff; }; diff --git a/src/core/include/unit.h b/src/core/include/unit.h index 13e47945..5d6dafb3 100644 --- a/src/core/include/unit.h +++ b/src/core/include/unit.h @@ -6,6 +6,8 @@ #include "measure.h" #include "logger.h" +#define TASK_CHECK_INIT_VALUE 10 + namespace State { enum States @@ -63,7 +65,7 @@ public: int getID() { return ID; } void updateExportData(json::value json); void updateMissionData(json::value json); - json::value getData(long long time); + json::value getData(long long time, bool getAll = false); virtual wstring getCategory() { return L"No category"; }; /********** Base data **********/ @@ -100,7 +102,7 @@ public: void setFuel(double newFuel) { fuel = newFuel; addMeasure(L"fuel", json::value(newFuel));} void setAmmo(json::value newAmmo) { ammo = newAmmo; addMeasure(L"ammo", json::value(newAmmo));} void setTargets(json::value newTargets) {targets = newTargets; addMeasure(L"targets", json::value(newTargets));} - void setHasTask(bool newHasTask) { hasTask = newHasTask; addMeasure(L"hasTask", json::value(newHasTask)); } + void setHasTask(bool newHasTask); void setCoalitionID(int newCoalitionID); void setFlags(json::value newFlags) { flags = newFlags; addMeasure(L"flags", json::value(newFlags));} @@ -181,6 +183,7 @@ protected: int ID; map