diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index dd4506dc..0cc857a7 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -240,6 +240,7 @@ export const defaultMapLayers = {}; export const NOT_INITIALIZED = "Not initialized"; export const IDLE = "Idle"; export const SPAWN_UNIT = "Spawn unit"; +export const SPAWN_EFFECT = "Spawn effect"; export const CONTEXT_ACTION = "Context action"; export const COALITIONAREA_DRAW_POLYGON = "Draw Coalition Area polygon"; export const COALITIONAREA_DRAW_CIRCLE = "Draw Coalition Area circle"; @@ -275,6 +276,7 @@ export const MAP_OPTIONS_DEFAULTS = { showUnitsAcquisitionRings: true, fillSelectedRing: false, showMinimap: false, + protectDCSUnits: true } as MapOptions; export const MAP_HIDDEN_TYPES_DEFAULTS = { diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 07f139f4..f0a1b03f 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -71,6 +71,10 @@ export interface SpawnRequestTable { unit: UnitSpawnTable; } +export interface EffectRequestTable { + type: string; +} + export interface UnitSpawnTable { unitType: string; location: LatLng; diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 993683be..841ec3b2 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -20,10 +20,11 @@ import { COALITIONAREA_EDIT, COALITIONAREA_DRAW_CIRCLE, NOT_INITIALIZED, + SPAWN_EFFECT, } from "../constants/constants"; import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon"; import { MapHiddenTypes, MapOptions } from "../types/types"; -import { SpawnRequestTable } from "../interfaces"; +import { EffectRequestTable, SpawnRequestTable } from "../interfaces"; import { ContextAction } from "../unit/contextaction"; /* Stylesheets */ @@ -35,6 +36,7 @@ import { CoalitionCircle } from "./coalitionarea/coalitioncircle"; import { initDraggablePath } from "./coalitionarea/draggablepath"; import { faDrawPolygon, faHandPointer, faJetFighter, faMap } from "@fortawesome/free-solid-svg-icons"; +import { ExplosionMarker } from "./markers/explosionmarker"; /* Register the handler for the box selection */ L.Map.addInitHook("addHandler", "boxSelect", BoxSelect); @@ -108,6 +110,7 @@ export class Map extends L.Map { /* Unit spawning */ #spawnRequestTable: SpawnRequestTable | null = null; + #effectRequestTable: EffectRequestTable | null = null; #temporaryMarkers: TemporaryUnitMarker[] = []; #currentSpawnMarker: TemporaryUnitMarker | null = null; @@ -342,6 +345,7 @@ export class Map extends L.Map { state: string, options?: { spawnRequestTable?: SpawnRequestTable; + effectRequestTable?: EffectRequestTable; contextAction?: ContextAction | null; defaultContextAction?: ContextAction | null; } @@ -366,6 +370,13 @@ export class Map extends L.Map { console.log(this.#spawnRequestTable); this.#currentSpawnMarker = new TemporaryUnitMarker(new L.LatLng(0, 0), this.#spawnRequestTable?.unit.unitType ?? "", this.#spawnRequestTable?.coalition ?? "neutral") this.#currentSpawnMarker.addTo(this); + } else if (this.#state === SPAWN_EFFECT) { + this.deselectAllCoalitionAreas(); + this.#effectRequestTable = options?.effectRequestTable ?? null; + console.log(`Effect request table:`); + console.log(this.#effectRequestTable); + //this.#currentEffectMarker = new TemporaryUnitMarker(new L.LatLng(0, 0), this.#spawnRequestTable?.unit.unitType ?? "", this.#spawnRequestTable?.coalition ?? "neutral") + //this.#currentEffectMarker.addTo(this); } else if (this.#state === CONTEXT_ACTION) { this.deselectAllCoalitionAreas(); this.#contextAction = options?.contextAction ?? null; @@ -435,6 +446,24 @@ export class Map extends L.Map { text: "Move map location", }, ]; + } else if (this.#state === SPAWN_EFFECT) { + return [ + { + actions: [touch ? faHandPointer : "LMB"], + target: faMap, + text: "Spawn effect", + }, + { + actions: [touch ? faHandPointer : "LMB", 2], + target: faMap, + text: "Exit spawn mode", + }, + { + actions: [touch ? faHandPointer : "LMB", "Drag"], + target: faMap, + text: "Move map location", + }, + ]; } else if (this.#state === CONTEXT_ACTION) { let controls = [ { @@ -670,6 +699,12 @@ export class Map extends L.Map { return marker; } + addExplosionMarker(latlng: L.LatLng, timeout: number = 30) { + var marker = new ExplosionMarker(latlng, timeout); + marker.addTo(this); + return marker; + } + getSelectedCoalitionArea() { const coalitionArea = this.#coalitionAreas.find((coalitionArea: CoalitionPolygon | CoalitionCircle) => { return coalitionArea.getSelected(); @@ -695,24 +730,6 @@ export class Map extends L.Map { return this.#previousZoom; } - getIsUnitProtected(unit: Unit) { - //const toggles = this.#mapMarkerVisibilityControls.reduce((list, control: MapMarkerVisibilityControl) => { - // if (control.isProtected) { - // list = list.concat(control.toggles); - // } - // return list; - //}, [] as string[]); - // - //if (toggles.length === 0) - // return false; - // - //return toggles.some((toggle: string) => { - // // Specific coding for robots - extend later if needed - // return (toggle === "dcs" && !unit.getControlled() && !unit.getHuman()); - //}); - return false; - } - setSlaveDCSCamera(newSlaveDCSCamera: boolean) { this.#slaveDCSCamera = newSlaveDCSCamera; let button = document.getElementById("camera-link-control"); @@ -829,6 +846,7 @@ export class Map extends L.Map { this.setState(COALITIONAREA_EDIT); } else { this.setState(IDLE); + document.dispatchEvent(new CustomEvent("hideAllMenus")) } } @@ -861,7 +879,11 @@ export class Map extends L.Map { } ); } - } else if (this.#state === COALITIONAREA_DRAW_POLYGON) { + } else if (this.#state === SPAWN_EFFECT) { + if (e.originalEvent.button != 2 && this.#effectRequestTable !== null) { + getApp().getServerManager().spawnExplosion(50, 'normal', pressLocation); + } + } else if (this.#state === COALITIONAREA_DRAW_POLYGON) { const selectedArea = this.getSelectedCoalitionArea(); if (selectedArea && selectedArea instanceof CoalitionPolygon) { selectedArea.addTemporaryLatLng(pressLocation); diff --git a/frontend/react/src/map/markers/explosionmarker.ts b/frontend/react/src/map/markers/explosionmarker.ts new file mode 100644 index 00000000..44beedaf --- /dev/null +++ b/frontend/react/src/map/markers/explosionmarker.ts @@ -0,0 +1,38 @@ +import { CustomMarker } from "./custommarker"; +import { DivIcon, LatLng } from "leaflet"; +import { SVGInjector } from "@tanem/svg-injector"; +import { getApp } from "../../olympusapp"; + +export class ExplosionMarker extends CustomMarker { + #timer: number = 0; + #timeout: number = 0; + + constructor(latlng: LatLng, timeout: number) { + super(latlng, { interactive: false }); + + this.#timeout = timeout; + + this.#timer = window.setTimeout(() => { + this.removeFrom(getApp().getMap()); + }, timeout * 1000); + } + + createIcon() { + /* Set the icon */ + var icon = new DivIcon({ + className: "leaflet-explosion-icon", + iconAnchor: [25, 25], + iconSize: [50, 50], + }); + this.setIcon(icon); + + var el = document.createElement("div"); + var img = document.createElement("img"); + img.src = `/vite/images/markers/smoke.svg`; + img.onload = () => SVGInjector(img); + el.append(img); + + this.getElement()?.appendChild(el); + this.getElement()?.classList.add("ol-temporary-marker"); + } +} diff --git a/frontend/react/src/mission/airbase.ts b/frontend/react/src/mission/airbase.ts index 1c4ec32c..2d5a7117 100644 --- a/frontend/react/src/mission/airbase.ts +++ b/frontend/react/src/mission/airbase.ts @@ -2,6 +2,8 @@ import { DivIcon } from "leaflet"; import { CustomMarker } from "../map/markers/custommarker"; import { SVGInjector } from "@tanem/svg-injector"; import { AirbaseChartData, AirbaseOptions } from "../interfaces"; +import { getApp } from "../olympusapp"; +import { IDLE } from "../constants/constants"; export class Airbase extends CustomMarker { #name: string = ""; @@ -21,6 +23,12 @@ export class Airbase extends CustomMarker { this.#name = options.name; this.#img = document.createElement("img"); + + this.addEventListener("click", (ev) => { + if (getApp().getMap().getState() === IDLE) { + document.dispatchEvent(new CustomEvent("airbaseClick", { detail: ev.target })); + } + }); } createIcon() { @@ -34,7 +42,7 @@ export class Airbase extends CustomMarker { var el = document.createElement("div"); el.classList.add("airbase-icon"); el.setAttribute("data-object", "airbase"); - + this.#img.src = "/vite/images/markers/airbase.svg"; this.#img.onload = () => SVGInjector(this.#img); el.appendChild(this.#img); diff --git a/frontend/react/src/mission/carrier.ts b/frontend/react/src/mission/carrier.ts index 50c030ac..35b5412b 100644 --- a/frontend/react/src/mission/carrier.ts +++ b/frontend/react/src/mission/carrier.ts @@ -2,8 +2,6 @@ import { DivIcon, LatLng, Map } from "leaflet"; import { Airbase } from "./airbase"; export class Carrier extends Airbase { - #heading: number = 0; - createIcon() { var icon = new DivIcon({ className: "leaflet-airbase-marker", @@ -42,7 +40,6 @@ export class Carrier extends Airbase { } setHeading(heading: number) { - this.#heading = heading; this.getImg().style.transform = `rotate(${heading - 3.14 / 2}rad)`; } @@ -53,6 +50,7 @@ export class Carrier extends Airbase { const maxMeters = this._map.containerPointToLatLng([0, y]).distanceTo(this._map.containerPointToLatLng([x, y])); const meterPerPixel = maxMeters / x; this.getImg().style.width = `${Math.round(333 / meterPerPixel)}px`; + this.setZIndexOffset(-10000); } } } diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index 8215f9e0..1167a38b 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -91,7 +91,6 @@ export class MissionManager { position: new LatLng(airbase.latitude, airbase.longitude), name: airbaseCallsign, }).addTo(getApp().getMap()); - this.#airbases[airbaseCallsign].on("click", (e) => this.#onAirbaseClick(e)); this.#loadAirbaseChartData(airbaseCallsign); } } @@ -321,10 +320,6 @@ export class MissionManager { if (requestRefresh) getApp().getServerManager().refreshAll(); } - #onAirbaseClick(ev: any) { - document.dispatchEvent(new CustomEvent("airbaseclick", { detail: ev.target })); - } - #loadAirbaseChartData(callsign: string) { if (!this.#theatre) { return; diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index c4198ceb..1d4f8d02 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -349,7 +349,7 @@ export class ServerManager { this.PUT(data, callback); } - createFormation(ID: number, isLeader: boolean, wingmenIDs: number[], callback: CallableFunction = () => {}) { + showFormationMenu(ID: number, isLeader: boolean, wingmenIDs: number[], callback: CallableFunction = () => {}) { var command = { ID: ID, wingmenIDs: wingmenIDs, isLeader: isLeader }; var data = { setLeader: command }; this.PUT(data, callback); diff --git a/frontend/react/src/types/types.ts b/frontend/react/src/types/types.ts index 11f7ddad..abffc856 100644 --- a/frontend/react/src/types/types.ts +++ b/frontend/react/src/types/types.ts @@ -20,6 +20,7 @@ export type MapOptions = { showUnitsAcquisitionRings: boolean; fillSelectedRing: boolean; showMinimap: boolean; + protectDCSUnits: boolean; }; export type MapHiddenTypes = { diff --git a/frontend/react/src/ui/components/oleffectlistentry.tsx b/frontend/react/src/ui/components/oleffectlistentry.tsx new file mode 100644 index 00000000..bb535e7d --- /dev/null +++ b/frontend/react/src/ui/components/oleffectlistentry.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight"; + +export function OlEffectListEntry(props: { icon: IconProp; label: string, onClick: () => void }) { + return ( +
+ +
{props.label}
+ +
+ ); +} diff --git a/frontend/react/src/ui/components/olunitlistentry.tsx b/frontend/react/src/ui/components/olunitlistentry.tsx index 1cfccda8..537534fc 100644 --- a/frontend/react/src/ui/components/olunitlistentry.tsx +++ b/frontend/react/src/ui/components/olunitlistentry.tsx @@ -2,17 +2,16 @@ import React from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { UnitBlueprint } from "../../interfaces"; -import { faArrowRightLong, faCaretRight, faCircleArrowRight, faLongArrowAltRight } from "@fortawesome/free-solid-svg-icons"; import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight"; -export function OlUnitEntryList(props: { icon: IconProp; blueprint: UnitBlueprint; onClick: () => void }) { +export function OlUnitListEntry(props: { icon: IconProp; blueprint: UnitBlueprint; onClick: () => void }) { return (
diff --git a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx index 357affad..e95db72d 100644 --- a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx +++ b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx @@ -84,6 +84,7 @@ export function MapContextMenu(props: {}) { getApp() .getUnitsManager() .getSelectedUnits() + .filter(unit => !unit.getHuman()) .forEach((unit: Unit) => { unit.appendContextActions(newContextActionSet); }); diff --git a/frontend/react/src/ui/modals/protectionprompt.tsx b/frontend/react/src/ui/modals/protectionprompt.tsx new file mode 100644 index 00000000..69002077 --- /dev/null +++ b/frontend/react/src/ui/modals/protectionprompt.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import { Modal } from "./components/modal"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; +import { Unit } from "../../unit/unit"; +import { FaLock } from "react-icons/fa6"; + +export function ProtectionPrompt(props: {onContinue: (units: Unit[]) => void, onBack: () => void, units: Unit[] }) { + return ( + +
+
+ + Your selection contains protected units, are you sure you want to continue? + + + Pressing "Continue" will cause all DCS controlled units in the current selection to abort their mission and start following Olympus commands only. + + + If you are trying to delete a human player unit, they will be killed and de-slotted. Be careful! + + + To disable this warning, press on the button + +
+
+ + +
+
+
+ ); +} diff --git a/frontend/react/src/ui/panels/airbasemenu.tsx b/frontend/react/src/ui/panels/airbasemenu.tsx index 91fa1b1f..a7c46f18 100644 --- a/frontend/react/src/ui/panels/airbasemenu.tsx +++ b/frontend/react/src/ui/panels/airbasemenu.tsx @@ -7,7 +7,7 @@ import { getUnitsByLabel } from "../../other/utils"; import { UnitBlueprint } from "../../interfaces"; import { OlSearchBar } from "../components/olsearchbar"; import { OlAccordion } from "../components/olaccordion"; -import { OlUnitEntryList } from "../components/olunitlistentry"; +import { OlUnitListEntry } from "../components/olunitlistentry"; import { olButtonsVisibilityAircraft, olButtonsVisibilityHelicopter } from "../components/olicons"; import { UnitSpawnMenu } from "./unitspawnmenu"; @@ -103,7 +103,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase `} > {Object.entries(filteredAircraft).map((entry) => { - return setBlueprint(entry[1])} />; + return setBlueprint(entry[1])} />; })}
@@ -114,7 +114,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase `} > {Object.entries(filteredHelicopters).map((entry) => { - return setBlueprint(entry[1])} />; + return setBlueprint(entry[1])} />; })} diff --git a/frontend/react/src/ui/panels/components/menu.tsx b/frontend/react/src/ui/panels/components/menu.tsx index 8477f51f..d415a4f8 100644 --- a/frontend/react/src/ui/panels/components/menu.tsx +++ b/frontend/react/src/ui/panels/components/menu.tsx @@ -51,7 +51,7 @@ export function Menu(props: { onClick={props.onBack ?? (() => {})} icon={faArrowLeft} className={` - mr-1 h-8 cursor-pointer rounded-md p-2 + mr-1 h-4 cursor-pointer rounded-md p-2 dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white `} /> diff --git a/frontend/react/src/ui/panels/effectspawnmenu.tsx b/frontend/react/src/ui/panels/effectspawnmenu.tsx new file mode 100644 index 00000000..0aab3822 --- /dev/null +++ b/frontend/react/src/ui/panels/effectspawnmenu.tsx @@ -0,0 +1,45 @@ +import React, { useEffect, useState } from "react"; +import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; +import { getApp } from "../../olympusapp"; +import { IDLE, SPAWN_EFFECT } from "../../constants/constants"; + +export function EffectSpawnMenu(props: { effect: string }) { + const [explosionType, setExplosionType] = useState("High explosive"); + + /* When the menu is opened show the unit preview on the map as a cursor */ + useEffect(() => { + if (props.effect !== null) { + getApp() + ?.getMap() + ?.setState(SPAWN_EFFECT, { + effectRequestTable: { + type: props.effect, + } + }); + } else { + if (getApp().getMap().getState() === SPAWN_EFFECT) getApp().getMap().setState(IDLE); + } + + }); + + return ( +
+ Explosion type + + + {["High explosive", "Napalm", "White phosphorous"].map((optionExplosionType) => { + return ( + { + setExplosionType(optionExplosionType); + }} + > + {optionExplosionType} + + ); + })} + +
+ ); +} diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index 561725da..de76d58d 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -115,7 +115,7 @@ export function Header() { flex h-fit flex-row items-center justify-start gap-1 `} > - {}} tooltip="Lock/unlock protected units (from scripted mission)" /> + {getApp().getMap().setOption("protectDCSUnits", !appState.mapOptions.protectDCSUnits)}} tooltip="Lock/unlock protected units (from scripted mission)" /> { diff --git a/frontend/react/src/ui/panels/sidebar.tsx b/frontend/react/src/ui/panels/sidebar.tsx index b5edf8a7..c7512d3d 100644 --- a/frontend/react/src/ui/panels/sidebar.tsx +++ b/frontend/react/src/ui/panels/sidebar.tsx @@ -1,10 +1,9 @@ -import React, { useState } from "react"; +import React from "react"; import { OlStateButton } from "../components/olstatebutton"; -import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass, faPlaneDeparture, faRadio, faVolumeHigh } from "@fortawesome/free-solid-svg-icons"; +import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass, faRadio, faVolumeHigh } from "@fortawesome/free-solid-svg-icons"; import { EventsConsumer } from "../../eventscontext"; import { StateConsumer } from "../../statecontext"; import { IDLE } from "../../constants/constants"; -import { faSpeakerDeck } from "@fortawesome/free-brands-svg-icons"; export function SideBar() { return ( @@ -53,12 +52,6 @@ export function SideBar() { icon={faPencil} tooltip="Hide/show drawing menu" > - void; children?: JSX.Element | JSX.Element[] }) { const [blueprint, setBlueprint] = useState(null as null | UnitBlueprint); + const [effect, setEffect] = useState(null as null | string); const [filterString, setFilterString] = useState(""); const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = getUnitsByLabel(filterString); @@ -25,7 +29,10 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? useEffect(() => { if (!props.open && getApp()) { if (getApp().getMap().getState() === SPAWN_UNIT) getApp().getMap().setState(IDLE); + else if (getApp().getMap().getState() === SPAWN_EFFECT) getApp().getMap().setState(IDLE); + if (blueprint !== null) setBlueprint(null); + if (effect !== null) setEffect(null); } }); @@ -33,15 +40,16 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? { getApp().getMap().setState(IDLE); setBlueprint(null); + setEffect(null); }} > <> - {blueprint === null && ( + {blueprint === null && effect === null && (
setFilterString(value)} text={filterString} /> @@ -51,7 +59,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? `} > {Object.entries(filteredAircraft).map((entry) => { - return setBlueprint(entry[1])} />; + return setBlueprint(entry[1])} />; })}
@@ -62,7 +70,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? `} > {Object.entries(filteredHelicopters).map((entry) => { - return setBlueprint(entry[1])} />; + return setBlueprint(entry[1])} />; })} @@ -73,7 +81,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? `} > {Object.entries(filteredAirDefense).map((entry) => { - return setBlueprint(entry[1])} />; + return setBlueprint(entry[1])} />; })} @@ -84,7 +92,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? `} > {Object.entries(filteredGroundUnits).map((entry) => { - return setBlueprint(entry[1])} />; + return setBlueprint(entry[1])} />; })} @@ -95,15 +103,39 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? `} > {Object.entries(filteredNavyUnits).map((entry) => { - return setBlueprint(entry[1])} />; + return setBlueprint(entry[1])} />; })} - + +
+ { + setEffect("explosion"); + }} + /> + { + setEffect("smoke"); + }} + /> +
+
)} {!(blueprint === null) && } + {!(effect === null) && }
); diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index 10818e1c..a9f6253d 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -61,6 +61,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { ROE: undefined as undefined | string, reactionToThreat: undefined as undefined | string, emissionsCountermeasures: undefined as undefined | string, + scenicAAA: undefined as undefined | boolean, + missOnPurpose: undefined as undefined | boolean, shotsScatter: undefined as undefined | number, shotsIntensity: undefined as undefined | number, operateAs: undefined as undefined | Coalition, @@ -112,42 +114,6 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { if (!props.open && filterString !== "") setFilterString(""); }); - /* */ - const minAltitude = 0; - const maxAltitude = getApp() - ?.getUnitsManager() - ?.getSelectedUnitsCategories() - .every((category) => { - return category === "Helicopter"; - }) - ? 20000 - : 60000; - const altitudeStep = getApp() - ?.getUnitsManager() - ?.getSelectedUnitsCategories() - .every((category) => { - return category === "Helicopter"; - }) - ? 100 - : 500; - const minSpeed = 0; - const maxSpeed = getApp() - ?.getUnitsManager() - ?.getSelectedUnitsCategories() - .every((category) => { - return category === "Helicopter"; - }) - ? 200 - : 800; - const speedStep = getApp() - ?.getUnitsManager() - ?.getSelectedUnitsCategories() - .every((category) => { - return category === "Helicopter"; - }) - ? 5 - : 10; - useEffect(() => { /* When a unit is selected, update the data */ document.addEventListener("unitsSelection", (ev: CustomEventInit) => { @@ -190,6 +156,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { emissionsCountermeasures: (unit: Unit) => { return unit.getEmissionsCountermeasures(); }, + scenicAAA: (unit: Unit) => { + return unit.getState() === "scenic-aaa"; + }, + missOnPurpose: (unit: Unit) => { + return unit.getState() === "miss-on-purpose"; + }, shotsScatter: (unit: Unit) => { return unit.getShotsScatter(); }, @@ -213,8 +185,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { }, isAudioSink: (unit: Unit) => { return ( - getApp()?.getAudioManager().getSinks().filter((sink) => { - return sink instanceof UnitSink}).length > 0 && + getApp() + ?.getAudioManager() + .getSinks() + .filter((sink) => { + return sink instanceof UnitSink; + }).length > 0 && getApp() ?.getAudioManager() .getSinks() @@ -257,6 +233,37 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { [key: string]: UnitBlueprint; }; + const everyUnitIsGround = selectedCategories.every((category) => { + return category === "GroundUnit"; + }); + const everyUnitIsNavy = selectedCategories.every((category) => { + return category === "NavyUnit"; + }); + const everyUnitIsHelicopter = selectedCategories.every((category) => { + return category === "Helicopter"; + }); + + const minAltitude = 0; + const minSpeed = 0; + + let maxAltitude = 60000; + let maxSpeed = 800; + + let altitudeStep = 500; + let speedStep = 10; + + if (everyUnitIsHelicopter) { + maxAltitude = 20000; + maxSpeed = 200; + speedStep = 5; + altitudeStep = 100; + } + + if (everyUnitIsGround || everyUnitIsNavy) { + maxSpeed = 60; + speedStep = 1; + } + return ( void }) { {selectedUnitsData.desiredSpeed !== undefined ? selectedUnitsData.desiredSpeed + " KTS" : "Different values"} - { - selectedUnits.forEach((unit) => { - unit.setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS"); - setSelectedUnitsData({ - ...selectedUnitsData, - desiredSpeedType: selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS", + {!(everyUnitIsGround || everyUnitIsNavy) && ( + { + selectedUnits.forEach((unit) => { + unit.setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS"); + setSelectedUnitsData({ + ...selectedUnitsData, + desiredSpeedType: selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS", + }); }); - }); - }} - /> + }} + /> + )} { @@ -853,94 +862,152 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { return ["GroundUnit", "NavyUnit"].includes(category); }) && ( <> - {/* ============== Shots scatter START ============== */} -
- - Shots scatter - - - {[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => { - return ( - { - selectedUnits.forEach((unit) => { - unit.setShotsScatter(idx + 1); - setSelectedUnitsData({ - ...selectedUnitsData, - shotsScatter: idx + 1, - }); - }); - }} - active={selectedUnitsData.shotsScatter === idx + 1} - icon={icon} - /> - ); - })} - -
- {/* ============== Shots scatter END ============== */} - {/* ============== Shots intensity START ============== */} -
- - Shots intensity - - - {[olButtonsIntensity1, olButtonsIntensity2, olButtonsIntensity3].map((icon, idx) => { - return ( - { - selectedUnits.forEach((unit) => { - unit.setShotsIntensity(idx + 1); - setSelectedUnitsData({ - ...selectedUnitsData, - shotsIntensity: idx + 1, - }); - }); - }} - active={selectedUnitsData.shotsIntensity === idx + 1} - icon={icon} - /> - ); - })} - -
- {/* ============== Shots intensity END ============== */} - {/* ============== Operate as toggle START ============== */} -
- - Operate as - - { - selectedUnits.forEach((unit) => { - unit.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue"); - setSelectedUnitsData({ - ...selectedUnitsData, - operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue", +
+ {/* ============== Scenic AAA toggle START ============== */} +
+ + Scenic AAA mode + + { + selectedUnits.forEach((unit) => { + selectedUnitsData.scenicAAA ? unit.changeSpeed("stop") : unit.scenicAAA(); + setSelectedUnitsData({ + ...selectedUnitsData, + scenicAAA: !selectedUnitsData.scenicAAA, + missOnPurpose: false, + }); }); - }); - }} - /> + }} + /> +
+ {/* ============== Scenic AAA toggle END ============== */} + {/* ============== Miss on purpose toggle START ============== */} +
+ + Miss on purpose mode + + { + selectedUnits.forEach((unit) => { + selectedUnitsData.missOnPurpose ? unit.changeSpeed("stop") : unit.missOnPurpose(); + setSelectedUnitsData({ + ...selectedUnitsData, + scenicAAA: false, + missOnPurpose: !selectedUnitsData.missOnPurpose, + }); + }); + }} + /> +
+ {/* ============== Miss on purpose toggle END ============== */} +
+ {/* ============== Shots scatter START ============== */} +
+ + Shots scatter + + + {[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => { + return ( + { + selectedUnits.forEach((unit) => { + unit.setShotsScatter(idx + 1); + setSelectedUnitsData({ + ...selectedUnitsData, + shotsScatter: idx + 1, + }); + }); + }} + active={selectedUnitsData.shotsScatter === idx + 1} + icon={icon} + /> + ); + })} + +
+ {/* ============== Shots scatter END ============== */} + {/* ============== Shots intensity START ============== */} +
+ + Shots intensity + + + {[olButtonsIntensity1, olButtonsIntensity2, olButtonsIntensity3].map((icon, idx) => { + return ( + { + selectedUnits.forEach((unit) => { + unit.setShotsIntensity(idx + 1); + setSelectedUnitsData({ + ...selectedUnitsData, + shotsIntensity: idx + 1, + }); + }); + }} + active={selectedUnitsData.shotsIntensity === idx + 1} + icon={icon} + /> + ); + })} + +
+ {/* ============== Shots intensity END ============== */} +
+ {/* ============== Operate as toggle START ============== */} +
+ + Operate as + + { + selectedUnits.forEach((unit) => { + unit.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue"); + setSelectedUnitsData({ + ...selectedUnitsData, + operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue", + }); + }); + }} + /> +
+ {/* ============== Operate as toggle END ============== */}
- {/* ============== Operate as toggle END ============== */} {/* ============== Follow roads toggle START ============== */}
void }) { value={activeAdvancedSettings ? activeAdvancedSettings.TACAN.channel : 1} > - + { @@ -1250,9 +1318,11 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { className={` flex content-center gap-2 rounded-full ${selectedUnits[0].getFuel() > 40 && `bg-green-700`} - ${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && ` - bg-yellow-700 - `} + ${ + selectedUnits[0].getFuel() > 10 && + selectedUnits[0].getFuel() <= 40 && + `bg-yellow-700` + } ${selectedUnits[0].getFuel() <= 10 && `bg-red-700`} px-2 py-1 text-sm font-bold text-white `} diff --git a/frontend/react/src/ui/panels/unitexplosionmenu.tsx b/frontend/react/src/ui/panels/unitexplosionmenu.tsx new file mode 100644 index 00000000..72be6675 --- /dev/null +++ b/frontend/react/src/ui/panels/unitexplosionmenu.tsx @@ -0,0 +1,56 @@ +import React, { useState } from "react"; +import { Menu } from "./components/menu"; +import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; +import { Unit } from "../../unit/unit"; +import { getApp } from "../../olympusapp"; + +export function UnitExplosionMenu(props: { open: boolean; onClose: () => void; units: Unit[] | null; children?: JSX.Element | JSX.Element[] }) { + const [explosionType, setExplosionType] = useState("High explosive"); + + return ( + +
+ Explosion type + + + {["High explosive", "Napalm", "White phosphorous"].map((optionExplosionType) => { + return ( + { + setExplosionType(optionExplosionType); + }} + > + {optionExplosionType} + + ); + })} + + {props.units !== null && ( + + )} +
+
+ ); +} diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index 632a260d..bca7f513 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -26,6 +26,8 @@ import { RadioMenu } from "./panels/radiomenu"; import { AudioMenu } from "./panels/audiomenu"; import { FormationMenu } from "./panels/formationmenu"; import { Unit } from "../unit/unit"; +import { ProtectionPrompt } from "./modals/protectionprompt"; +import { UnitExplosionMenu } from "./panels/unitexplosionmenu"; export type OlympusUIState = { mainMenuVisible: boolean; @@ -51,6 +53,7 @@ export function UI() { const [optionsMenuVisible, setOptionsMenuVisible] = useState(false); const [airbaseMenuVisible, setAirbaseMenuVisible] = useState(false); const [formationMenuVisible, setFormationMenuVisible] = useState(false); + const [unitExplosionMenuVisible, setUnitExplosionMenuVisible] = useState(false); const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS); const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS); const [checkingPassword, setCheckingPassword] = useState(false); @@ -62,6 +65,10 @@ export function UI() { const [airbase, setAirbase] = useState(null as null | Airbase); const [formationLeader, setFormationLeader] = useState(null as null | Unit); const [formationWingmen, setFormationWingmen] = useState(null as null | Unit[]); + const [protectionPromptVisible, setProtectionPromptVisible] = useState(false); + const [protectionCallback, setProtectionCallback] = useState(null as any); + const [protectionUnits, setProtectionUnits] = useState([] as Unit[]); + const [unitExplosionUnits, setUnitExplosionUnits] = useState([] as Unit[]); useEffect(() => { document.addEventListener("hiddenTypesChanged", (ev) => { @@ -73,11 +80,15 @@ export function UI() { }); document.addEventListener("mapStateChanged", (ev) => { - if ((ev as CustomEvent).detail === IDLE) hideAllMenus(); - else if ((ev as CustomEvent).detail === CONTEXT_ACTION && window.innerWidth > 1000) setUnitControlMenuVisible(true); + //if ((ev as CustomEvent).detail === IDLE) hideAllMenus(); + /*else*/ if ((ev as CustomEvent).detail === CONTEXT_ACTION && window.innerWidth > 1000) setUnitControlMenuVisible(true); setMapState(String((ev as CustomEvent).detail)); }); + document.addEventListener("hideAllMenus", (ev) => { + hideAllMenus(); + }); + document.addEventListener("mapSourceChanged", (ev) => { var source = (ev as CustomEvent).detail; setActiveMapSource(source); @@ -90,18 +101,29 @@ export function UI() { setActiveMapSource(sources[0]); }); - document.addEventListener("airbaseclick", (ev) => { + document.addEventListener("airbaseClick", (ev) => { hideAllMenus(); getApp().getMap().setState(IDLE); setAirbase((ev as CustomEvent).detail); setAirbaseMenuVisible(true); }); - document.addEventListener("createFormation", (ev) => { + document.addEventListener("showFormationMenu", (ev) => { setFormationMenuVisible(true); setFormationLeader((ev as CustomEvent).detail.leader); setFormationWingmen((ev as CustomEvent).detail.wingmen); }); + + document.addEventListener("showProtectionPrompt", (ev: CustomEventInit) => { + setProtectionPromptVisible(true); + setProtectionCallback(() => {return ev.detail.callback}); + setProtectionUnits(ev.detail.units); + }); + + document.addEventListener("showUnitExplosionMenu", (ev) => { + setUnitExplosionMenuVisible(true); + setUnitExplosionUnits((ev as CustomEvent).detail.units); + }) }, []); function hideAllMenus() { @@ -115,6 +137,7 @@ export function UI() { setRadioMenuVisible(false); setAudioMenuVisible(false); setFormationMenuVisible(false); + setUnitExplosionMenuVisible(false); } function checkPassword(password: string) { @@ -246,16 +269,36 @@ export function UI() { /> )} + {protectionPromptVisible && ( + <> +
+ { + protectionCallback(units); + setProtectionPromptVisible(false); + }} + onBack={() => { + setProtectionPromptVisible(false); + }} + units={protectionUnits} + /> + + )}
setMainMenuVisible(false)} /> setSpawnMenuVisible(false)} /> setOptionsMenuVisible(false)} options={mapOptions} /> setUnitControlMenuVisible(false)} /> setDrawingMenuVisible(false)} /> - setAirbaseMenuVisible(false)} airbase={airbase}/> + setAirbaseMenuVisible(false)} airbase={airbase} /> setRadioMenuVisible(false)} /> setAudioMenuVisible(false)} /> setFormationMenuVisible(false)} /> + setUnitExplosionMenuVisible(false)} /> diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 1ec7bf35..d58ef9bf 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -12,7 +12,6 @@ import { rad2deg, bearing, deg2rad, - ftToM, getGroundElevation, coalitionToEnum, nmToM, @@ -51,30 +50,22 @@ import { Group } from "./group"; import { ContextActionSet } from "./contextactionset"; import * as turf from "@turf/turf"; import { - olButtonsContextMissOnPurpose, - olButtonsContextScenicAaa, olButtonsContextSimulateFireFight, - olButtonsContextDiamond, - olButtonsContextEchelonLh, - olButtonsContextEchelonRh, olButtonsContextFollow, - olButtonsContextFront, olButtonsContextLandAtPoint, - olButtonsContextLineAbreast, - olButtonsContextTrail, olButtonsContextAttack, olButtonsContextRefuel, } from "../ui/components/olicons"; import { - faArrowDown, - faExclamation, + faExplosion, faLocationCrosshairs, faLocationDot, faMapLocation, faPeopleGroup, + faPlaneArrival, faQuestionCircle, faRoute, - faVolumeHigh, + faTrash, faXmarksLines, } from "@fortawesome/free-solid-svg-icons"; import { Carrier } from "../mission/carrier"; @@ -841,7 +832,7 @@ export abstract class Unit extends CustomMarker { contextActionSet.addContextAction( this, "path", - "Append destination", + "Create route", "Click on the map to add a destination to the path", faRoute, "position", @@ -850,6 +841,36 @@ export abstract class Unit extends CustomMarker { } ); + contextActionSet.addContextAction( + this, + "delete", + "Delete unit", + "Deletes the unit", + faTrash, + null, + (units: Unit[], _1, _2) => { + getApp().getUnitsManager().delete(false); + }, + { + executeImmediately: true, + } + ); + + contextActionSet.addContextAction( + this, + "explode", + "Explode unit", + "Explodes the unit", + faExplosion, + null, + (units: Unit[], _1, _2) => { + document.dispatchEvent(new CustomEvent("showUnitExplosionMenu", { detail: { units: units } })); + }, + { + executeImmediately: true, + } + ); + contextActionSet.addDefaultContextAction(this, "default", "Set destination", "", faRoute, null, (units: Unit[], targetUnit, targetPosition) => { if (targetPosition) { getApp().getUnitsManager().clearDestinations(units); @@ -1211,7 +1232,24 @@ export abstract class Unit extends CustomMarker { } delete(explosion: boolean, explosionType: string, immediate: boolean) { - getApp().getServerManager().deleteUnit(this.ID, explosion, explosionType, immediate); + getApp() + .getServerManager() + .deleteUnit(this.ID, explosion, explosionType, immediate, (commandHash) => { + /* When the command is executed, add an explosion marker where the unit was */ + if (explosion) { + // TODO some commands don't currently return a commandHash, fix that! + let timer = window.setTimeout(() => { + //getApp() + // .getServerManager() + // .isCommandExecuted((res: any) => { + // if (res.commandExecuted) { + getApp().getMap().addExplosionMarker(this.getPosition()); + window.clearInterval(timer); + // } + // }, commandHash); + }, 500); + } + }); } refuel() { @@ -1294,38 +1332,6 @@ export abstract class Unit extends CustomMarker { this.#redrawMarker(); } - 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 ((dialog.querySelector(`#formation-${clock}`)).checked) break; - clock++; - } - var angleDeg = 360 - (clock - 1) * 45; - var angleRad = deg2rad(angleDeg); - var distance = ftToM(parseInt((dialog.querySelector(`#distance`)?.querySelector("input")).value)); - var upDown = ftToM(parseInt((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); - } - } - /***********************************************/ #onMouseUp(e: any) { this.#isMouseDown = false; @@ -1847,7 +1853,7 @@ export abstract class AirUnit extends Unit { (units: Unit[], targetUnit: Unit | null, _) => { if (targetUnit) { document.dispatchEvent( - new CustomEvent("createFormation", { + new CustomEvent("showFormationMenu", { detail: { leader: targetUnit, wingmen: units.filter((unit) => unit !== targetUnit), @@ -1881,6 +1887,18 @@ export abstract class AirUnit extends Unit { if (targetPosition) getApp().getUnitsManager().carpetBomb(targetPosition, units); } ); + + contextActionSet.addContextAction( + this, + "land", + "Land", + "Click on a point to land at the nearest airbase", + faPlaneArrival, + "position", + (units: Unit[], _, targetPosition: LatLng | null) => { + if (targetPosition) getApp().getUnitsManager().landAt(targetPosition, units); + } + ); } } @@ -1990,33 +2008,6 @@ export class GroundUnit extends Unit { { executeImmediately: true } ); - if (this.canAAA()) { - contextActionSet.addContextAction( - this, - "scenic-aaa", - "Scenic AAA", - "Shoot AAA in the air without aiming at any target, when an enemy unit gets close enough. WARNING: works correctly only on neutral units, blue or red units will aim", - olButtonsContextScenicAaa, - null, - (units: Unit[]) => { - getApp().getUnitsManager().scenicAAA(units); - }, - { executeImmediately: true } - ); - contextActionSet.addContextAction( - this, - "miss-aaa", - "Dynamic accuracy AAA", - "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", - olButtonsContextMissOnPurpose, - null, - (units: Unit[]) => { - getApp().getUnitsManager().missOnPurpose(units); - }, - { executeImmediately: true } - ); - } - /* Context actions that require a target unit */ contextActionSet.addContextAction( this, diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index 9cd6e7d7..98ef83f5 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -62,23 +62,10 @@ export class UnitsManager { this.#requestDetectionUpdate = true; }); document.addEventListener("copy", () => this.copy()); - document.addEventListener("deleteSelectedUnits", () => this.delete()); - document.addEventListener("explodeSelectedUnits", (e: any) => this.delete(true, e.detail.type)); - document.addEventListener("exportToFile", () => this.exportToFile()); - document.addEventListener("importFromFile", () => this.importFromFile()); document.addEventListener("keyup", (event) => this.#onKeyUp(event)); document.addEventListener("paste", () => this.paste()); - document.addEventListener("selectedUnitsChangeAltitude", (e: any) => { - this.changeAltitude(e.detail.type); - }); - document.addEventListener("selectedUnitsChangeSpeed", (e: any) => { - this.changeSpeed(e.detail.type); - }); document.addEventListener("unitDeselection", (e) => this.#onUnitDeselection((e as CustomEvent).detail)); document.addEventListener("unitSelection", (e) => this.#onUnitSelection((e as CustomEvent).detail)); - document.addEventListener("toggleMarkerProtection", (e) => { - this.#showNumberOfSelectedProtectedUnits(); - }); //this.#slowDeleteDialog = new Dialog("slow-delete-dialog"); } @@ -127,45 +114,6 @@ export class UnitsManager { } } - /** Sort units segregated groups based on controlling type and protection, if DCS-controlled - * - * @param units - * @returns Object - */ - segregateUnits(units: Unit[]): { [key: string]: [] } { - const data: any = { - controllable: [], - dcsProtected: [], - dcsUnprotected: [], - human: [], - olympus: [], - }; - const map = getApp().getMap(); - - units.forEach((unit) => { - if (unit.getHuman()) data.human.push(unit); - else if (unit.isControlledByOlympus()) data.olympus.push(unit); - else if (map.getIsUnitProtected(unit)) data.dcsProtected.push(unit); - else data.dcsUnprotected.push(unit); - }); - data.controllable = [].concat(data.dcsUnprotected, data.human, data.olympus); - return data; - } - - /** - * - * @param numOfProtectedUnits number - */ - showProtectedUnitsPopup(numOfProtectedUnits: number) { - if (numOfProtectedUnits < 1) return; - const messageText = numOfProtectedUnits === 1 ? `Unit is protected` : `All selected units are protected`; - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(messageText); - // Cheap way for now until we use more locks - let lock = document.querySelector("#unit-visibility-control button.lock"); - lock.classList.add("prompt"); - setTimeout(() => lock.classList.remove("prompt"), 4000); - } - /** Update the data of all the units. The data is directly decoded from the binary buffer received from the REST Server. This is necessary for performance and bandwidth reasons. * * @param buffer The arraybuffer, encoded according to the ICD defined in: TODO Add reference to ICD @@ -301,38 +249,10 @@ export class UnitsManager { /** Get all the currently selected units * - * @param options Selection options * @returns Array of selected units */ - getSelectedUnits(options?: { excludeHumans?: boolean; excludeProtected?: boolean; onlyOnePerGroup?: boolean; showProtectionReminder?: boolean }) { - let selectedUnits: Unit[] = []; - let numProtectedUnits = 0; - for (const [ID, unit] of Object.entries(this.#units)) { - if (unit.getSelected()) { - if (options) { - if (options.excludeHumans && unit.getHuman()) continue; - - if (options.excludeProtected === true && this.#unitIsProtected(unit)) { - numProtectedUnits++; - continue; - } - } - selectedUnits.push(unit); - } - } - if (options) { - if (options.showProtectionReminder === true && numProtectedUnits > selectedUnits.length && selectedUnits.length === 0) - this.showProtectedUnitsPopup(numProtectedUnits); - - if (options.onlyOnePerGroup) { - var temp: Unit[] = []; - for (let unit of selectedUnits) { - if (!temp.some((otherUnit: Unit) => unit.getGroupName() == otherUnit.getGroupName())) temp.push(unit); - } - selectedUnits = temp; - } - } - return selectedUnits; + getSelectedUnits() { + return Object.values(this.#units).filter((unit) => unit.getSelected()); } /** Deselects all currently selected units @@ -344,15 +264,7 @@ export class UnitsManager { } } - /** Deselect a specific unit - * - * @param ID ID of the unit to deselect - */ - deselectUnit(ID: number) { - this.#units[ID]?.setSelected(false); - } - - /** This function allows to quickly determine the categories (Aircraft, Helicopter, GroundUnit, NavyUnit) of an array units. This allows to enable/disable specific controls which can only be applied + /** This function allows to quickly determine the categories (Aircraft, Helicopter, GroundUnit, NavyUnit) of an array of units. This allows to enable/disable specific controls which can only be applied * to specific categories. * * @param units Array of units of which to retrieve the categories @@ -418,71 +330,62 @@ export class UnitsManager { * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ addDestination(latlng: L.LatLng, mantainRelativePosition: boolean, rotation: number, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } - - units = segregatedUnits.controllable; - - /* Compute the destination for each unit. If mantainRelativePosition is true, compute the destination so to hold the relative positions */ - var unitDestinations: { [key: number]: LatLng } = {}; - if (mantainRelativePosition) unitDestinations = this.computeGroupDestination(latlng, rotation); - else - units.forEach((unit: Unit) => { - unitDestinations[unit.ID] = latlng; - }); - - 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 (unit.getState() === "follow") { - const leader = this.getUnitByID(unit.getLeaderID()); - if (leader && leader.getSelected()) leader.addDestination(latlng); - else unit.addDestination(latlng); - } else { - if (unit.ID in unitDestinations) unit.addDestination(unitDestinations[unit.ID]); - } + units = units.filter((unit) => { + return !unit.getHuman(); }); - this.#showActionMessage(units, " new destination added"); + + let callback = (units) => { + /* Compute the destination for each unit. If mantainRelativePosition is true, compute the destination so to hold the relative positions */ + var unitDestinations: { [key: number]: LatLng } = {}; + if (mantainRelativePosition) unitDestinations = this.computeGroupDestination(latlng, rotation); + else + units.forEach((unit: Unit) => { + unitDestinations[unit.ID] = latlng; + }); + + 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 (unit.getState() === "follow") { + const leader = this.getUnitByID(unit.getLeaderID()); + if (leader && leader.getSelected()) leader.addDestination(latlng); + else unit.addDestination(latlng); + } else { + if (unit.ID in unitDestinations) unit.addDestination(unitDestinations[unit.ID]); + } + }); + this.#showActionMessage(units, " new destination added"); + }; + + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } /** Clear the destinations of all the selected units * */ clearDestinations(units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: false, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + for (let idx in units) { + const unit = units[idx]; + if (unit.getState() === "follow") { + const leader = this.getUnitByID(unit.getLeaderID()); + if (leader && leader.getSelected()) leader.clearDestinations(); + else unit.clearDestinations(); + } else unit.clearDestinations(); + } - units = segregatedUnits.controllable; - - for (let idx in units) { - const unit = units[idx]; - if (unit.getState() === "follow") { - const leader = this.getUnitByID(unit.getLeaderID()); - if (leader && leader.getSelected()) leader.clearDestinations(); - else unit.clearDestinations(); - } else unit.clearDestinations(); - } + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); + }; } /** Instruct all the selected units to land at a specific location @@ -491,311 +394,239 @@ export class UnitsManager { * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ landAt(latlng: LatLng, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.landAt(latlng)); - units = segregatedUnits.controllable; + this.#showActionMessage(units, " landing"); + }; - units.forEach((unit: Unit) => unit.landAt(latlng)); - - this.#showActionMessage(units, " landing"); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** 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 units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ changeSpeed(speedChange: string, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.changeSpeed(speedChange)); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.changeSpeed(speedChange)); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** 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 units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ changeAltitude(altitudeChange: string, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.changeAltitude(altitudeChange)); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.changeAltitude(altitudeChange)); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Set a specific speed to all the selected units * * @param speed Value to set, in m/s * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ setSpeed(speed: number, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.setSpeed(speed)); + this.#showActionMessage(units, `setting speed to ${msToKnots(speed)} kts`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.setSpeed(speed)); - this.#showActionMessage(units, `setting speed to ${msToKnots(speed)} kts`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(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 units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ setSpeedType(speedType: string, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.setSpeedType(speedType)); + this.#showActionMessage(units, `setting speed type to ${speedType}`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.setSpeedType(speedType)); - this.#showActionMessage(units, `setting speed type to ${speedType}`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Set a specific altitude to all the selected units * * @param altitude Value to set, in m * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ setAltitude(altitude: number, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.setAltitude(altitude)); + this.#showActionMessage(units, `setting altitude to ${mToFt(altitude)} ft`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.setAltitude(altitude)); - this.#showActionMessage(units, `setting altitude to ${mToFt(altitude)} ft`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(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 units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ setAltitudeType(altitudeType: string, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.setAltitudeType(altitudeType)); + this.#showActionMessage(units, `setting altitude type to ${altitudeType}`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.setAltitudeType(altitudeType)); - this.#showActionMessage(units, `setting altitude type to ${altitudeType}`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Set a specific ROE to all the selected units * * @param ROE Value to set, see constants for acceptable values * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ setROE(ROE: string, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.setROE(ROE)); + this.#showActionMessage(units, `ROE set to ${ROE}`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.setROE(ROE)); - this.#showActionMessage(units, `ROE set to ${ROE}`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Set a specific reaction to threat to all the selected units * * @param reactionToThreat Value to set, see constants for acceptable values * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ setReactionToThreat(reactionToThreat: string, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.setReactionToThreat(reactionToThreat)); + this.#showActionMessage(units, `reaction to threat set to ${reactionToThreat}`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.setReactionToThreat(reactionToThreat)); - this.#showActionMessage(units, `reaction to threat set to ${reactionToThreat}`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Set a specific emissions & countermeasures to all the selected units * * @param emissionCountermeasure Value to set, see constants for acceptable values * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ setEmissionsCountermeasures(emissionCountermeasure: string, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.setEmissionsCountermeasures(emissionCountermeasure)); + this.#showActionMessage(units, `emissions & countermeasures set to ${emissionCountermeasure}`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.setEmissionsCountermeasures(emissionCountermeasure)); - this.#showActionMessage(units, `emissions & countermeasures set to ${emissionCountermeasure}`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(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 units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ setOnOff(onOff: boolean, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.setOnOff(onOff)); + this.#showActionMessage(units, `unit active set to ${onOff}`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.setOnOff(onOff)); - this.#showActionMessage(units, `unit active set to ${onOff}`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Instruct the selected units to follow roads, only works on ground units * * @param followRoads If true, units will follow roads * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ setFollowRoads(followRoads: boolean, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.setFollowRoads(followRoads)); + this.#showActionMessage(units, `follow roads set to ${followRoads}`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.setFollowRoads(followRoads)); - this.#showActionMessage(units, `follow roads set to ${followRoads}`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Instruct selected units to operate as a certain coalition * * @param operateAsBool If true, units will operate as blue @@ -803,74 +634,59 @@ export class UnitsManager { */ setOperateAs(operateAsBool: boolean, units: Unit[] | null = null) { var operateAs = operateAsBool ? "blue" : "red"; - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.setOperateAs(operateAs)); + this.#showActionMessage(units, `operate as set to ${operateAs}`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.setOperateAs(operateAs)); - this.#showActionMessage(units, `operate as set to ${operateAs}`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Instruct units to attack a specific unit * * @param ID ID of the unit to attack * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ attackUnit(ID: number, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.attackUnit(ID)); + this.#showActionMessage(units, `attacking unit ${this.getUnitByID(ID)?.getUnitName()}`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.attackUnit(ID)); - this.#showActionMessage(units, `attacking unit ${this.getUnitByID(ID)?.getUnitName()}`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Instruct units to refuel at the nearest tanker, if possible. Else units will RTB * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ refuel(units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + units = units.filter((unit) => { + return !unit.getHuman(); + }); - segregatedUnits.controllable.forEach((unit: Unit) => unit.refuel()); - this.#showActionMessage(segregatedUnits.controllable, `sent to nearest tanker`); + let callback = (units) => { + units.forEach((unit: Unit) => unit.refuel()); + this.#showActionMessage(units, `sent to nearest tanker`); + }; + + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Instruct the selected units to follow another unit in a formation. Only works for aircrafts and helicopters. * * @param ID ID of the unit to follow @@ -879,52 +695,46 @@ export class UnitsManager { * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ 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 === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } - - units = segregatedUnits.controllable; - - if (offset == undefined) { - /* Simple formations with fixed offsets */ - offset = { x: 0, y: 0, z: 0 }; - if (formation === "trail") { - offset.x = -50; - offset.y = -30; - offset.z = 0; - } else if (formation === "echelon-lh") { - offset.x = -50; - offset.y = -10; - offset.z = -50; - } else if (formation === "echelon-rh") { - offset.x = -50; - offset.y = -10; - offset.z = 50; - } else if (formation === "line-abreast-lh") { - offset.x = 0; - offset.y = 0; - offset.z = -50; - } else if (formation === "line-abreast-rh") { - offset.x = 0; - offset.y = 0; - offset.z = 50; - } else if (formation === "front") { - offset.x = 100; - offset.y = 0; - offset.z = 0; - } else offset = undefined; - } + let callback = (units) => { + if (offset == undefined) { + /* Simple formations with fixed offsets */ + offset = { x: 0, y: 0, z: 0 }; + if (formation === "trail") { + offset.x = -50; + offset.y = -30; + offset.z = 0; + } else if (formation === "echelon-lh") { + offset.x = -50; + offset.y = -10; + offset.z = -50; + } else if (formation === "echelon-rh") { + offset.x = -50; + offset.y = -10; + offset.z = 50; + } else if (formation === "line-abreast-lh") { + offset.x = 0; + offset.y = 0; + offset.z = -50; + } else if (formation === "line-abreast-rh") { + offset.x = 0; + offset.y = 0; + offset.z = 50; + } else if (formation === "front") { + offset.x = 100; + offset.y = 0; + offset.z = 0; + } else offset = undefined; + } + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); + }; var count = 1; var xr = 0; var yr = 1; @@ -974,234 +784,183 @@ export class UnitsManager { * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ bombPoint(latlng: LatLng, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.bombPoint(latlng)); + this.#showActionMessage(units, `unit bombing point`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.bombPoint(latlng)); - this.#showActionMessage(units, `unit bombing point`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Instruct the selected units to perform carpet bombing of specific coordinates * * @param latlng Location to bomb * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ carpetBomb(latlng: LatLng, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.carpetBomb(latlng)); + this.#showActionMessage(units, `unit carpet bombing point`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.carpetBomb(latlng)); - this.#showActionMessage(units, `unit carpet bombing point`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Instruct the selected units to fire at specific coordinates * * @param latlng Location to fire at * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ fireAtArea(latlng: LatLng, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.fireAtArea(latlng)); + this.#showActionMessage(units, `unit firing at area`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.fireAtArea(latlng)); - this.#showActionMessage(units, `unit firing at area`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Instruct the selected units to simulate a fire fight at specific coordinates * * @param latlng Location to fire at * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ simulateFireFight(latlng: LatLng, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); - - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } - - units = segregatedUnits.controllable; - - getGroundElevation(latlng, (response: string) => { - var groundElevation: number | null = null; - try { - groundElevation = parseFloat(response); - } catch { - console.warn("Simulate fire fight: could not retrieve ground elevation"); - } - units?.forEach((unit: Unit) => unit.simulateFireFight(latlng, groundElevation)); - }); - this.#showActionMessage(units, `unit simulating fire fight`); + // TODO + // if (units === null) + // units = this.getSelectedUnits(); + // units = units.filter((unit => {!unit.getHuman()})); + // + // let callback = (units) => { + // + // getGroundElevation(latlng, (response: string) => { + // var groundElevation: number | null = null; + // try { + // groundElevation = parseFloat(response); + // } catch { + // console.warn("Simulate fire fight: could not retrieve ground elevation"); + // } + // + //if (getApp().getMap().getOptions().protectDCSUnits && !units.every(unit => unit.isControlledByOlympus())) + // this.showProtectedUnitsPopup(units.filter(unit => unit.isControlledByDCS()).length, callback);} units?.forEach((unit: Unit) => unit.simulateFireFight(latlng, groundElevation)); + // }); + // this.#showActionMessage(units, `unit simulating fire fight`); } /** Instruct units to enter into scenic AAA mode. Units will shoot in the air without aiming * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ scenicAAA(units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.scenicAAA()); + this.#showActionMessage(units, `unit set to perform scenic AAA`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.scenicAAA()); - this.#showActionMessage(units, `unit set to perform scenic AAA`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Instruct units to enter into dynamic accuracy/miss on purpose mode. Units will aim to the nearest enemy unit but not precisely. * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ missOnPurpose(units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.missOnPurpose()); + this.#showActionMessage(units, `unit set to perform miss-on-purpose AAA`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.missOnPurpose()); - this.#showActionMessage(units, `unit set to perform miss-on-purpose AAA`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Instruct units to land at specific point * * @param latlng Point where to land * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ landAtPoint(latlng: LatLng, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.landAtPoint(latlng)); + this.#showActionMessage(units, `unit landing at point`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.landAtPoint(latlng)); - this.#showActionMessage(units, `unit landing at point`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Set a specific shots scatter to all the selected units * * @param shotsScatter Value to set * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ setShotsScatter(shotsScatter: number, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - onlyOnePerGroup: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.setShotsScatter(shotsScatter)); + this.#showActionMessage(units, `shots scatter set to ${shotsScatter}`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.setShotsScatter(shotsScatter)); - this.#showActionMessage(units, `shots scatter set to ${shotsScatter}`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /** Set a specific shots intensity to all the selected units * * @param shotsScatter Value to set * @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units. */ setShotsIntensity(shotsIntensity: number, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - onlyOnePerGroup: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + units.forEach((unit: Unit) => unit.setShotsIntensity(shotsIntensity)); + this.#showActionMessage(units, `shots intensity set to ${shotsIntensity}`); + }; - units = segregatedUnits.controllable; - - units.forEach((unit: Unit) => unit.setShotsIntensity(shotsIntensity)); - this.#showActionMessage(units, `shots intensity set to ${shotsIntensity}`); + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } - /*********************** Control operations on selected units ************************/ /** See getUnitsCategories for more info * @@ -1224,30 +983,25 @@ export class UnitsManager { * */ createGroup(units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: false, - showProtectionReminder: true, - }); + if (units === null) units = this.getSelectedUnits(); + units = units.filter((unit) => { + return !unit.getHuman(); + }); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } + let callback = (units) => { + if (this.getUnitsCategories(units).length == 1) { + 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 { + //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Groups can only be created from units of the same category`); + } - units = segregatedUnits.controllable; - - if (this.getUnitsCategories(units).length == 1) { - 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 { - //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Groups can only be created from units of the same category`); - } + if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); + }; } /** Set the hotgroup for the selected units. It will be the only hotgroup of the unit @@ -1279,45 +1033,17 @@ export class UnitsManager { * @returns */ delete(explosion: boolean = false, explosionType: string = "", units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeProtected: true, - showProtectionReminder: true, - }); /* Can be applied to humans too */ + // TODO add fast delete option + if (units === null) units = this.getSelectedUnits(); /* Can be applied to humans too */ - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return; - } - - units = segregatedUnits.controllable; - - const selectionContainsAHuman = units.some((unit: Unit) => { - return unit.getHuman() === true; - }); - - if ( - selectionContainsAHuman && - !confirm("Your selection includes a human player. Deleting humans causes their vehicle to crash.\n\nAre you sure you want to do this?") - ) { - return; - } - - const doDelete = (explosion = false, explosionType = "", immediate = false) => { - units?.forEach((unit: Unit) => unit.delete(explosion, explosionType, immediate)); + let callback = (units) => { + units?.forEach((unit: Unit) => unit.delete(explosion, explosionType, false)); this.#showActionMessage(units as Unit[], `deleted`); }; - //if (units.length >= DELETE_SLOW_THRESHOLD) - // //this.#showSlowDeleteDialog(units).then((action: any) => { - // // if (action === "delete-slow") - // // doDelete(explosion, explosionType, false); - // // else if (action === "delete-immediate") - // // doDelete(explosion, explosionType, true); - // //}) - //else - doDelete(explosion, explosionType); + if ((getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) || units.find((unit) => unit.getHuman())) + document.dispatchEvent(new CustomEvent("showProtectionPrompt", { detail: { callback: callback, units: units } })); + else callback(units); } /** Compute the destinations of every unit in the selected units. This function preserves the relative positions of the units, and rotates the whole formation by rotation. @@ -1328,20 +1054,12 @@ export class UnitsManager { * @returns Array of positions for each unit, in order */ computeGroupDestination(latlng: LatLng, rotation: number, units: Unit[] | null = null) { - if (units === null) - units = this.getSelectedUnits({ - excludeHumans: true, - excludeProtected: true, - onlyOnePerGroup: true, - }); + // TODO handle protected units + if (units === null) units = this.getSelectedUnits(); - const segregatedUnits = this.segregateUnits(units); - if (segregatedUnits.controllable.length === 0) { - this.showProtectedUnitsPopup(segregatedUnits.dcsProtected.length); - return {}; - } - - units = segregatedUnits.controllable; + units = units.filter((unit) => { + return !unit.getHuman(); + }); if (units.length === 0) return {}; @@ -1818,7 +1536,7 @@ export class UnitsManager { const map = getApp().getMap(); const units = this.getSelectedUnits(); const numSelectedUnits = units.length; - const numProtectedUnits = units.filter((unit: Unit) => map.getIsUnitProtected(unit)).length; + //const numProtectedUnits = units.filter((unit: Unit) => map.getIsUnitProtected(unit)).length; //if (numProtectedUnits === 1 && numSelectedUnits === numProtectedUnits) //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Notice: unit is protected`); @@ -1826,8 +1544,4 @@ export class UnitsManager { //if (numProtectedUnits > 1) //(getApp().getPopupsManager().get("infoPopup") as Popup).setText(`Notice: selection contains ${numProtectedUnits} protected units.`); } - - #unitIsProtected(unit: Unit) { - return getApp().getMap().getIsUnitProtected(unit); - } }