From b021ddaf112f21675e188a47196554ca73cd0403 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Fri, 2 Aug 2024 17:33:48 +0200 Subject: [PATCH 1/2] Started implementing panel, missing loadout summary --- .../react/src/ui/panels/unitcontrolmenu.tsx | 64 ++++++++++++++++++- frontend/server/src/demo/demo.ts | 16 ++++- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index 2594ed24..d345b6bc 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -34,6 +34,7 @@ import { } from "../components/olicons"; import { Coalition } from "../../types/types"; import { ftToM, knotsToMs, mToFt, msToKnots } from "../../other/utils"; +import { FaGasPump } from "react-icons/fa"; export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { var [selectedUnits, setSelectedUnits] = useState([] as Unit[]); @@ -184,7 +185,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { return ( @@ -716,6 +717,67 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { )} + <> + {selectedUnits.length === 1 && ( +
+
+
40 && + `bg-green-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 + `} + > + + {selectedUnits[0].getFuel()}% +
+
+
+ {selectedUnits[0].getAmmo().map((ammo) => { + return ( +
+
+ {ammo.quantity} +
+
+ {ammo.name} +
+
+ ); + })} +
+
+ )} +
); } diff --git a/frontend/server/src/demo/demo.ts b/frontend/server/src/demo/demo.ts index 8a1db149..5639f45c 100644 --- a/frontend/server/src/demo/demo.ts +++ b/frontend/server/src/demo/demo.ts @@ -66,7 +66,7 @@ module.exports = function (configLocation) { isActiveAWACS: false, onOff: true, followRoads: false, - fuel: 50, + fuel: 10, desiredSpeed: 300, desiredSpeedType: 1, desiredAltitude: 1000, @@ -87,7 +87,19 @@ module.exports = function (configLocation) { prohibitAirWpn: false, prohibitJettison: false, }, - ammo: [], + ammo: [{ + quantity: 2, + name: "A super nice missile", + guidance: 1, + category: 1, + missileCategory: 1 + }, { + quantity: 4, + name: "A less nice missile", + guidance: 1, + category: 1, + missileCategory: 1 + }], contacts: [], activePath: [{ lat: 37.1, lng: -116.1 }], isLeader: true, From 57f3b84f20d06877041935a4a078439b7a0f4ff3 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Mon, 5 Aug 2024 16:05:51 +0200 Subject: [PATCH 2/2] Added fuel and loadout panel, also added search tool --- frontend/react/src/interfaces.ts | 1 - frontend/react/src/other/utils.ts | 52 + .../react/src/ui/components/olcheckbox.tsx | 4 + .../react/src/ui/components/oldropdown.tsx | 127 +- .../react/src/ui/components/olsearchbar.tsx | 9 +- .../react/src/ui/panels/components/menu.tsx | 2 +- frontend/react/src/ui/panels/header.tsx | 38 +- frontend/react/src/ui/panels/sidebar.tsx | 7 - frontend/react/src/ui/panels/spawnmenu.tsx | 110 +- .../react/src/ui/panels/unitcontrolmenu.tsx | 1464 ++++++++++------- frontend/react/src/ui/ui.tsx | 6 +- frontend/react/src/unit/unit.ts | 13 - 12 files changed, 1097 insertions(+), 736 deletions(-) diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index b1a3be44..6c8c4904 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -135,7 +135,6 @@ export interface Offset { export interface UnitData { category: string; - categoryDisplayName: string; ID: number; alive: boolean; human: boolean; diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index 3ec5ba96..d9d577aa 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -601,3 +601,55 @@ export function getFunctionArguments(func) { if (result === null) result = []; return result; } + +export function filterBlueprintsByLabel( + blueprints: { [key: string]: UnitBlueprint }, + filterString: string +) { + var filteredBlueprints: { [key: string]: UnitBlueprint } = {}; + if (blueprints) { + Object.entries(blueprints).forEach(([key, value]) => { + if ( + value.enabled && + (filterString === "" || value.label.includes(filterString)) + ) + filteredBlueprints[key] = value; + }); + } + return filteredBlueprints; +} + +export function getUnitsByLabel(filterString: string) { + /* Filter aircrafts, helicopters, and navyunits */ + const filteredAircraft = filterBlueprintsByLabel( + getApp()?.getAircraftDatabase()?.blueprints, + filterString + ); + const filteredHelicopters = filterBlueprintsByLabel( + getApp()?.getHelicopterDatabase()?.blueprints, + filterString + ); + const filteredNavyUnits = filterBlueprintsByLabel( + getApp()?.getNavyUnitDatabase()?.blueprints, + filterString + ); + + /* Split ground units between air defence and all others */ + var filteredAirDefense: { [key: string]: UnitBlueprint } = {}; + var filteredGroundUnits: { [key: string]: UnitBlueprint } = {}; + Object.keys(getApp()?.getGroundUnitDatabase()?.blueprints ?? {}).forEach( + (key) => { + var blueprint = getApp()?.getGroundUnitDatabase()?.blueprints[key]; + var type = blueprint.label; + if (/\bAAA|SAM\b/.test(type) || /\bmanpad|stinger\b/i.test(type)) { + filteredAirDefense[key] = blueprint; + } else { + filteredGroundUnits[key] = blueprint; + } + } + ); + filteredAirDefense = filterBlueprintsByLabel(filteredAirDefense, filterString); + filteredGroundUnits = filterBlueprintsByLabel(filteredGroundUnits, filterString); + + return [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] +} \ No newline at end of file diff --git a/frontend/react/src/ui/components/olcheckbox.tsx b/frontend/react/src/ui/components/olcheckbox.tsx index 58d40b1d..2b3201cb 100644 --- a/frontend/react/src/ui/components/olcheckbox.tsx +++ b/frontend/react/src/ui/components/olcheckbox.tsx @@ -2,6 +2,8 @@ import React, { ChangeEvent } from "react"; export function OlCheckbox(props: { checked: boolean; + className?: string; + disabled?: boolean; onChange: (e: ChangeEvent) => void; }) { return ( @@ -10,7 +12,9 @@ export function OlCheckbox(props: { type="checkbox" checked={props.checked} value="" + disabled={props.disabled ?? false} className={` + ${props.className ?? ""} my-auto h-4 w-4 cursor-pointer rounded border-gray-300 bg-gray-100 text-blue-600 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 diff --git a/frontend/react/src/ui/components/oldropdown.tsx b/frontend/react/src/ui/components/oldropdown.tsx index a55b7336..c39e7e76 100644 --- a/frontend/react/src/ui/components/oldropdown.tsx +++ b/frontend/react/src/ui/components/oldropdown.tsx @@ -1,17 +1,21 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, MutableRefObject } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; export function OlDropdown(props: { - className: string; + className?: string; leftIcon?: IconProp; rightIcon?: IconProp; - label: string; + label?: string; children?: JSX.Element | JSX.Element[]; + buttonRef?: MutableRefObject | null; + open?: boolean; }) { - var [open, setOpen] = useState(false); + var [open, setOpen] = + props.open !== undefined ? [props.open, () => {}] : useState(false); var contentRef = useRef(null); - var buttonRef = useRef(null); + var buttonRef = + props.buttonRef !== undefined ? props.buttonRef : useRef(null); function setPosition(content: HTMLDivElement, button: HTMLButtonElement) { /* Reset the position of the content */ @@ -19,21 +23,7 @@ export function OlDropdown(props: { content.style.top = "0px"; content.style.height = ""; - /* Get the position and size of the button */ - var [bxl, byt, bxr, byb, bw, bh] = [ - button.getBoundingClientRect().x, - button.getBoundingClientRect().y, - button.getBoundingClientRect().x + button.clientWidth, - button.getBoundingClientRect().y + button.clientHeight, - button.clientWidth, - button.clientHeight, - ]; - - /* Set the minimum and maximum width to be equal to the button width */ - content.style.minWidth = `${bw}px`; - content.style.maxWidth = `${bw}px`; - - /* Get the position and size of the content element */ + /* Get the position and size of the button and the content elements */ var [cxl, cyt, cxr, cyb, cw, ch] = [ content.getBoundingClientRect().x, content.getBoundingClientRect().y, @@ -42,6 +32,14 @@ export function OlDropdown(props: { content.clientWidth, content.clientHeight, ]; + var [bxl, byt, bxr, byb, bbw, bh] = [ + button.getBoundingClientRect().x, + button.getBoundingClientRect().y, + button.getBoundingClientRect().x + button.clientWidth, + button.getBoundingClientRect().y + button.clientHeight, + button.clientWidth, + button.clientHeight, + ]; /* Limit the maximum height */ if (ch > 400) { @@ -70,10 +68,11 @@ export function OlDropdown(props: { /* Apply the offset */ content.style.left = `${offsetX}px`; content.style.top = `${offsetY}px`; + content.style.width = `${bbw}px`; } useEffect(() => { - if (contentRef.current && buttonRef.current) { + if (contentRef.current && buttonRef?.current) { const content = contentRef.current as HTMLDivElement; const button = buttonRef.current as HTMLButtonElement; @@ -94,53 +93,57 @@ export function OlDropdown(props: { }); return ( -
- + {props.leftIcon && ( + + )} + + {props.label ?? ""} + + + + + + )}
{})} className={` ${props.className ?? ""} - flex cursor-pointer select-none flex-row content-center rounded-md px-4 - py-2 + flex w-full cursor-pointer select-none flex-row content-center + rounded-md px-4 py-2 dark:hover:bg-gray-600 dark:hover:text-white hover:bg-gray-100 `} diff --git a/frontend/react/src/ui/components/olsearchbar.tsx b/frontend/react/src/ui/components/olsearchbar.tsx index 60eea8d9..131e2819 100644 --- a/frontend/react/src/ui/components/olsearchbar.tsx +++ b/frontend/react/src/ui/components/olsearchbar.tsx @@ -1,15 +1,17 @@ import { faMultiply, faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { ChangeEvent, useId, useRef } from "react"; +import React, { useId, useRef } from "react"; export function OlSearchBar(props: { - onChange: (e: ChangeEvent) => void; + onChange: (e: string) => void; + text: string; }) { const searchId = useId(); const inputRef = useRef(null); function resetSearch() { inputRef.current && ((inputRef.current as HTMLInputElement).value = ""); + props.onChange(""); } return ( @@ -36,7 +38,7 @@ export function OlSearchBar(props: { type="search" ref={inputRef} id={searchId} - onChange={props.onChange} + onChange={(e) => props.onChange(e.currentTarget.value)} className={` mb-2 block w-full rounded-full border border-gray-300 bg-gray-50 p-3 ps-10 text-sm text-gray-900 @@ -46,6 +48,7 @@ export function OlSearchBar(props: { focus:border-blue-500 focus:ring-blue-500 `} placeholder="Search" + value={props.text} required /> {props.canBeHidden == true && (
setHide(!hide)}> diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index 0ffc0902..5d77d7f9 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -35,18 +35,17 @@ export function Header() { const [scrolledLeft, setScrolledLeft] = useState(true); const [scrolledRight, setScrolledRight] = useState(false); - /* Initialize the "scroll" position of the element */ - var scrollRef = useRef(null); - useEffect(() => { - if (scrollRef.current) { - onScroll(scrollRef.current); - } - }); + /* Initialize the "scroll" position of the element */ + var scrollRef = useRef(null); + useEffect(() => { + if (scrollRef.current) { + onScroll(scrollRef.current); + } + }); function onScroll(el) { const sl = el.scrollLeft; - const sr = - el.scrollWidth - el.scrollLeft - el.clientWidth; + const sr = el.scrollWidth - el.scrollLeft - el.clientWidth; sl < 1 && !scrolledLeft && setScrolledLeft(true); sl > 1 && scrolledLeft && setScrolledLeft(false); @@ -249,29 +248,28 @@ export function Header() { onClick={() => {}} tooltip="Activate/deactivate camera plugin" /> - + {appState.mapSources.map((source) => { return ( getApp().getMap().setLayerName(source)} > - {source} +
{source}
); })}
{!scrolledRight && ( - - )} + + )} )} diff --git a/frontend/react/src/ui/panels/sidebar.tsx b/frontend/react/src/ui/panels/sidebar.tsx index 0cf62d6f..60627f0d 100644 --- a/frontend/react/src/ui/panels/sidebar.tsx +++ b/frontend/react/src/ui/panels/sidebar.tsx @@ -11,15 +11,8 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { EventsConsumer } from "../../eventscontext"; import { StateConsumer } from "../../statecontext"; -import { IDLE, SPAWN_UNIT } from "../../constants/constants"; export function SideBar() { - const [mapState, setMapState] = useState(IDLE); - - document.addEventListener("mapStateChanged", (ev) => { - setMapState((ev as CustomEvent).detail); - }); - return ( {(appState) => ( diff --git a/frontend/react/src/ui/panels/spawnmenu.tsx b/frontend/react/src/ui/panels/spawnmenu.tsx index 9771d87c..e20b948f 100644 --- a/frontend/react/src/ui/panels/spawnmenu.tsx +++ b/frontend/react/src/ui/panels/spawnmenu.tsx @@ -16,26 +16,10 @@ import { olButtonsVisibilityNavyunit, } from "../components/olicons"; import { IDLE, SPAWN_UNIT } from "../../constants/constants"; +import { getUnitsByLabel } from "../../other/utils"; library.add(faPlus); -function filterUnits( - blueprints: { [key: string]: UnitBlueprint }, - filterString: string -) { - var filteredUnits = {}; - if (blueprints) { - Object.entries(blueprints).forEach(([key, value]) => { - if ( - value.enabled && - (filterString === "" || value.label.includes(filterString)) - ) - filteredUnits[key] = value; - }); - } - return filteredUnits; -} - export function SpawnMenu(props: { open: boolean; onClose: () => void; @@ -44,36 +28,13 @@ export function SpawnMenu(props: { var [blueprint, setBlueprint] = useState(null as null | UnitBlueprint); var [filterString, setFilterString] = useState(""); - /* Filter aircrafts, helicopters, and navyunits */ - const filteredAircraft = filterUnits( - getApp()?.getAircraftDatabase()?.blueprints, - filterString - ); - const filteredHelicopters = filterUnits( - getApp()?.getHelicopterDatabase()?.blueprints, - filterString - ); - const filteredNavyUnits = filterUnits( - getApp()?.getNavyUnitDatabase()?.blueprints, - filterString - ); - - /* Split ground units between air defence and all others */ - var filteredAirDefense = {}; - var filteredGroundUnits = {}; - Object.keys(getApp()?.getGroundUnitDatabase()?.blueprints ?? {}).forEach( - (key) => { - var blueprint = getApp()?.getGroundUnitDatabase()?.blueprints[key]; - var type = blueprint.label; - if (/\bAAA|SAM\b/.test(type) || /\bmanpad|stinger\b/i.test(type)) { - filteredAirDefense[key] = blueprint; - } else { - filteredGroundUnits[key] = blueprint; - } - } - ); - filteredAirDefense = filterUnits(filteredAirDefense, filterString); - filteredGroundUnits = filterUnits(filteredGroundUnits, filterString); + const [ + filteredAircraft, + filteredHelicopters, + filteredAirDefense, + filteredGroundUnits, + filteredNavyUnits, + ] = getUnitsByLabel(filterString); useEffect(() => { if (!props.open) { @@ -97,22 +58,23 @@ export function SpawnMenu(props: { <> {blueprint === null && (
- setFilterString(ev.target.value)} /> + setFilterString(value)} + text={filterString} + />
- {Object.keys(filteredAircraft).map((key) => { - const blueprint = - getApp().getAircraftDatabase().blueprints[key]; + {Object.entries(filteredAircraft).map((entry) => { return ( setBlueprint(blueprint)} + blueprint={entry[1]} + onClick={() => setBlueprint(entry[1])} /> ); })} @@ -124,15 +86,13 @@ export function SpawnMenu(props: { flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar `} > - {Object.keys(filteredHelicopters).map((key) => { - const blueprint = - getApp().getHelicopterDatabase().blueprints[key]; + {Object.entries(filteredHelicopters).map((entry) => { return ( setBlueprint(blueprint)} + blueprint={entry[1]} + onClick={() => setBlueprint(entry[1])} /> ); })} @@ -144,15 +104,13 @@ export function SpawnMenu(props: { flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar `} > - {Object.keys(filteredAirDefense).map((key) => { - const blueprint = - getApp().getGroundUnitDatabase().blueprints[key]; + {Object.entries(filteredAirDefense).map((entry) => { return ( setBlueprint(blueprint)} + blueprint={entry[1]} + onClick={() => setBlueprint(entry[1])} /> ); })} @@ -164,15 +122,13 @@ export function SpawnMenu(props: { flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar `} > - {Object.keys(filteredGroundUnits).map((key) => { - const blueprint = - getApp().getGroundUnitDatabase().blueprints[key]; + {Object.entries(filteredGroundUnits).map((entry) => { return ( setBlueprint(blueprint)} + blueprint={entry[1]} + onClick={() => setBlueprint(entry[1])} /> ); })} @@ -184,15 +140,13 @@ export function SpawnMenu(props: { flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar `} > - {Object.keys(filteredNavyUnits).map((key) => { - const blueprint = - getApp().getNavyUnitDatabase().blueprints[key]; + {Object.entries(filteredNavyUnits).map((entry) => { return ( setBlueprint(blueprint)} + blueprint={entry[1]} + onClick={() => setBlueprint(entry[1])} /> ); })} diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index d345b6bc..912b953e 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -1,10 +1,11 @@ -import React, { useState } from "react"; +import React, { MutableRefObject, useEffect, useRef, useState } from "react"; import { Menu } from "./components/menu"; import { Unit } from "../../unit/unit"; import { OlLabelToggle } from "../components/ollabeltoggle"; import { OlRangeSlider } from "../components/olrangeslider"; import { getApp } from "../../olympusapp"; import { OlButtonGroup, OlButtonGroupItem } from "../components/olbuttongroup"; +import { OlCheckbox } from "../components/olcheckbox"; import { ROEs, emissionsCountermeasures, @@ -31,14 +32,31 @@ import { olButtonsThreatManoeuvre, olButtonsThreatNone, olButtonsThreatPassive, + olButtonsVisibilityAircraft, + olButtonsVisibilityDcs, + olButtonsVisibilityGroundunit, + olButtonsVisibilityGroundunitSam, + olButtonsVisibilityHelicopter, + olButtonsVisibilityHuman, + olButtonsVisibilityNavyunit, + olButtonsVisibilityOlympus, } from "../components/olicons"; import { Coalition } from "../../types/types"; -import { ftToM, knotsToMs, mToFt, msToKnots } from "../../other/utils"; -import { FaGasPump } from "react-icons/fa"; +import { + ftToM, + getUnitsByLabel, + knotsToMs, + mToFt, + msToKnots, +} from "../../other/utils"; +import { FaGasPump, FaQuestionCircle } from "react-icons/fa"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { OlSearchBar } from "../components/olsearchbar"; +import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; +import { UnitBlueprint } from "../../interfaces"; export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { var [selectedUnits, setSelectedUnits] = useState([] as Unit[]); - var [selectedUnitsData, setSelectedUnitsData] = useState({ desiredAltitude: undefined as undefined | number, desiredAltitudeType: undefined as undefined | string, @@ -55,6 +73,51 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { isActiveTanker: undefined as undefined | boolean, onOff: undefined as undefined | boolean, }); + var [selectionFilter, setSelectionFilter] = useState({ + control: { + human: true, + dcs: true, + olympus: true, + }, + blue: { + aircraft: true, + helicopter: true, + "groundunit-sam": true, + groundunit: true, + navyunit: true, + }, + neutral: { + aircraft: true, + helicopter: true, + "groundunit-sam": true, + groundunit: true, + navyunit: true, + }, + red: { + aircraft: true, + helicopter: true, + "groundunit-sam": true, + groundunit: true, + navyunit: true, + }, + }); + var [selectionBlueprint, setSelectionBlueprint] = useState( + null as null | UnitBlueprint + ); + const [searchBarRefState, setSearchBarRefState] = useState( + null as MutableRefObject | null + ); + const [filterString, setFilterString] = useState(""); + + var searchBarRef = useRef(null); + + useEffect(() => { + if (!searchBarRefState) setSearchBarRefState(searchBarRef); + + if (!props.open && selectionBlueprint !== null) setSelectionBlueprint(null); + + if (!props.open && filterString !== "") setFilterString(""); + }); /* */ const minAltitude = 0; @@ -182,600 +245,903 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { const selectedCategories = getApp()?.getUnitsManager()?.getSelectedUnitsCategories() ?? []; + const [ + filteredAircraft, + filteredHelicopters, + filteredAirDefense, + filteredGroundUnits, + filteredNavyUnits, + ] = getUnitsByLabel(filterString); + + const mergedFilteredUnits = Object.assign( + {}, + filteredAircraft, + filteredHelicopters, + filteredAirDefense, + filteredGroundUnits, + filteredNavyUnits + ) as { [key: string]: UnitBlueprint }; + return ( 0 + ? `Units selected (x${selectedUnits.length})` + : `No units selected` + } onClose={props.onClose} canBeHidden={true} > - {/* Units list */} -
-
- { - <> - {["blue", "red", "neutral"].map((coalition) => { - return Object.keys(unitOccurences[coalition]).map((name) => { - return ( -
- - {name} - - - x{unitOccurences[coalition][name]} - -
- ); - }); - })} - - } -
-
-
- { - /* Altitude selector */ - selectedCategories.every((category) => { - return ["Aircraft", "Helicopter"].includes(category); - }) && ( -
+ <> + {selectedUnits.length == 0 && ( +
+
+ Selection tool +
+
+ The selection tools allows you to select units depending on their category, coalition, and control mode. You can also + select units depending on their specific type by using the search input. +
+
-
+ Control mode +
+
+ {Object.entries({ + human: ["Human", olButtonsVisibilityHuman], + olympus: ["Olympus controlled", olButtonsVisibilityOlympus], + dcs: ["From DCS mission", olButtonsVisibilityDcs], + }).map((entry) => { + return ( +
+ + {entry[1][0] as string} + + { + selectionFilter["control"][entry[0]] = + !selectionFilter["control"][entry[0]]; + setSelectionFilter( + JSON.parse(JSON.stringify(selectionFilter)) + ); + }} + toggled={selectionFilter["control"][entry[0]]} + /> +
+ ); + })} +
+ +
+ Types and coalitions +
+ + + + + + + + {selectionBlueprint === null && + Object.entries({ + aircraft: olButtonsVisibilityAircraft, + helicopter: olButtonsVisibilityHelicopter, + "groundunit-sam": olButtonsVisibilityGroundunitSam, + groundunit: olButtonsVisibilityGroundunit, + navyunit: olButtonsVisibilityNavyunit, + }).map((entry) => { + return ( + + + {["blue", "neutral", "red"].map((coalition) => { + return ( + + ); + })} + + ); + })} + + + + + + +
+ BLUE + + NEUTRAL + + RED +
+ + + { + selectionFilter[coalition][entry[0]] = + !selectionFilter[coalition][entry[0]]; + setSelectionFilter( + JSON.parse(JSON.stringify(selectionFilter)) + ); + }} + /> +
+ value + )} + onChange={() => { + const newValue = !Object.values( + selectionFilter["blue"] + ).some((value) => value); + Object.keys(selectionFilter["blue"]).forEach((key) => { + selectionFilter["blue"][key] = newValue; + }); + setSelectionFilter( + JSON.parse(JSON.stringify(selectionFilter)) + ); + }} + /> + + value + )} + onChange={() => { + const newValue = !Object.values( + selectionFilter["neutral"] + ).some((value) => value); + Object.keys(selectionFilter["neutral"]).forEach( + (key) => { + selectionFilter["neutral"][key] = newValue; + } + ); + setSelectionFilter( + JSON.parse(JSON.stringify(selectionFilter)) + ); + }} + /> + + value + )} + onChange={() => { + const newValue = !Object.values( + selectionFilter["red"] + ).some((value) => value); + Object.keys(selectionFilter["red"]).forEach((key) => { + selectionFilter["red"][key] = newValue; + }); + setSelectionFilter( + JSON.parse(JSON.stringify(selectionFilter)) + ); + }} + /> +
+
+
+ { + setFilterString(value); + selectionBlueprint && setSelectionBlueprint(null); + }} + text={ + selectionBlueprint + ? selectionBlueprint.label + : filterString + } + /> +
+ +
+ {filterString !== "" && + Object.keys(mergedFilteredUnits).length > 0 && + Object.entries(mergedFilteredUnits).map((entry) => { + const blueprint = entry[1]; + return ( + { + setSelectionBlueprint(blueprint); + }} + > + {blueprint.label} + + ); + })} + {Object.keys(mergedFilteredUnits).length == 0 && ( + No results + )} +
+
+
+
+ +
+ )} + + <> + {selectedUnits.length > 0 && ( + <> + {/* Units list */} +
+
+ { + <> + {["blue", "red", "neutral"].map((coalition) => { + return Object.keys(unitOccurences[coalition]).map( + (name) => { + return ( +
+ + {name} + + + x{unitOccurences[coalition][name]} + +
+ ); + } + ); + })} + + } +
+
+
+ { + /* Altitude selector */ + selectedCategories.every((category) => { + return ["Aircraft", "Helicopter"].includes(category); + }) && ( +
+
+
+ + Altitude + + + {selectedUnitsData.desiredAltitude !== undefined + ? Intl.NumberFormat("en-US").format( + selectedUnitsData.desiredAltitude + ) + " FT" + : "Different values"} + +
+ { + selectedUnits.forEach((unit) => { + unit.setAltitudeType( + selectedUnitsData.desiredAltitudeType === "ASL" + ? "AGL" + : "ASL" + ); + setSelectedUnitsData({ + ...selectedUnitsData, + desiredAltitudeType: + selectedUnitsData.desiredAltitudeType === "ASL" + ? "AGL" + : "ASL", + }); + }); + }} + /> +
+ { + selectedUnits.forEach((unit) => { + unit.setAltitude(ftToM(Number(ev.target.value))); + setSelectedUnitsData({ + ...selectedUnitsData, + desiredAltitude: Number(ev.target.value), + }); + }); + }} + value={selectedUnitsData.desiredAltitude} + min={minAltitude} + max={maxAltitude} + step={altitudeStep} + /> +
+ ) + } + {/* Airspeed selector */} +
+
+
+ + Speed + + + {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", + }); + }); + }} + /> +
+ { + selectedUnits.forEach((unit) => { + unit.setSpeed(knotsToMs(Number(ev.target.value))); + setSelectedUnitsData({ + ...selectedUnitsData, + desiredSpeed: Number(ev.target.value), + }); + }); + }} + value={selectedUnitsData.desiredSpeed} + min={minSpeed} + max={maxSpeed} + step={speedStep} + /> +
+
+ + Rules of engagement + + + {[ + olButtonsRoeHold, + olButtonsRoeReturn, + olButtonsRoeDesignated, + olButtonsRoeFree, + ].map((icon, idx) => { + return ( + { + selectedUnits.forEach((unit) => { + unit.setROE(ROEs[idx]); + setSelectedUnitsData({ + ...selectedUnitsData, + ROE: ROEs[idx], + }); + }); + }} + active={selectedUnitsData.ROE === ROEs[idx]} + icon={icon} + /> + ); + })} + +
+ {selectedCategories.every((category) => { + return ["Aircraft", "Helicopter"].includes(category); + }) && ( + <> + {" "} +
+ + Threat reaction + + + {[ + olButtonsThreatNone, + olButtonsThreatPassive, + olButtonsThreatManoeuvre, + olButtonsThreatEvade, + ].map((icon, idx) => { + return ( + { + selectedUnits.forEach((unit) => { + unit.setReactionToThreat( + reactionsToThreat[idx] + ); + setSelectedUnitsData({ + ...selectedUnitsData, + reactionToThreat: reactionsToThreat[idx], + }); + }); + }} + active={ + selectedUnitsData.reactionToThreat === + reactionsToThreat[idx] + } + icon={icon} + /> + ); + })} + +
+
+ + Radar and ECM + + + {[ + olButtonsEmissionsSilent, + olButtonsEmissionsDefend, + olButtonsEmissionsAttack, + olButtonsEmissionsFree, + ].map((icon, idx) => { + return ( + { + selectedUnits.forEach((unit) => { + unit.setEmissionsCountermeasures( + emissionsCountermeasures[idx] + ); + setSelectedUnitsData({ + ...selectedUnitsData, + emissionsCountermeasures: + emissionsCountermeasures[idx], + }); + }); + }} + active={ + selectedUnitsData.emissionsCountermeasures === + emissionsCountermeasures[idx] + } + icon={icon} + /> + ); + })} + +
+ + )} + {getApp() + ?.getUnitsManager() + ?.getSelectedUnitsVariable((unit) => { + return unit.isTanker(); + }) && ( +
- Altitude + {" "} + Act as tanker{" "} + { + selectedUnits.forEach((unit) => { + unit.setAdvancedOptions( + !selectedUnitsData.isActiveTanker, + unit.getIsActiveAWACS(), + unit.getTACAN(), + unit.getRadio(), + unit.getGeneralSettings() + ); + setSelectedUnitsData({ + ...selectedUnitsData, + isActiveTanker: !selectedUnitsData.isActiveTanker, + }); + }); + }} + /> +
+ )} + {getApp() + ?.getUnitsManager() + ?.getSelectedUnitsVariable((unit) => { + return unit.isAWACS(); + }) && ( +
- {selectedUnitsData.desiredAltitude !== undefined - ? Intl.NumberFormat("en-US").format( - selectedUnitsData.desiredAltitude - ) + " FT" - : "Different values"} + {" "} + Act as AWACS{" "} -
- { - selectedUnits.forEach((unit) => { - unit.setAltitudeType( - selectedUnitsData.desiredAltitudeType === "ASL" - ? "AGL" - : "ASL" - ); - setSelectedUnitsData({ - ...selectedUnitsData, - desiredAltitudeType: - selectedUnitsData.desiredAltitudeType === "ASL" - ? "AGL" - : "ASL", - }); - }); - }} - /> -
- { - selectedUnits.forEach((unit) => { - unit.setAltitude(ftToM(Number(ev.target.value))); - setSelectedUnitsData({ - ...selectedUnitsData, - desiredAltitude: Number(ev.target.value), - }); - }); - }} - value={selectedUnitsData.desiredAltitude} - min={minAltitude} - max={maxAltitude} - step={altitudeStep} - /> -
- ) - } - {/* Airspeed selector */} -
-
-
- - Speed - - - {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", - }); - }); - }} - /> -
- { - selectedUnits.forEach((unit) => { - unit.setSpeed(knotsToMs(Number(ev.target.value))); - setSelectedUnitsData({ - ...selectedUnitsData, - desiredSpeed: Number(ev.target.value), - }); - }); - }} - value={selectedUnitsData.desiredSpeed} - min={minSpeed} - max={maxSpeed} - step={speedStep} - /> -
-
- - Rules of engagement - - - {[ - olButtonsRoeHold, - olButtonsRoeReturn, - olButtonsRoeDesignated, - olButtonsRoeFree, - ].map((icon, idx) => { - return ( - { - selectedUnits.forEach((unit) => { - unit.setROE(ROEs[idx]); - setSelectedUnitsData({ - ...selectedUnitsData, - ROE: ROEs[idx], - }); - }); - }} - active={selectedUnitsData.ROE === ROEs[idx]} - icon={icon} - /> - ); - })} - -
- {selectedCategories.every((category) => { - return ["Aircraft", "Helicopter"].includes(category); - }) && ( - <> - {" "} -
- - Threat reaction - - - {[ - olButtonsThreatNone, - olButtonsThreatPassive, - olButtonsThreatManoeuvre, - olButtonsThreatEvade, - ].map((icon, idx) => { - return ( - { - selectedUnits.forEach((unit) => { - unit.setReactionToThreat(reactionsToThreat[idx]); - setSelectedUnitsData({ - ...selectedUnitsData, - reactionToThreat: reactionsToThreat[idx], - }); + { + selectedUnits.forEach((unit) => { + unit.setAdvancedOptions( + unit.getIsActiveTanker(), + !selectedUnitsData.isActiveAWACS, + unit.getTACAN(), + unit.getRadio(), + unit.getGeneralSettings() + ); + setSelectedUnitsData({ + ...selectedUnitsData, + isActiveAWACS: !selectedUnitsData.isActiveAWACS, }); - }} - active={ - selectedUnitsData.reactionToThreat === - reactionsToThreat[idx] - } - icon={icon} - /> - ); - })} - -
-
- - Radar and ECM - - - {[ - olButtonsEmissionsSilent, - olButtonsEmissionsDefend, - olButtonsEmissionsAttack, - olButtonsEmissionsFree, - ].map((icon, idx) => { - return ( - +
+ )} + {selectedCategories.every((category) => { + return ["GroundUnit", "NavyUnit"].includes(category); + }) && ( + <> + {" "} +
+ + 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 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} + /> + ); + })} + +
+
+ + {" "} + Operate as{" "} + + { selectedUnits.forEach((unit) => { - unit.setEmissionsCountermeasures( - emissionsCountermeasures[idx] + unit.setOperateAs( + selectedUnitsData.operateAs === "blue" + ? "red" + : "blue" ); setSelectedUnitsData({ ...selectedUnitsData, - emissionsCountermeasures: - emissionsCountermeasures[idx], + operateAs: + selectedUnitsData.operateAs === "blue" + ? "red" + : "blue", }); }); }} - active={ - selectedUnitsData.emissionsCountermeasures === - emissionsCountermeasures[idx] - } - icon={icon} /> - ); - })} - -
- - )} - {getApp() - ?.getUnitsManager() - ?.getSelectedUnitsVariable((unit) => { - return unit.isTanker(); - }) && ( -
- - {" "} - Act as tanker{" "} - - { - selectedUnits.forEach((unit) => { - unit.setAdvancedOptions( - !selectedUnitsData.isActiveTanker, - unit.getIsActiveAWACS(), - unit.getTACAN(), - unit.getRadio(), - unit.getGeneralSettings() - ); - setSelectedUnitsData({ - ...selectedUnitsData, - isActiveTanker: !selectedUnitsData.isActiveTanker, - }); - }); - }} - /> -
- )} - {getApp() - ?.getUnitsManager() - ?.getSelectedUnitsVariable((unit) => { - return unit.isAWACS(); - }) && ( -
- - {" "} - Act as AWACS{" "} - - { - selectedUnits.forEach((unit) => { - unit.setAdvancedOptions( - unit.getIsActiveTanker(), - !selectedUnitsData.isActiveAWACS, - unit.getTACAN(), - unit.getRadio(), - unit.getGeneralSettings() - ); - setSelectedUnitsData({ - ...selectedUnitsData, - isActiveAWACS: !selectedUnitsData.isActiveAWACS, - }); - }); - }} - /> -
- )} - {selectedCategories.every((category) => { - return ["GroundUnit", "NavyUnit"].includes(category); - }) && ( - <> - {" "} -
- - 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 intensity - - - {[ - olButtonsIntensity1, - olButtonsIntensity2, - olButtonsIntensity3, - ].map((icon, idx) => { - return ( - +
+ + {" "} + Follow roads{" "} + + { selectedUnits.forEach((unit) => { - unit.setShotsIntensity(idx + 1); + unit.setFollowRoads(!selectedUnitsData.followRoads); setSelectedUnitsData({ ...selectedUnitsData, - shotsIntensity: idx + 1, + followRoads: !selectedUnitsData.followRoads, }); }); }} - active={selectedUnitsData.shotsIntensity === idx + 1} - icon={icon} /> - ); - })} - -
-
- - {" "} - Operate as{" "} - - { - selectedUnits.forEach((unit) => { - unit.setOperateAs( - selectedUnitsData.operateAs === "blue" ? "red" : "blue" - ); - setSelectedUnitsData({ - ...selectedUnitsData, - operateAs: - selectedUnitsData.operateAs === "blue" ? "red" : "blue", - }); - }); - }} - /> -
-
- - {" "} - Follow roads{" "} - - { - selectedUnits.forEach((unit) => { - unit.setFollowRoads(!selectedUnitsData.followRoads); - setSelectedUnitsData({ - ...selectedUnitsData, - followRoads: !selectedUnitsData.followRoads, - }); - }); - }} - /> -
-
- - {" "} - Unit active{" "} - - { - selectedUnits.forEach((unit) => { - unit.setOnOff(!selectedUnitsData.onOff); - setSelectedUnitsData({ - ...selectedUnitsData, - onOff: !selectedUnitsData.onOff, - }); - }); - }} - /> -
- - )} -
- <> - {selectedUnits.length === 1 && ( -
-
-
40 && - `bg-green-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 - `} - > - - {selectedUnits[0].getFuel()}% -
-
-
- {selectedUnits[0].getAmmo().map((ammo) => { - return ( -
-
+
+ - {ammo.quantity} -
+ {" "} + Unit active{" "} + + { + selectedUnits.forEach((unit) => { + unit.setOnOff(!selectedUnitsData.onOff); + setSelectedUnitsData({ + ...selectedUnitsData, + onOff: !selectedUnitsData.onOff, + }); + }); + }} + /> +
+ + )} +
+ <> + {selectedUnits.length === 1 && ( +
+
40 && `bg-green-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 `} > - {ammo.name} + + {selectedUnits[0].getFuel()}%
- ); - })} -
-
+
+ {selectedUnits[0].getAmmo().map((ammo) => { + return ( +
+
+ {ammo.quantity} +
+
+ {ammo.name} +
+
+ ); + })} +
+
+ )} + + )}
diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index c1a24e96..3092f52f 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -54,6 +54,7 @@ export function UI() { var [mapSources, setMapSources] = useState([] as string[]); var [activeMapSource, setActiveMapSource] = useState(""); var [mapBoxSelection, setMapBoxSelection] = useState(false); + var [mapState, setMapState] = useState(IDLE); document.addEventListener("hiddenTypesChanged", (ev) => { setMapHiddenTypes({ ...getApp().getMap().getHiddenTypes() }); @@ -64,9 +65,10 @@ export function UI() { }); document.addEventListener("mapStateChanged", (ev) => { - if ((ev as CustomEvent).detail == IDLE) { + if ((ev as CustomEvent).detail === IDLE && mapState !== IDLE) hideAllMenus(); - } + + setMapState(String((ev as CustomEvent).detail)) }); document.addEventListener("mapSourceChanged", (ev) => { diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 1b7bf65e..6d842a99 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -443,18 +443,6 @@ export abstract class Unit extends CustomMarker { */ abstract getDefaultMarker(): string; - /** Get the category but for display use - for the user. (i.e. has spaces in it) - * - * @returns string - */ - getCategoryLabel() { - return ( - GROUND_UNIT_AIR_DEFENCE_REGEX.test(this.getType()) - ? "Air Defence" - : this.getCategory() - ).replace(/([a-z])([A-Z])/g, "$1 $2"); - } - /********************** Unit data *************************/ /** This function is called by the units manager to update all the data coming from the backend. It reads the binary raw data using a DataExtractor * @@ -657,7 +645,6 @@ export abstract class Unit extends CustomMarker { getData(): UnitData { return { category: this.getCategory(), - categoryDisplayName: this.getCategoryLabel(), ID: this.ID, alive: this.#alive, human: this.#human,