From 1791eaa37df6f28a9e2bd57304633c24f54e0ef9 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Sat, 23 Nov 2024 17:18:16 +0100 Subject: [PATCH] Started adding AWACS panel --- frontend/react/src/constants/constants.ts | 35 ++++-- frontend/react/src/events.ts | 29 +++-- frontend/react/src/interfaces.ts | 2 + frontend/react/src/map/map.ts | 6 +- .../src/map/markers/stylesheets/units.css | 51 +++++++++ frontend/react/src/map/stylesheets/map.css | 2 +- frontend/react/src/other/utils.ts | 4 + frontend/react/src/types/types.ts | 3 + .../src/ui/contextmenus/mapcontextmenu.tsx | 22 +++- frontend/react/src/ui/panels/awacsmenu.tsx | 108 ++++++++++++++++++ .../react/src/ui/panels/coordinatespanel.tsx | 6 +- frontend/react/src/ui/panels/header.tsx | 3 +- frontend/react/src/ui/panels/hotgroupsbar.tsx | 23 ++-- frontend/react/src/ui/panels/minimappanel.tsx | 2 +- .../src/ui/panels/radiossummarypanel.tsx | 23 ++-- frontend/react/src/ui/panels/sidebar.tsx | 12 +- .../react/src/ui/panels/unitcontrolbar.tsx | 7 +- .../react/src/ui/panels/unitcontrolmenu.tsx | 1 - frontend/react/src/ui/ui.tsx | 4 +- frontend/react/src/unit/contextaction.ts | 2 - frontend/react/src/unit/contextactionset.ts | 2 +- frontend/react/src/unit/unit.ts | 49 +++++++- frontend/react/src/unit/unitsmanager.ts | 17 ++- 23 files changed, 344 insertions(+), 69 deletions(-) create mode 100644 frontend/react/src/ui/panels/awacsmenu.tsx diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 06f6f031..0ddd46e1 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -12,6 +12,7 @@ import { faPlaneArrival, faRoute, faTrash, + faWifi, faXmarksLines, } from "@fortawesome/free-solid-svg-icons"; import { Unit } from "../unit/unit"; @@ -260,7 +261,14 @@ export const mapBounds = { }; export const defaultMapMirrors = {}; -export const defaultMapLayers = {}; +export const defaultMapLayers = { + "AWACS": { + "urlTemplate": 'https://abcd.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', + "minZoom": 1, + "maxZoom": 19, + "attribution": `© OpenStreetMap contributors © CARTO'` + }, +}; export enum OlympusState { NOT_INITIALIZED = "Not initialized", @@ -272,6 +280,7 @@ export enum OlympusState { SPAWN_CONTEXT = "Spawn context", DRAW = "Draw", JTAC = "JTAC", + AWACS = "AWACS", OPTIONS = "Options", AUDIO = "Audio", AIRBASE = "Airbase", @@ -340,6 +349,9 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = { cameraPluginEnabled: false, cameraPluginMode: "map", tabletMode: false, + showUnitBullseyes: false, + showUnitBRAA: false, + AWACSMode: false }; export const MAP_HIDDEN_TYPES_DEFAULTS = { @@ -459,7 +471,6 @@ export namespace ContextActions { getApp().getUnitsManager().stop(units); }, { - executeImmediately: true, type: ContextActionType.MOVE, code: "Space", } @@ -506,7 +517,6 @@ export namespace ContextActions { getApp().getUnitsManager().delete(false); }, { - executeImmediately: true, type: ContextActionType.DELETE, code: "Delete", ctrlKey: false, @@ -526,7 +536,6 @@ export namespace ContextActions { UnitExplosionRequestEvent.dispatch(units); }, { - executeImmediately: true, type: ContextActionType.DELETE, code: "Delete", ctrlKey: false, @@ -544,7 +553,7 @@ export namespace ContextActions { (units: Unit[]) => { getApp().getMap().centerOnUnit(units[0]); }, - { executeImmediately: true, type: ContextActionType.OTHER, code: "KeyM", ctrlKey: false, shiftKey: false, altKey: false } + { type: ContextActionType.OTHER, code: "KeyM", ctrlKey: false, shiftKey: false, altKey: false } ); export const REFUEL = new ContextAction( @@ -556,7 +565,7 @@ export namespace ContextActions { (units: Unit[]) => { getApp().getUnitsManager().refuel(units); }, - { executeImmediately: true, type: ContextActionType.ADMIN, code: "KeyR", ctrlKey: false, shiftKey: false, altKey: false } + { type: ContextActionType.ADMIN, code: "KeyR", ctrlKey: false, shiftKey: false, altKey: false } ); export const FOLLOW = new ContextAction( @@ -643,7 +652,7 @@ export namespace ContextActions { (units: Unit[], _1, _2) => { getApp().getUnitsManager().createGroup(units); }, - { executeImmediately: true, type: ContextActionType.OTHER, code: "KeyG", ctrlKey: false, shiftKey: false, altKey: false } + { type: ContextActionType.OTHER, code: "KeyG", ctrlKey: false, shiftKey: false, altKey: false } ); export const ATTACK = new ContextAction( @@ -687,4 +696,16 @@ export namespace ContextActions { }, { type: ContextActionType.ADMIN, code: "KeyX", ctrlKey: false, shiftKey: false } ); + + export const SET_AWACS_REFERENCE = new ContextAction( + "set-awacs-reference", + "Set AWACS reference", + "Set unit as AWACS reference", + faWifi, + ContextActionTarget.NONE, + (units: Unit[], _1, _2) => { + getApp().getUnitsManager().setAWACSReference(units[0].ID); + }, + { type: ContextActionType.ADMIN, code: "KeyU", ctrlKey: false, shiftKey: false, altKey: false } + ); } diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index de9d0309..eab00388 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -151,6 +151,19 @@ export class ModalEvent { } /************** Map events ***************/ +export class MouseMovedEvent { + static on(callback: (latlng: LatLng, elevation: number) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.latlng, ev.detail.elevation); + }); + } + + static dispatch(latlng: LatLng, elevation?: number) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng, elevation } })); + // Logging disabled since periodic + } +} + export class HiddenTypesChangedEvent { static on(callback: (hiddenTypes: MapHiddenTypes) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { @@ -346,13 +359,13 @@ export class SpawnContextMenuRequestEvent { } export class HotgroupsChangedEvent { - static on(callback: (hotgroups: { [key: number]: number }) => void) { + static on(callback: (hotgroups: { [key: number]: Unit[] }) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { callback(ev.detail.hotgroups); }); } - static dispatch(hotgroups: { [key: number]: number }) { + static dispatch(hotgroups: { [key: number]: Unit[] }) { document.dispatchEvent(new CustomEvent(this.name, { detail: { hotgroups } })); console.log(`Event ${this.name} dispatched`); } @@ -371,15 +384,15 @@ export class StarredSpawnsChangedEvent { } } -export class MouseMovedEvent { - static on(callback: (latlng: LatLng, elevation: number) => void) { +export class AWACSReferenceChangedEvent { + static on(callback: (unit: Unit | null) => void) { document.addEventListener(this.name, (ev: CustomEventInit) => { - callback(ev.detail.latlng, ev.detail.elevation); + callback(ev.detail); }); } - static dispatch(latlng: LatLng, elevation?: number) { - document.dispatchEvent(new CustomEvent(this.name, { detail: { latlng, elevation } })); + static dispatch(unit: Unit | null) { + document.dispatchEvent(new CustomEvent(this.name, { detail: unit })); // Logging disabled since periodic } } @@ -461,7 +474,7 @@ export class BullseyesDataChanged { }); } - static dispatch(bullseyes: { [name: string]: Bullseye } ) { + static dispatch(bullseyes: { [name: string]: Bullseye }) { document.dispatchEvent(new CustomEvent(this.name, { detail: { bullseyes } })); // Logging disabled since periodic } diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 34758ceb..156122e2 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -130,6 +130,8 @@ export interface ObjectIconOptions { showSummary: boolean; showCallsign: boolean; rotateToHeading: boolean; + showBullseyes: boolean; + showBRAA: boolean; } export interface GeneralSettings { diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index b9384789..1e9b56b0 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -207,7 +207,7 @@ export class Map extends L.Map { /* Custom touch events for touchscreen support */ L.DomEvent.on(this.getContainer(), "touchstart", this.#onMouseDown, this); L.DomEvent.on(this.getContainer(), "touchend", this.#onMouseUp, this); - L.DomEvent.on(this.getContainer(), 'wheel', this.#onMouseWheel, this); + L.DomEvent.on(this.getContainer(), "wheel", this.#onMouseWheel, this); /* Event listeners */ AppStateChangedEvent.on((state, subState) => this.#onStateChanged(state, subState)); @@ -227,6 +227,8 @@ export class Map extends L.Map { MapOptionsChangedEvent.on((options: MapOptions) => { this.getContainer().toggleAttribute("data-hide-labels", !options.showUnitLabels); + this.getContainer().toggleAttribute("data-hide-bullseyes", !options.showUnitBullseyes); + this.getContainer().toggleAttribute("data-hide-BRAA", !options.showUnitBRAA); this.#cameraControlPort = options.cameraPluginPort; this.#cameraZoomRatio = 50 / (20 + options.cameraPluginRatio); this.#slaveDCSCamera = options.cameraPluginEnabled; @@ -400,7 +402,7 @@ export class Map extends L.Map { const contextActionSet = this.getContextActionSet(); if (this.getContextAction() === null || contextAction !== this.getContextAction()) { if (getApp().getState() === OlympusState.UNIT_CONTROL && contextActionSet && contextAction.getId() in contextActionSet.getContextActions()) { - if (contextAction.getOptions().executeImmediately) contextAction.executeCallback(null, null); + if (contextAction.getTarget() === ContextActionTarget.NONE) contextAction.executeCallback(null, null); else this.setContextAction(contextAction); } } else { diff --git a/frontend/react/src/map/markers/stylesheets/units.css b/frontend/react/src/map/markers/stylesheets/units.css index 32de33e9..94a65ba4 100644 --- a/frontend/react/src/map/markers/stylesheets/units.css +++ b/frontend/react/src/map/markers/stylesheets/units.css @@ -100,6 +100,7 @@ font-weight: var(--unit-font-weight); line-height: normal; position: absolute; + font-size: 12px; } [data-object|="unit-groundunit"] .unit-short-label { @@ -398,3 +399,53 @@ .ol-temporary-marker { opacity: 0.5; } + +/*** Unit summary ***/ +[data-object|="unit"] .unit-tactical { + color: white; + column-gap: 6px; + display: flex; + flex-wrap: wrap; + font-size: 11px; + font-weight: bold; + justify-content: left; + line-height: 12px; + pointer-events: none; + position: absolute; + row-gap: 1px; + + left: 100%; + width: fit-content; +} + +[data-hide-bullseyes] [data-object|="unit"] .unit-bullseyes { + display: none; +} + +[data-hide-BRAA] [data-object|="unit"] .unit-braa { + display: none; +} + +[data-object|="unit"] .unit-blue-bullseye { + text-shadow: + -1px -1px 0 #00F, + 1px -1px 0 #00F, + -1px 1px 0 #00F, + 1px 1px 0 #00F; +} + +[data-object|="unit"] .unit-red-bullseye { + text-shadow: + -1px -1px 0 #F00, + 1px -1px 0 #F00, + -1px 1px 0 #F00, + 1px 1px 0 #F00; +} + +[data-object|="unit"] .unit-braa { + text-shadow: + -1px -1px 0 #000, + 1px -1px 0 #000, + -1px 1px 0 #000, + 1px 1px 0 #000; +} diff --git a/frontend/react/src/map/stylesheets/map.css b/frontend/react/src/map/stylesheets/map.css index 5758cadc..d6bf3be6 100644 --- a/frontend/react/src/map/stylesheets/map.css +++ b/frontend/react/src/map/stylesheets/map.css @@ -180,4 +180,4 @@ path.leaflet-interactive:focus { outline: none; -} \ No newline at end of file +} diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index 02675a46..307c9d4f 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -393,3 +393,7 @@ export function wait(time) { setTimeout(resolve, time); }); } + +export function computeBearingRangeString(latlng1, latlng2) { + return `${bearing(latlng1.lat, latlng1.lng, latlng2.lat, latlng2.lng).toFixed()}/${(latlng1.distanceTo(latlng2) / 1852).toFixed(0)}`; +} diff --git a/frontend/react/src/types/types.ts b/frontend/react/src/types/types.ts index 522c6235..45eb78ed 100644 --- a/frontend/react/src/types/types.ts +++ b/frontend/react/src/types/types.ts @@ -26,6 +26,9 @@ export type MapOptions = { cameraPluginEnabled: boolean; cameraPluginMode: string; tabletMode: boolean; + showUnitBullseyes: false; + showUnitBRAA: false; + AWACSMode: false; }; export type MapHiddenTypes = { diff --git a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx index dcb215b0..1934063c 100644 --- a/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx +++ b/frontend/react/src/ui/contextmenus/mapcontextmenu.tsx @@ -10,6 +10,7 @@ import { ContextActionChangedEvent, ContextActionSetChangedEvent, MapContextMenuRequestEvent, + SelectedUnitsChangedEvent, SelectionClearedEvent, UnitContextMenuRequestEvent, } from "../../events"; @@ -24,6 +25,7 @@ export function MapContextMenu(props: {}) { const [yPosition, setYPosition] = useState(0); const [latLng, setLatLng] = useState(null as null | LatLng); const [unit, setUnit] = useState(null as null | Unit); + const [selectedUnits, setSelectedUnits] = useState([] as Unit[]); var contentRef = useRef(null); @@ -45,6 +47,7 @@ export function MapContextMenu(props: {}) { setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x); setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y); }); + SelectedUnitsChangedEvent.on((selectedUnits) => setSelectedUnits([...selectedUnits])); }, []); useEffect(() => { @@ -70,7 +73,13 @@ export function MapContextMenu(props: {}) { let reorderedActions: ContextAction[] = contextActionSet ? Object.values( - contextActionSet.getContextActions(appSubState === UnitControlSubState.MAP_CONTEXT_MENU ? ContextActionTarget.POINT : ContextActionTarget.UNIT) + contextActionSet.getContextActions( + selectedUnits.length === 1 && unit === selectedUnits[0] + ? ContextActionTarget.NONE + : appSubState === UnitControlSubState.MAP_CONTEXT_MENU + ? ContextActionTarget.POINT + : ContextActionTarget.UNIT + ) ).sort((a: ContextAction, b: ContextAction) => (a.getOptions().type < b.getOptions().type ? -1 : 1)) : []; @@ -104,7 +113,7 @@ export function MapContextMenu(props: {}) { ${colorString} `} onClick={() => { - if (contextActionIt.getOptions().executeImmediately) { + if (contextActionIt.getTarget() === ContextActionTarget.NONE) { contextActionIt.executeCallback(null, null); } else { if (appSubState === UnitControlSubState.MAP_CONTEXT_MENU) { @@ -113,10 +122,13 @@ export function MapContextMenu(props: {}) { contextActionIt.executeCallback(unit, null); } window.setTimeout(() => { - if (getApp().getSubState() === UnitControlSubState.MAP_CONTEXT_MENU || getApp().getSubState() === UnitControlSubState.UNIT_CONTEXT_MENU) { - getApp().setState(OlympusState.UNIT_CONTROL) + if ( + getApp().getSubState() === UnitControlSubState.MAP_CONTEXT_MENU || + getApp().getSubState() === UnitControlSubState.UNIT_CONTEXT_MENU + ) { + getApp().setState(OlympusState.UNIT_CONTROL); } - }, 200) + }, 200); } }} > diff --git a/frontend/react/src/ui/panels/awacsmenu.tsx b/frontend/react/src/ui/panels/awacsmenu.tsx new file mode 100644 index 00000000..8d9fcbd4 --- /dev/null +++ b/frontend/react/src/ui/panels/awacsmenu.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from "react"; +import { Menu } from "./components/menu"; +import { OlToggle } from "../components/oltoggle"; +import { MAP_OPTIONS_DEFAULTS } from "../../constants/constants"; +import { AWACSReferenceChangedEvent as AWACSReferenceUnitChangedEvent, HotgroupsChangedEvent, MapOptionsChangedEvent } from "../../events"; +import { getApp } from "../../olympusapp"; +import { OlCoalitionToggle } from "../components/olcoalitiontoggle"; +import { Coalition } from "../../types/types"; +import { FaQuestionCircle } from "react-icons/fa"; +import { Unit } from "../../unit/unit"; + +export function AWACSMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { + const [callsign, setCallsign] = useState("Magic"); + const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS); + const [coalition, setCoalition] = useState("blue" as Coalition); + const [hotgroups, setHotgroups] = useState({} as { [key: number]: Unit[] }); + const [referenceUnit, setReferenceUnit] = useState(null as Unit | null); + + useEffect(() => { + MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions })); + HotgroupsChangedEvent.on((hotgroups) => setHotgroups({ ...hotgroups })); + AWACSReferenceUnitChangedEvent.on((unit) => setReferenceUnit(unit)); + }, []); + + const enemyGroups = Object.values(hotgroups).filter((hotgroup) => { + return hotgroup.every((unit) => unit.getCoalition() !== coalition) + }) + + return ( + +
+ <> +
+ +
+

1 Use the coalition toggle to change your coalition as AWACS.

+

2 Set a friendly unit as reference by right clicking on it and selecting "Set AWACS reference".

+

3 Set enemy unit hotgroups to automatically create picture and tactical calls to read on radio for your CAP.

+
+
+
+ Callsign + setCallsign(ev.target.value)} + > + { + coalition === "blue" && setCoalition("neutral"); + coalition === "neutral" && setCoalition("red"); + coalition === "red" && setCoalition("blue"); + }} + coalition={coalition} + /> +
+
+
+ { + getApp().getMap().setOption("showUnitBullseyes", !mapOptions.showUnitBullseyes); + }} + toggled={mapOptions.showUnitBullseyes} + />{" "} + Show units Bullseye position +
+
+ { + getApp().getMap().setOption("showUnitBRAA", !mapOptions.showUnitBRAA); + }} + toggled={mapOptions.showUnitBRAA} + /> + Show units BRAA from reference unit +
+
+
+ { + referenceUnit ? <> + { + enemyGroups.length == 0 ? <> + No enemy or neutral hotgroup + :<> + + + } + :<>No reference unit selected + } +
+ +
+
+ ); +} diff --git a/frontend/react/src/ui/panels/coordinatespanel.tsx b/frontend/react/src/ui/panels/coordinatespanel.tsx index b74b1d60..09aa018f 100644 --- a/frontend/react/src/ui/panels/coordinatespanel.tsx +++ b/frontend/react/src/ui/panels/coordinatespanel.tsx @@ -28,9 +28,9 @@ export function CoordinatesPanel(props: {}) { return (
diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index 4d0f980c..046529d7 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -35,7 +35,8 @@ export function Header() { MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions })); MapSourceChangedEvent.on((source) => setMapSource(source)); ConfigLoadedEvent.on((config: OlympusConfig) => { - var sources = Object.keys(config.frontend.mapMirrors).concat(Object.keys(config.frontend.mapLayers)); + + var sources = Object.keys(config.frontend.mapMirrors).concat(Object.keys(config.frontend.mapLayers)).concat(getApp().getMap().getLayers()); setMapSources(sources); }); CommandModeOptionsChangedEvent.on((commandModeOptions) => { diff --git a/frontend/react/src/ui/panels/hotgroupsbar.tsx b/frontend/react/src/ui/panels/hotgroupsbar.tsx index 336bed7e..c892ccf5 100644 --- a/frontend/react/src/ui/panels/hotgroupsbar.tsx +++ b/frontend/react/src/ui/panels/hotgroupsbar.tsx @@ -1,15 +1,13 @@ import React, { useEffect, useState } from "react"; -import { AppStateChangedEvent, ContextActionChangedEvent, HotgroupsChangedEvent, InfoPopupEvent } from "../../events"; +import { AppStateChangedEvent, HotgroupsChangedEvent } from "../../events"; import { OlympusState } from "../../constants/constants"; -import { ContextAction } from "../../unit/contextaction"; import { OlStateButton } from "../components/olstatebutton"; -import { faUserGroup } from "@fortawesome/free-solid-svg-icons"; import { getApp } from "../../olympusapp"; +import { Unit } from "../../unit/unit"; export function HotGroupBar(props: {}) { - const [hotgroups, setHotgroups] = useState({} as { [key: number]: number }); + const [hotgroups, setHotgroups] = useState({} as { [key: number]: Unit[] }); const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); - const [menuHidden, setMenuHidden] = useState(false); useEffect(() => { AppStateChangedEvent.on((state, subState) => setAppState(state)); @@ -18,14 +16,11 @@ export function HotGroupBar(props: {}) { return (
- {Object.entries(hotgroups).map(([hotgroup, counter]) => { + {Object.entries(hotgroups).map(([hotgroup, units]) => { return (
{hotgroup}
- {getApp().getUnitsManager().selectUnitsByHotgroup(parseInt(hotgroup))}} tooltip=""> + {getApp().getUnitsManager().selectUnitsByHotgroup(parseInt(hotgroup))}} tooltip="Select units of this hotgroup" className={` + min-h-12 min-w-12 + `}> - {counter} + {units.length}
diff --git a/frontend/react/src/ui/panels/minimappanel.tsx b/frontend/react/src/ui/panels/minimappanel.tsx index a86c6a1c..cb694c43 100644 --- a/frontend/react/src/ui/panels/minimappanel.tsx +++ b/frontend/react/src/ui/panels/minimappanel.tsx @@ -58,7 +58,7 @@ export function MiniMapPanel(props: {}) { ${mapOptions.showMinimap ? `bottom-[188px]` : `bottom-[20px]`} flex w-[288px] items-center justify-between ${mapOptions.showMinimap ? `rounded-t-lg` : `rounded-lg`} - bg-gray-200 p-3 text-sm backdrop-blur-lg backdrop-grayscale + h-12 bg-gray-200 px-3 text-sm backdrop-blur-lg backdrop-grayscale dark:bg-olympus-800/90 dark:text-gray-200 `} > diff --git a/frontend/react/src/ui/panels/radiossummarypanel.tsx b/frontend/react/src/ui/panels/radiossummarypanel.tsx index 5c846c37..34ff7991 100644 --- a/frontend/react/src/ui/panels/radiossummarypanel.tsx +++ b/frontend/react/src/ui/panels/radiossummarypanel.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { AudioSinksChangedEvent } from "../../events"; import { AudioSink } from "../../audio/audiosink"; import { RadioSink } from "../../audio/radiosink"; -import { FaJetFighter, FaRadio } from "react-icons/fa6"; +import { FaJetFighter, FaRadio, FaVolumeHigh } from "react-icons/fa6"; import { OlStateButton } from "../components/olstatebutton"; import { UnitSink } from "../../audio/unitsink"; @@ -19,13 +19,12 @@ export function RadiosSummaryPanel(props: {}) {
- + {audioSinks.filter((audioSinks) => audioSinks instanceof RadioSink).length > 0 && audioSinks .filter((audioSinks) => audioSinks instanceof RadioSink) @@ -43,15 +42,14 @@ export function RadiosSummaryPanel(props: {}) { }} tooltip="Click to talk, lights up when receiving" buttonColor={radioSink.getReceiving() ? "white" : null} + className="min-h-12 min-w-12" > - {idx + 1} +
{idx + 1}
); })} - - {audioSinks.filter((audioSinks) => audioSinks instanceof UnitSink).length > 0 && } {audioSinks.filter((audioSinks) => audioSinks instanceof UnitSink).length > 0 && audioSinks .filter((audioSinks) => audioSinks instanceof UnitSink) @@ -68,8 +66,11 @@ export function RadiosSummaryPanel(props: {}) { unitSink.setPtt(false); }} tooltip="Click to talk" + className="min-h-12 min-w-12" > - {idx + 1} +
{idx + 1}
); })} diff --git a/frontend/react/src/ui/panels/sidebar.tsx b/frontend/react/src/ui/panels/sidebar.tsx index 60390e37..0e87c1a0 100644 --- a/frontend/react/src/ui/panels/sidebar.tsx +++ b/frontend/react/src/ui/panels/sidebar.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import { OlStateButton } from "../components/olstatebutton"; -import { faGamepad, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faVolumeHigh, faJ, faCrown } from "@fortawesome/free-solid-svg-icons"; +import { faGamepad, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faVolumeHigh, faJ, faCrown, faA } from "@fortawesome/free-solid-svg-icons"; import { getApp } from "../../olympusapp"; import { NO_SUBSTATE, OlympusState, OlympusSubState, SpawnSubState } from "../../constants/constants"; import { AppStateChangedEvent } from "../../events"; @@ -65,13 +65,21 @@ export function SideBar() { icon={faVolumeHigh} tooltip="Hide/show audio menu" > - { getApp().setState(appState !== OlympusState.JTAC ? OlympusState.JTAC : OlympusState.IDLE); }} checked={appState === OlympusState.JTAC} icon={faJ} tooltip="Hide/show JTAC menu" + >{*/} + { + getApp().setState(appState !== OlympusState.AWACS ? OlympusState.AWACS : OlympusState.IDLE); + }} + checked={appState === OlympusState.AWACS} + icon={faA} + tooltip="Hide/show AWACS menu" > { diff --git a/frontend/react/src/ui/panels/unitcontrolbar.tsx b/frontend/react/src/ui/panels/unitcontrolbar.tsx index edbb374c..43bd83fa 100644 --- a/frontend/react/src/ui/panels/unitcontrolbar.tsx +++ b/frontend/react/src/ui/panels/unitcontrolbar.tsx @@ -3,9 +3,8 @@ import { ContextActionSet } from "../../unit/contextactionset"; import { OlStateButton } from "../components/olstatebutton"; import { getApp } from "../../olympusapp"; import { ContextAction } from "../../unit/contextaction"; -import { CONTEXT_ACTION_COLORS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants"; -import { FaInfoCircle } from "react-icons/fa"; -import { FaChevronDown, FaChevronLeft, FaChevronRight, FaChevronUp } from "react-icons/fa6"; +import { CONTEXT_ACTION_COLORS, ContextActionTarget, MAP_OPTIONS_DEFAULTS } from "../../constants/constants"; +import { FaChevronDown,FaChevronUp } from "react-icons/fa6"; import { OlympusState } from "../../constants/constants"; import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent } from "../../events"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -83,7 +82,7 @@ export function UnitControlBar(props: {}) { tooltip={contextActionIt.getLabel()} buttonColor={CONTEXT_ACTION_COLORS[contextActionIt.getOptions().type ?? 0]} onClick={() => { - if (contextActionIt.getOptions().executeImmediately) { + if (contextActionIt.getTarget() === ContextActionTarget.NONE) { contextActionIt.executeCallback(null, null); } else { contextActionIt !== contextAction diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index e6531466..905b83da 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -12,7 +12,6 @@ import { emissionsCountermeasures, maxAltitudeValues, maxSpeedValues, - minAltitudeValues, reactionsToThreat, speedIncrements, } from "../../constants/constants"; diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index 392b8b13..22950104 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -31,6 +31,7 @@ import { HotGroupBar } from "./panels/hotgroupsbar"; import { SpawnContextMenu } from "./contextmenus/spawncontextmenu"; import { CoordinatesPanel } from "./panels/coordinatespanel"; import { RadiosSummaryPanel } from "./panels/radiossummarypanel"; +import { AWACSMenu } from "./panels/awacsmenu"; export type OlympusUIState = { mainMenuVisible: boolean; @@ -93,7 +94,8 @@ export function UI() { open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_EXPLOSION_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} /> + {/*} getApp().setState(OlympusState.IDLE)} />{*/} + getApp().setState(OlympusState.IDLE)} /> diff --git a/frontend/react/src/unit/contextaction.ts b/frontend/react/src/unit/contextaction.ts index 56742ce5..b4241cb5 100644 --- a/frontend/react/src/unit/contextaction.ts +++ b/frontend/react/src/unit/contextaction.ts @@ -4,7 +4,6 @@ import { LatLng } from "leaflet"; import { ContextActionTarget, ContextActionType } from "../constants/constants"; export interface ContextActionOptions { - executeImmediately?: boolean; type: ContextActionType; code: string | null; shiftKey?: boolean; @@ -32,7 +31,6 @@ export class ContextAction { this.#callback = callback; this.#icon = icon; this.#options = { - executeImmediately: false, ...options, }; } diff --git a/frontend/react/src/unit/contextactionset.ts b/frontend/react/src/unit/contextactionset.ts index 21fe4c4c..cb0f30cd 100644 --- a/frontend/react/src/unit/contextactionset.ts +++ b/frontend/react/src/unit/contextactionset.ts @@ -21,7 +21,7 @@ export class ContextActionSet { } getContextActions(targetFilter?: ContextActionTarget) { - if (targetFilter) { + if (targetFilter !== undefined) { var filteredContextActionSet = new ContextActionSet(); Object.keys(this.#contextActions).forEach((key) => { if (this.#contextActions[key].getTarget() === targetFilter) filteredContextActionSet[key] = this.#contextActions[key]; diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 63ee7055..4e4bf28b 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -969,6 +969,27 @@ export abstract class Unit extends CustomMarker { el.appendChild(summary); } + var tactical = document.createElement("div"); + tactical.classList.add("unit-tactical"); + if (iconOptions.showBullseyes) { + var bullseyes = document.createElement("div"); + bullseyes.classList.add("unit-bullseyes"); + var blueBullseye = document.createElement("div"); + blueBullseye.classList.add("unit-blue-bullseye"); + var redBullseye = document.createElement("div"); + redBullseye.classList.add("unit-red-bullseye"); + bullseyes.appendChild(blueBullseye); + bullseyes.appendChild(redBullseye); + tactical.appendChild(bullseyes); + } + + if (iconOptions.showBRAA) { + var BRAA = document.createElement("div"); + BRAA.classList.add("unit-braa"); + tactical.appendChild(BRAA); + } + el.appendChild(tactical) + this.getElement()?.appendChild(el); } @@ -1490,6 +1511,21 @@ export abstract class Unit extends CustomMarker { const hotgroupEl = element.querySelector(".unit-hotgroup-id") as HTMLElement; if (hotgroupEl) hotgroupEl.innerText = String(this.#hotgroup); } + + /* Set bullseyes positions */ + const bullseyes = getApp().getMissionManager().getBullseyes(); + const blueBullseye = `${bearing(bullseyes[2].getLatLng().lat, bullseyes[2].getLatLng().lng, this.getLatLng().lat, this.getLatLng().lng).toFixed()}/${(bullseyes[2].getLatLng().distanceTo(this.getLatLng()) / 1852).toFixed(0)}` + const redBullseye = `${bearing(bullseyes[1].getLatLng().lat, bullseyes[1].getLatLng().lng, this.getLatLng().lat, this.getLatLng().lng).toFixed()}/${(bullseyes[1].getLatLng().distanceTo(this.getLatLng()) / 1852).toFixed(0)}` + if (element.querySelector(".unit-blue-bullseye")) (element.querySelector(".unit-blue-bullseye")).innerText = `${blueBullseye}`; + if (element.querySelector(".unit-red-bullseye")) (element.querySelector(".unit-red-bullseye")).innerText = `${redBullseye}`; + + /* Set BRAA */ + const reference = getApp().getUnitsManager().getAWACSReference(); + if (reference && reference !== this) { + const BRAA = `${bearing(reference.getLatLng().lat, reference.getLatLng().lng, this.getLatLng().lat, this.getLatLng().lng).toFixed()}/${(reference.getLatLng().distanceTo(this.getLatLng()) / 1852).toFixed(0)}` + if (element.querySelector(".unit-braa")) (element.querySelector(".unit-braa")).innerText = `${BRAA}`; + } + else if (element.querySelector(".unit-braa")) (element.querySelector(".unit-braa")).innerText = ``; } /* Set vertical offset for altitude stacking */ @@ -1770,7 +1806,9 @@ export abstract class AirUnit extends Unit { showSummary: belongsToCommandedCoalition || this.getDetectionMethods().some((value) => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value)), showCallsign: belongsToCommandedCoalition, rotateToHeading: false, - }; + showBullseyes: true, + showBRAA: true, + } as ObjectIconOptions; } appendContextActions(contextActionSet: ContextActionSet) { @@ -1783,6 +1821,7 @@ export abstract class AirUnit extends Unit { /* Context actions that require a target unit */ contextActionSet.addContextAction(this, ContextActions.ATTACK); contextActionSet.addContextAction(this, ContextActions.FOLLOW); + contextActionSet.addContextAction(this, ContextActions.SET_AWACS_REFERENCE) if (this.canTargetPoint()) { /* Context actions that require a target position */ @@ -1858,7 +1897,9 @@ export class GroundUnit extends Unit { showSummary: false, showCallsign: belongsToCommandedCoalition, rotateToHeading: false, - }; + showBullseyes: false, + showBRAA: false, + } as ObjectIconOptions; } appendContextActions(contextActionSet: ContextActionSet) { @@ -1927,7 +1968,9 @@ export class NavyUnit extends Unit { showSummary: false, showCallsign: belongsToCommandedCoalition, rotateToHeading: false, - }; + showBullseyes: false, + showBRAA: false, + } as ObjectIconOptions; } appendContextActions(contextActionSet: ContextActionSet) { diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index c443fd0a..700189a6 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -23,6 +23,7 @@ import { UnitDataFileImport } from "./importexport/unitdatafileimport"; import { CoalitionCircle } from "../map/coalitionarea/coalitioncircle"; import { ContextActionSet } from "./contextactionset"; import { + AWACSReferenceChangedEvent, CommandModeOptionsChangedEvent, ContactsUpdatedEvent, HotgroupsChangedEvent, @@ -49,6 +50,7 @@ export class UnitsManager { #unitDataImport!: UnitDataFileImport; #unitDatabase: UnitDatabase; #protectionCallback: (units: Unit[]) => void = (units) => {}; + #AWACSReference: Unit | null = null; constructor() { this.#unitDatabase = new UnitDatabase(); @@ -1125,15 +1127,15 @@ export class UnitsManager { units.forEach((unit: Unit) => unit.setHotgroup(hotgroup)); this.#showActionMessage(units, `added to hotgroup ${hotgroup}`); - let hotgroups: { [key: number]: number } = {}; + let hotgroups: { [key: number]: Unit[] } = {}; for (let ID in this.#units) { const unit = this.#units[ID]; if (unit.getAlive() && !unit.getHuman()) { const hotgroup = unit.getHotgroup(); if (hotgroup) { if (!(hotgroup in hotgroups)) { - hotgroups[hotgroup] = 1; - } else hotgroups[hotgroup] += 1; + hotgroups[hotgroup] = [unit]; + } else hotgroups[hotgroup].push(unit); } } } @@ -1544,6 +1546,15 @@ export class UnitsManager { this.#protectionCallback(this.getSelectedUnits()); } + setAWACSReference(ID) { + this.#AWACSReference = this.#units[ID] ?? null; + AWACSReferenceChangedEvent.dispatch(this.#AWACSReference) + } + + getAWACSReference() { + return this.#AWACSReference; + } + /***********************************************/ #onUnitSelection(unit: Unit) { if (this.getSelectedUnits().length > 0) {