diff --git a/frontend/react/package.json b/frontend/react/package.json index 012f0c6e..dc94f284 100644 --- a/frontend/react/package.json +++ b/frontend/react/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.8.1", "chart.js": "^4.4.7", "react-chartjs-2": "^5.3.0", "react-circular-progressbar": "^2.1.0", diff --git a/frontend/react/public/images/others/arrow.svg b/frontend/react/public/images/others/arrow.svg index 38ef0bce..96c515f6 100644 --- a/frontend/react/public/images/others/arrow.svg +++ b/frontend/react/public/images/others/arrow.svg @@ -22,9 +22,9 @@ inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" - inkscape:zoom="12.703125" - inkscape:cx="27.316114" - inkscape:cy="18.145142" + inkscape:zoom="17.964932" + inkscape:cx="25.549777" + inkscape:cy="24.241673" inkscape:window-width="1920" inkscape:window-height="1009" inkscape:window-x="1912" @@ -37,4 +37,9 @@ id="path1" style="stroke:none;stroke-width:2.30583;stroke-dasharray:none;stroke-opacity:1" sodipodi:nodetypes="csccsscc" /> + diff --git a/frontend/react/public/images/training/starred.png b/frontend/react/public/images/training/starred.png new file mode 100644 index 00000000..39f8064e Binary files /dev/null and b/frontend/react/public/images/training/starred.png differ diff --git a/frontend/react/public/images/training/step1.gif b/frontend/react/public/images/training/step1.gif new file mode 100644 index 00000000..ec744feb Binary files /dev/null and b/frontend/react/public/images/training/step1.gif differ diff --git a/frontend/react/public/images/training/step2.gif b/frontend/react/public/images/training/step2.gif new file mode 100644 index 00000000..855fce9c Binary files /dev/null and b/frontend/react/public/images/training/step2.gif differ diff --git a/frontend/react/public/images/training/step3.gif b/frontend/react/public/images/training/step3.gif new file mode 100644 index 00000000..86544b5d Binary files /dev/null and b/frontend/react/public/images/training/step3.gif differ diff --git a/frontend/react/public/images/training/step4.gif b/frontend/react/public/images/training/step4.gif new file mode 100644 index 00000000..b57d93bf Binary files /dev/null and b/frontend/react/public/images/training/step4.gif differ diff --git a/frontend/react/public/images/training/step5.gif b/frontend/react/public/images/training/step5.gif new file mode 100644 index 00000000..21f0e374 Binary files /dev/null and b/frontend/react/public/images/training/step5.gif differ diff --git a/frontend/react/public/images/training/step6.gif b/frontend/react/public/images/training/step6.gif new file mode 100644 index 00000000..4377fa2e Binary files /dev/null and b/frontend/react/public/images/training/step6.gif differ diff --git a/frontend/react/public/images/training/step7.gif b/frontend/react/public/images/training/step7.gif new file mode 100644 index 00000000..e4758cfc Binary files /dev/null and b/frontend/react/public/images/training/step7.gif differ diff --git a/frontend/react/public/images/training/unitfilter.png b/frontend/react/public/images/training/unitfilter.png new file mode 100644 index 00000000..91416686 Binary files /dev/null and b/frontend/react/public/images/training/unitfilter.png differ diff --git a/frontend/react/public/images/training/unitmarker.png b/frontend/react/public/images/training/unitmarker.png new file mode 100644 index 00000000..76a33b29 Binary files /dev/null and b/frontend/react/public/images/training/unitmarker.png differ diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 7fc93bab..36c2dd8b 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -327,7 +327,8 @@ export enum OlympusState { IMPORT_EXPORT = "Import/export", WARNING = "Warning modal", DATABASE_EDITOR = "Database editor", - MEASURE = "Measure" + MEASURE = "Measure", + TRAINING = "Training" } export const NO_SUBSTATE = "No substate"; diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 1eb296da..e2ba2aa8 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -181,7 +181,7 @@ export class Map extends L.Map { maxBoundsViscosity: 1.0, minZoom: 7, keyboard: true, - keyboardPanDelta: 0 + keyboardPanDelta: 0, }); this.setView([37.23, -115.8], 10); @@ -592,10 +592,20 @@ export class Map extends L.Map { } setSpawnRequestTable(spawnRequestTable: SpawnRequestTable) { + let redrawMarker = false; + if ( + this.#spawnRequestTable?.unit.location !== spawnRequestTable.unit.location || + this.#spawnRequestTable?.unit.unitType !== spawnRequestTable.unit.unitType || + this.#spawnRequestTable?.coalition !== spawnRequestTable.coalition + ) + redrawMarker = true; + this.#spawnRequestTable = spawnRequestTable; - this.#currentSpawnMarker?.removeFrom(this); - this.#currentSpawnMarker = this.addTemporaryMarker(spawnRequestTable.unit.location, spawnRequestTable.unit.unitType, spawnRequestTable.coalition, true); + if (redrawMarker) { + this.#currentSpawnMarker?.removeFrom(this); + this.#currentSpawnMarker = this.addTemporaryMarker(spawnRequestTable.unit.location, spawnRequestTable.unit.unitType, spawnRequestTable.coalition, true); + } } getSpawnRequestTable() { @@ -966,7 +976,7 @@ export class Map extends L.Map { if (Date.now() - this.#rightMouseDownEpoch < SHORT_PRESS_MILLISECONDS) this.#onRightShortClick(e); this.#isRightMouseDown = false; } else if (e.originalEvent?.button === 1) { - getApp().setState(getApp().getState() === OlympusState.MEASURE? OlympusState.IDLE: OlympusState.MEASURE); + getApp().setState(getApp().getState() === OlympusState.MEASURE ? OlympusState.IDLE : OlympusState.MEASURE); if (getApp().getState() === OlympusState.MEASURE) { const newMeasure = new Measure(this); const previousMeasure = this.#measures[this.#measures.length - 1]; @@ -984,7 +994,9 @@ export class Map extends L.Map { } #onMouseDown(e: any) { - if (e.originalEvent.button === 1) {this.dragging.disable();} // Disable dragging when right clicking + if (e.originalEvent.button === 1) { + this.dragging.disable(); + } // Disable dragging when right clicking this.#originalMouseClickLatLng = e.latlng; if (e.originalEvent?.button === 0) { diff --git a/frontend/react/src/map/markers/stylesheets/units.css b/frontend/react/src/map/markers/stylesheets/units.css index 803960b0..a2326448 100644 --- a/frontend/react/src/map/markers/stylesheets/units.css +++ b/frontend/react/src/map/markers/stylesheets/units.css @@ -590,11 +590,12 @@ width: 30px; height: 30px; position: absolute; - top: -40px; + top: -30px; left: 50%; transform: translateX(-50%); cursor: move; pointer-events: all; + opacity: 0.5; } .ol-temporary-marker .heading-handle svg { diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index d0942f12..b4506c10 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -6,6 +6,7 @@ import { Converter } from "usng"; import { MGRS } from "../types/types"; import { featureCollection } from "turf"; import MagVar from "magvar"; +import axios from 'axios'; export function bearing(lat1: number, lon1: number, lat2: number, lon2: number, magnetic = true) { const φ1 = deg2rad(lat1); // φ, λ in radians @@ -654,4 +655,103 @@ export function decimalToRGBA(decimal: number): string { const a = (decimal & 0xff) / 255; return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`; +} + +export async function getWikipediaImage(unitName: string): Promise { + try { + // Search for the unit name on Wikipedia + const searchResponse = await axios.get('https://en.wikipedia.org/w/api.php', { + params: { + action: 'query', + list: 'search', + srsearch: unitName, + format: 'json', + origin: '*' + } + }); + + if (searchResponse.data.query.search.length === 0) { + console.error('No search results found for the unit name.'); + return null; + } + + // Get the title of the first search result + const pageTitle = searchResponse.data.query.search[0].title; + + // Get the page content to find the image + const pageResponse = await axios.get('https://en.wikipedia.org/w/api.php', { + params: { + action: 'query', + titles: pageTitle, + prop: 'pageimages', + pithumbsize: 500, + format: 'json', + origin: '*' + } + }); + + const pages = pageResponse.data.query.pages; + const pageId = Object.keys(pages)[0]; + const page = pages[pageId]; + + if (page.thumbnail && page.thumbnail.source) { + return page.thumbnail.source; + } else { + console.error('No image found for the unit name.'); + return null; + } + } catch (error) { + console.error('Error fetching data from Wikipedia:', error); + return null; + } +} + +export async function getWikipediaSummary(unitName: string): Promise { + try { + // Search for the unit name on Wikipedia + const searchResponse = await axios.get('https://en.wikipedia.org/w/api.php', { + params: { + action: 'query', + list: 'search', + srsearch: unitName, + format: 'json', + origin: '*' + } + }); + + if (searchResponse.data.query.search.length === 0) { + console.error('No search results found for the unit name.'); + return null; + } + + // Get the title of the first search result + const pageTitle = searchResponse.data.query.search[0].title; + + // Get the page content to find the summary + const pageResponse = await axios.get('https://en.wikipedia.org/w/api.php', { + params: { + action: 'query', + prop: 'extracts', + exintro: true, + explaintext: true, + titles: pageTitle, + format: 'json', + origin: '*' + } + }); + + const pages = pageResponse.data.query.pages; + const pageId = Object.keys(pages)[0]; + const page = pages[pageId]; + + if (page.extract) { + return page.extract; + } else { + console.error('No summary found for the unit name.'); + return null; + } + } catch (error) { + console.error('Error fetching data from Wikipedia:', error); + return null; + } } \ No newline at end of file diff --git a/frontend/react/src/ui/components/olbuttongroup.tsx b/frontend/react/src/ui/components/olbuttongroup.tsx index 4be547c8..5d5bf819 100644 --- a/frontend/react/src/ui/components/olbuttongroup.tsx +++ b/frontend/react/src/ui/components/olbuttongroup.tsx @@ -1,9 +1,50 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React from "react"; +import React, { useRef, useState } from "react"; +import { OlTooltip } from "./oltooltip"; -export function OlButtonGroup(props: { children?: JSX.Element | JSX.Element[] }) { - return
{props.children}
; +export function OlButtonGroup(props: { + tooltip?: string | (() => JSX.Element | JSX.Element[]); + tooltipPosition?: string; + tooltipRelativeToParent?: boolean; + children?: JSX.Element | JSX.Element[]; +}) { + const [hover, setHover] = useState(false); + const [hoverTimeout, setHoverTimeout] = useState(null as number | null); + var buttonRef = useRef(null); + return ( + <> +
{ + setHoverTimeout( + window.setTimeout(() => { + setHover(true); + setHoverTimeout(null); + }, 400) + ); + }} + onMouseLeave={() => { + setHover(false); + if (hoverTimeout) { + window.clearTimeout(hoverTimeout); + setHoverTimeout(null); + } + }} + > + {props.children} +
+ {hover && props.tooltip && ( + + )} + + ); } export function OlButtonGroupItem(props: { icon: IconProp; active: boolean; onClick: () => void }) { diff --git a/frontend/react/src/ui/components/olcoalitiontoggle.tsx b/frontend/react/src/ui/components/olcoalitiontoggle.tsx index 5ff0b4c8..4baa88c1 100644 --- a/frontend/react/src/ui/components/olcoalitiontoggle.tsx +++ b/frontend/react/src/ui/components/olcoalitiontoggle.tsx @@ -1,14 +1,52 @@ -import React from "react"; +import React, { useRef, useState } from "react"; import { Coalition } from "../../types/types"; +import { OlTooltip } from "./oltooltip"; + +export function OlCoalitionToggle(props: { + coalition: Coalition | undefined; + onClick: () => void; + tooltip?: string | (() => JSX.Element | JSX.Element[]); + tooltipPosition?: string; + tooltipRelativeToParent?: boolean; + className?: string; + showLabel?: boolean; +}) { + const [hover, setHover] = useState(false); + const [hoverTimeout, setHoverTimeout] = useState(null as number | null); + var buttonRef = useRef(null); -export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onClick: () => void; showLabel?: boolean }) { return ( -
+ <> +
{ + e.stopPropagation(); + props.onClick(); + props.onClick ?? setHover(false); + }} + onMouseEnter={() => { + setHoverTimeout( + window.setTimeout(() => { + setHover(true); + setHoverTimeout(null); + }, 400) + ); + }} + onMouseLeave={() => { + setHover(false); + if (hoverTimeout) { + window.clearTimeout(hoverTimeout); + setHoverTimeout(null); + } + }} + ref={buttonRef} + > - )} + {props.leftIcon && ( + + )} + {props.label ?? ""} + + + + + )} -
{ - props.disableAutoClose !== true && setOpen(false); - }} > - {props.children} +
{ + props.disableAutoClose !== true && setOpen(false); + }} + > + {props.children} +
-
+ {hover && !open && buttonRef && props.tooltip && ( + + )} + ); } diff --git a/frontend/react/src/ui/components/olexpandingtooltip.tsx b/frontend/react/src/ui/components/olexpandingtooltip.tsx new file mode 100644 index 00000000..02c8c239 --- /dev/null +++ b/frontend/react/src/ui/components/olexpandingtooltip.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { FaQuestionCircle } from "react-icons/fa"; + +export function OlExpandingTooltip(props: { title: string; content: string | JSX.Element | JSX.Element[] }) { + return ( +
+
+
+
+
+ +
+ +
+
{props.title}
+
+
+ {props.content} +
+
+ ); +} diff --git a/frontend/react/src/ui/components/ollabeltoggle.tsx b/frontend/react/src/ui/components/ollabeltoggle.tsx index 7b6c3e87..b983eae9 100644 --- a/frontend/react/src/ui/components/ollabeltoggle.tsx +++ b/frontend/react/src/ui/components/ollabeltoggle.tsx @@ -1,51 +1,91 @@ -import React from "react"; +import React, { useRef, useState } from "react"; +import { OlTooltip } from "./oltooltip"; + +export function OlLabelToggle(props: { + toggled: boolean | undefined; + leftLabel: string; + rightLabel: string; + tooltip?: string | (() => JSX.Element | JSX.Element[]); + tooltipPosition?: string; + tooltipRelativeToParent?: boolean; + onClick: () => void; +}) { + const [hover, setHover] = useState(false); + const [hoverTimeout, setHoverTimeout] = useState(null as number | null); + var buttonRef = useRef(null); -export function OlLabelToggle(props: { toggled: boolean | undefined; leftLabel: string; rightLabel: string; onClick: () => void }) { return ( - + + + {props.leftLabel} + + + {props.rightLabel} + + + {hover && props.tooltip && ( + + )} + ); } diff --git a/frontend/react/src/ui/components/ollocation.tsx b/frontend/react/src/ui/components/ollocation.tsx index ee0e5adc..82ffb6e2 100644 --- a/frontend/react/src/ui/components/ollocation.tsx +++ b/frontend/react/src/ui/components/ollocation.tsx @@ -42,7 +42,7 @@ export function OlLocation(props: { location: LatLng; className?: string; refere > {props.location.lat >= 0 ? "N" : "S"} - {zeroAppend(props.location.lat, 3, true, 6)} + {zeroAppend(props.location.lat, 3, true, 6)}°
{props.location.lng >= 0 ? "E" : "W"} - {zeroAppend(props.location.lng, 3, true, 6)} + {zeroAppend(props.location.lng, 3, true, 6)}°
); diff --git a/frontend/react/src/ui/components/olnumberinput.tsx b/frontend/react/src/ui/components/olnumberinput.tsx index a7e53395..ce2f352a 100644 --- a/frontend/react/src/ui/components/olnumberinput.tsx +++ b/frontend/react/src/ui/components/olnumberinput.tsx @@ -1,5 +1,6 @@ -import React, { ChangeEvent } from "react"; +import React, { ChangeEvent, useRef, useState } from "react"; import { zeroAppend } from "../../other/utils"; +import { OlTooltip } from "./oltooltip"; export function OlNumberInput(props: { value: number; @@ -7,10 +8,17 @@ export function OlNumberInput(props: { max: number; minLength?: number; className?: string; + tooltip?: string | (() => JSX.Element | JSX.Element[]); + tooltipPosition?: string; + tooltipRelativeToParent?: boolean; onDecrease: () => void; onIncrease: () => void; onChange: (e: ChangeEvent) => void; }) { + const [hover, setHover] = useState(false); + const [hoverTimeout, setHoverTimeout] = useState(null as number | null); + var buttonRef = useRef(null); + return (
-
+
{ + setHoverTimeout( + window.setTimeout(() => { + setHover(true); + setHoverTimeout(null); + }, 400) + ); + }} + onMouseLeave={() => { + setHover(false); + if (hoverTimeout) { + window.clearTimeout(hoverTimeout); + setHoverTimeout(null); + } + }} + >
+ {hover && props.tooltip && ( + + )}
); } diff --git a/frontend/react/src/ui/components/olstatebutton.tsx b/frontend/react/src/ui/components/olstatebutton.tsx index 3f81b24a..9650fa16 100644 --- a/frontend/react/src/ui/components/olstatebutton.tsx +++ b/frontend/react/src/ui/components/olstatebutton.tsx @@ -13,12 +13,14 @@ export function OlStateButton(props: { icon?: IconProp; tooltip?: string | (() => JSX.Element | JSX.Element[]); tooltipPosition?: string; + tooltipRelativeToParent?: boolean; onClick: () => void; onMouseUp?: () => void; onMouseDown?: () => void; children?: JSX.Element | JSX.Element[]; }) { const [hover, setHover] = useState(false); + const [hoverTimeout, setHoverTimeout] = useState(null as number | null); var buttonRef = useRef(null); const className = @@ -45,30 +47,63 @@ export function OlStateButton(props: { className={className} style={{ border: props.buttonColor ? "2px solid " + props.buttonColor : "0px solid transparent", - background: setOpacity(props.checked || hover ? (props.buttonColor ? props.buttonColor : colors.OLYMPUS_LIGHT_BLUE) : colors.OLYMPUS_BLUE, (!props.checked && hover)? 0.3: 1), + background: setOpacity( + props.checked || hover ? (props.buttonColor ? props.buttonColor : colors.OLYMPUS_LIGHT_BLUE) : colors.OLYMPUS_BLUE, + !props.checked && hover ? 0.3 : 1 + ), }} onMouseEnter={() => { - setHover(true); + setHoverTimeout( + window.setTimeout(() => { + setHover(true); + setHoverTimeout(null); + }, 400) + ); }} onMouseLeave={() => { setHover(false); + if (hoverTimeout) { + window.clearTimeout(hoverTimeout); + setHoverTimeout(null); + } }} >
- {props.icon && 200} className={` - m-auto text-gray-200 - data-[bright='true']:text-gray-800 - `} />} + {props.icon && ( + 200} + className={` + m-auto text-gray-200 + data-[bright='true']:text-gray-800 + `} + /> + )} {props.children}
- {hover && props.tooltip && } + {hover && props.tooltip && ( + + )} ); } -export function OlRoundStateButton(props: { className?: string; checked: boolean; icon: IconProp; tooltip: string; onClick: () => void }) { +export function OlRoundStateButton(props: { + className?: string; + checked: boolean; + icon: IconProp; + tooltip?: string | (() => JSX.Element | JSX.Element[]); + tooltipPosition?: string; + onClick: () => void; +}) { const [hover, setHover] = useState(false); + const [hoverTimeout, setHoverTimeout] = useState(null as number | null); var buttonRef = useRef(null); const className = @@ -96,21 +131,39 @@ export function OlRoundStateButton(props: { className?: string; checked: boolean type="button" className={className} onMouseEnter={() => { - setHover(true); + setHoverTimeout( + window.setTimeout(() => { + setHover(true); + setHoverTimeout(null); + }, 400) + ); }} onMouseLeave={() => { setHover(false); + if (hoverTimeout) { + window.clearTimeout(hoverTimeout); + setHoverTimeout(null); + } }} > - {hover && } + {hover && props.tooltip && ( + + )} ); } -export function OlLockStateButton(props: { className?: string; checked: boolean; tooltip: string; onClick: () => void }) { +export function OlLockStateButton(props: { + className?: string; + checked: boolean; + tooltip?: string | (() => JSX.Element | JSX.Element[]); + tooltipPosition?: string; + onClick: () => void; +}) { const [hover, setHover] = useState(false); + const [hoverTimeout, setHoverTimeout] = useState(null as number | null); var buttonRef = useRef(null); const className = @@ -135,15 +188,26 @@ export function OlLockStateButton(props: { className?: string; checked: boolean; type="button" className={className} onMouseEnter={() => { - setHover(true); + setHoverTimeout( + window.setTimeout(() => { + setHover(true); + setHoverTimeout(null); + }, 400) + ); }} onMouseLeave={() => { setHover(false); + if (hoverTimeout) { + window.clearTimeout(hoverTimeout); + setHoverTimeout(null); + } }} > - {hover && } + {hover && props.tooltip && ( + + )} ); } diff --git a/frontend/react/src/ui/components/oltoggle.tsx b/frontend/react/src/ui/components/oltoggle.tsx index 536eed1a..0500a8c3 100644 --- a/frontend/react/src/ui/components/oltoggle.tsx +++ b/frontend/react/src/ui/components/oltoggle.tsx @@ -1,33 +1,71 @@ -import React from "react"; +import React, { useRef, useState } from "react"; +import { OlTooltip } from "./oltooltip"; + +export function OlToggle(props: { + toggled: boolean | undefined; + tooltip?: string | (() => JSX.Element | JSX.Element[]); + tooltipPosition?: string; + tooltipRelativeToParent?: boolean; + onClick: () => void; +}) { + const [hover, setHover] = useState(false); + const [hoverTimeout, setHoverTimeout] = useState(null as number | null); + var buttonRef = useRef(null); -export function OlToggle(props: { toggled: boolean | undefined; onClick: () => void }) { return ( -
{ - e.stopPropagation(); - props.onClick(); - }} - > -
+ className="inline-flex cursor-pointer items-center" + ref={buttonRef} + onClick={(e) => { + e.stopPropagation(); + props.onClick(); + setHover(false); + }} + onMouseEnter={() => { + setHoverTimeout( + window.setTimeout(() => { + setHover(true); + setHoverTimeout(null); + }, 400) + ); + }} + onMouseLeave={() => { + setHover(false); + if (hoverTimeout) { + window.clearTimeout(hoverTimeout); + setHoverTimeout(null); + } + }} + > +
+ {hover && props.tooltip && ( + + )} + ); } diff --git a/frontend/react/src/ui/components/oltooltip.tsx b/frontend/react/src/ui/components/oltooltip.tsx index bbe26976..0e9df1c6 100644 --- a/frontend/react/src/ui/components/oltooltip.tsx +++ b/frontend/react/src/ui/components/oltooltip.tsx @@ -1,6 +1,11 @@ import React, { useEffect, useRef, useState } from "react"; -export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[]; buttonRef: React.MutableRefObject; position?: string }) { +export function OlTooltip(props: { + content: string | JSX.Element | JSX.Element[]; + buttonRef: React.MutableRefObject; + position?: string; + relativeToParent?: boolean; +}) { var contentRef = useRef(null); function setPosition(content: HTMLDivElement, button: HTMLButtonElement) { @@ -9,8 +14,16 @@ export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[] content.style.top = "0px"; content.style.height = ""; + button.style.position = "relative"; + button.style.left = "0px"; + button.style.top = "0px"; + + + let parent = button.closest(".ol-panel-container") as HTMLElement; + if (parent === null) parent = document.body; + /* Get the position and size of the button and the content elements */ - let [cxl, cyt, cxr, cyb, cw, ch] = [ + let [contentXLeft, contentYTop, contentXRight, contentYBottom, contentWidth, contentHeight] = [ content.getBoundingClientRect().x, content.getBoundingClientRect().y, content.getBoundingClientRect().x + content.offsetWidth, @@ -18,7 +31,7 @@ export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[] content.offsetWidth, content.offsetHeight, ]; - let [bxl, byt, bxr, byb, bbw, bh] = [ + let [buttonXLeft, buttonYTop, buttonXRight, buttonYBottom, buttonWidth, buttonHeight] = [ button.getBoundingClientRect().x, button.getBoundingClientRect().y, button.getBoundingClientRect().x + button.offsetWidth, @@ -28,38 +41,49 @@ export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[] ]; /* Limit the maximum height */ - if (ch > 400) { - ch = 400; - content.style.height = `${ch}px`; + if (contentHeight > 400) { + contentHeight = 400; + content.style.height = `${contentHeight}px`; } /* Compute the horizontal position of the center of the button and the content */ - var cxc = (cxl + cxr) / 2; - var bxc = (bxl + bxr) / 2; + var contentXCenter = (contentXLeft + contentXRight) / 2; + var buttonXCenter = (buttonXLeft + buttonXRight) / 2; /* Compute the x and y offsets needed to align the button and element horizontally, and to put the content depending on the requested position */ var offsetX = 0; var offsetY = 0; if (props.position === undefined || props.position === "below") { - offsetX = bxc - cxc; - offsetY = byb - cyt + 8; - } else if (props.position === "side") { - offsetX = bxr + 8; - offsetY = byt - cyt + (bh - ch) / 2; + offsetX = buttonXCenter - contentXCenter; + offsetY = buttonYBottom - contentYTop + 8; + } else if (props.position === "above") { + offsetX = buttonXCenter - contentXCenter; + offsetY = buttonYTop - contentYTop - contentHeight - 8; } + else if (props.position === "side") { + offsetX = buttonXRight + 8; + offsetY = buttonYTop - contentYTop + (buttonHeight - contentHeight) / 2; + } + + content.style.left = `${offsetX}px`; + content.style.top = `${offsetY}px`; /* Compute the new position of the left and right margins of the content */ - let ncxl = cxl + offsetX; - let ncxr = cxr + offsetX; - let ncyb = cyb + offsetY; + let newContentXLeft = props.relativeToParent ? offsetX : contentXLeft + offsetX; + let newContentXRight = newContentXLeft + contentWidth; + let newContentYBottom = props.relativeToParent ? offsetY : contentYBottom + offsetY; /* Try and move the content so it is inside the screen */ - if (ncxl < 0) offsetX -= cxl; - if (ncxr > window.innerWidth) { - offsetX = bxl - cxl - cw - 12; + if (newContentXLeft < 0) offsetX = 15; + if (newContentXRight > (props.relativeToParent ? parent.clientWidth : window.innerWidth)) { + if (props.position === "side") { + offsetX = buttonXLeft - contentXLeft - contentWidth - 12; + } else { + offsetX -= newContentXRight - (props.relativeToParent ? parent.clientWidth : window.innerWidth) + 15; + } } - if (ncyb > window.innerHeight) offsetY -= bh + ch + 16; + if (newContentYBottom > (props.relativeToParent ? parent.clientHeight : window.innerHeight)) offsetY -= buttonHeight + contentHeight + 16; /* Apply the offset */ content.style.left = `${offsetX}px`; @@ -77,7 +101,7 @@ export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[] setPosition(content, button); }); resizeObserver.observe(content); - return () => resizeObserver.disconnect(); // clean up + return () => resizeObserver.disconnect(); // clean up } }); @@ -86,9 +110,9 @@ export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[]
{props.content} diff --git a/frontend/react/src/ui/components/olunitsummary.tsx b/frontend/react/src/ui/components/olunitsummary.tsx index 698739a1..e08649c2 100644 --- a/frontend/react/src/ui/components/olunitsummary.tsx +++ b/frontend/react/src/ui/components/olunitsummary.tsx @@ -1,8 +1,23 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { UnitBlueprint } from "../../interfaces"; import { Coalition } from "../../types/types"; +import { getWikipediaImage, getWikipediaSummary } from "../../other/utils"; +import { OlStateButton } from "./olstatebutton"; +import { faImage } from "@fortawesome/free-solid-svg-icons"; +import { FaQuestionCircle } from "react-icons/fa"; export function OlUnitSummary(props: { blueprint: UnitBlueprint; coalition: Coalition }) { + const [imageUrl, setImageUrl] = useState(null as string | null); + const [summary, setSummary] = useState(null as string | null); + const [hover, setHover] = useState(false); + const [hoverTimeout, setHoverTimeout] = useState(null as number | null); + + useEffect(() => { + getWikipediaImage(props.blueprint.label).then((url) => { + setImageUrl(url); + }); + }, [props.blueprint]); + return (
{ + setHoverTimeout( + window.setTimeout(() => { + setHover(true); + setHoverTimeout(null); + }, 400) + ); + }} + onMouseLeave={() => { + setHover(false); + if (hoverTimeout) { + window.clearTimeout(hoverTimeout); + setHoverTimeout(null); + } + }} > -
- + {imageUrl && hover && } +
{props.blueprint.label}
+ {imageUrl && ( +
+ +
Hover to show image
+
+ )}
-
+

- {props.blueprint.description} + {summary ?? props.blueprint.description}

diff --git a/frontend/react/src/ui/modals/components/modal.tsx b/frontend/react/src/ui/modals/components/modal.tsx index 1592944a..b8734b8f 100644 --- a/frontend/react/src/ui/modals/components/modal.tsx +++ b/frontend/react/src/ui/modals/components/modal.tsx @@ -1,7 +1,12 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { ModalEvent } from "../../../events"; +import { FaXmark } from "react-icons/fa6"; +import { getApp, OlympusApp } from "../../../olympusapp"; +import { OlympusState } from "../../../constants/constants"; export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Element[]; className?: string }) { + const [splash, setSplash] = useState(Math.ceil(Math.random() * 7)); + useEffect(() => { ModalEvent.dispatch(props.open); }, [props.open]); @@ -13,13 +18,52 @@ export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Eleme
- {props.children} + +
+
+
+
+ {props.children} +
+ { + getApp().setState(OlympusState.IDLE); + }} + />{" "} +
+
+
)} diff --git a/frontend/react/src/ui/modals/importexportmodal.tsx b/frontend/react/src/ui/modals/importexportmodal.tsx index 9bd55183..34301d29 100644 --- a/frontend/react/src/ui/modals/importexportmodal.tsx +++ b/frontend/react/src/ui/modals/importexportmodal.tsx @@ -45,7 +45,7 @@ export function ImportExportModal(props: { open: boolean }) { "groundunit-sam": true, groundunit: true, navyunit: true, - } + }, }; } @@ -95,274 +95,263 @@ export function ImportExportModal(props: { open: boolean }) { }); return ( - -
-
- +
+ + {appSubState === ImportExportSubstate.EXPORT ? "Export to file" : "Import from file"} + + + + {appSubState === ImportExportSubstate.EXPORT ? <>Select what units you want to export to file using the toggles below : <>} + + +
+
- {appSubState === ImportExportSubstate.EXPORT ? "Export to file" : "Import from file"} - + Control mode +
- - {appSubState === ImportExportSubstate.EXPORT ? <>Select what units you want to export to file using the toggles below : <>} - +
+ {Object.entries({ + olympus: ["Olympus controlled", olButtonsVisibilityOlympus], + dcs: ["From DCS mission", olButtonsVisibilityDcs], + }).map((entry, idx) => { + return ( +
+ {entry[1][0] as string} + { + selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]]; + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + toggled={selectionFilter["control"][entry[0]]} + /> +
+ ); + })} +
-
-
- Control mode -
+
+ Types and coalitions +
-
+ + + + + + + + {Object.entries({ - olympus: ["Olympus controlled", olButtonsVisibilityOlympus], - dcs: ["From DCS mission", olButtonsVisibilityDcs], + "groundunit-sam": olButtonsVisibilityGroundunitSam, + groundunit: olButtonsVisibilityGroundunit, + navyunit: olButtonsVisibilityNavyunit, }).map((entry, idx) => { return ( -
- {entry[1][0] as string} - { - selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]]; - setSelectionFilter(deepCopyTable(selectionFilter)); - }} - toggled={selectionFilter["control"][entry[0]]} - /> -
+ + + {["blue", "neutral", "red"].map((coalition) => { + return ( + + ); + })} + ); })} - - -
- Types and coalitions -
- -
BLUENEUTRALRED
+ + + unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition) !== undefined) || + (appSubState === ImportExportSubstate.IMPORT && + selectionFilter[coalition][entry[0]] && + Object.values(importData).find((group) => + group.find((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition) + ) !== undefined) + } + disabled={ + (appSubState === ImportExportSubstate.EXPORT && + selectableUnits.find((unit) => unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition) === undefined) || + (appSubState === ImportExportSubstate.IMPORT && + Object.values(importData).find((group) => + group.find((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition) + ) === undefined) + } + onChange={() => { + selectionFilter[coalition][entry[0]] = !selectionFilter[coalition][entry[0]]; + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + /> + + {appSubState === ImportExportSubstate.EXPORT && + selectableUnits.filter((unit) => unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition).length} + {appSubState === ImportExportSubstate.IMPORT && + Object.values(importData) + .flatMap((unit) => unit) + .filter((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition).length}{" "} + units{" "} + +
- + { - - - - + + + + - {Object.entries({ - "groundunit-sam": olButtonsVisibilityGroundunitSam, - groundunit: olButtonsVisibilityGroundunit, - navyunit: olButtonsVisibilityNavyunit, - }).map((entry, idx) => { - return ( - - - {["blue", "neutral", "red"].map((coalition) => { - return ( - - ); - })} - - ); - })} - { - - - - - - - } - -
BLUENEUTRALRED + value)} + onChange={() => { + const newValue = !Object.values(selectionFilter["blue"]).some((value) => value); + Object.keys(selectionFilter["blue"]).forEach((key) => { + selectionFilter["blue"][key] = newValue; + }); + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + /> + + value)} + onChange={() => { + const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value); + Object.keys(selectionFilter["neutral"]).forEach((key) => { + selectionFilter["neutral"][key] = newValue; + }); + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + /> + + value)} + onChange={() => { + const newValue = !Object.values(selectionFilter["red"]).some((value) => value); + Object.keys(selectionFilter["red"]).forEach((key) => { + selectionFilter["red"][key] = newValue; + }); + setSelectionFilter(deepCopyTable(selectionFilter)); + }} + /> +
- - - unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition) !== undefined) || - (appSubState === ImportExportSubstate.IMPORT && - selectionFilter[coalition][entry[0]] && - Object.values(importData).find((group) => - group.find((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition) - ) !== undefined) - } - disabled={ - (appSubState === ImportExportSubstate.EXPORT && - selectableUnits.find((unit) => unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition) === undefined) || - (appSubState === ImportExportSubstate.IMPORT && - Object.values(importData).find((group) => - group.find((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition) - ) === undefined) - } - onChange={() => { - selectionFilter[coalition][entry[0]] = !selectionFilter[coalition][entry[0]]; - setSelectionFilter(deepCopyTable(selectionFilter)); - }} - /> - - {appSubState === ImportExportSubstate.EXPORT && - selectableUnits.filter((unit) => unit.getMarkerCategory() === entry[0] && unit.getCoalition() === coalition).length} - {appSubState === ImportExportSubstate.IMPORT && - Object.values(importData) - .flatMap((unit) => unit) - .filter((unit) => unit.markerCategory === entry[0] && unit.coalition === coalition).length}{" "} - units{" "} - -
- value)} - onChange={() => { - const newValue = !Object.values(selectionFilter["blue"]).some((value) => value); - Object.keys(selectionFilter["blue"]).forEach((key) => { - selectionFilter["blue"][key] = newValue; - }); - setSelectionFilter(deepCopyTable(selectionFilter)); - }} - /> - - value)} - onChange={() => { - const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value); - Object.keys(selectionFilter["neutral"]).forEach((key) => { - selectionFilter["neutral"][key] = newValue; - }); - setSelectionFilter(deepCopyTable(selectionFilter)); - }} - /> - - value)} - onChange={() => { - const newValue = !Object.values(selectionFilter["red"]).some((value) => value); - Object.keys(selectionFilter["red"]).forEach((key) => { - selectionFilter["red"][key] = newValue; - }); - setSelectionFilter(deepCopyTable(selectionFilter)); - }} - /> -
-
+ } + +
+
-
- + } else { + for (const [groupName, groupData] of Object.entries(importData)) { + if (groupName === "" || groupData.length === 0 || !groupData.some((unitData: UnitData) => unitData.alive)) continue; - -
+ let { markerCategory, coalition, category } = groupData[0]; + + if (selectionFilter[coalition][markerCategory] !== true) continue; + + let unitsToSpawn = groupData.map((unitData: UnitData) => { + return { unitType: unitData.name, location: unitData.position, liveryID: "", skill: "High" }; + }); + + getApp().getUnitsManager().spawnUnits(category.toLocaleLowerCase(), unitsToSpawn, coalition, false); + } + getApp().setState(OlympusState.IDLE); + } + }} + className={` + mb-2 me-2 ml-auto flex content-center items-center gap-2 rounded-sm + bg-blue-700 px-5 py-2.5 text-sm font-medium text-white + dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 + focus:outline-none focus:ring-4 focus:ring-blue-300 + hover:bg-blue-800 + `} + > + Continue + + + +
); diff --git a/frontend/react/src/ui/modals/keybindmodal.tsx b/frontend/react/src/ui/modals/keybindmodal.tsx index 47a22852..f39da88b 100644 --- a/frontend/react/src/ui/modals/keybindmodal.tsx +++ b/frontend/react/src/ui/modals/keybindmodal.tsx @@ -61,99 +61,91 @@ export function KeybindModal(props: { open: boolean }) { } return ( - -
-
- - {shortcut?.getOptions().label} - - - Press the key you want to bind to this event - -
-
- {ctrlKey && "Ctrl + "} - {altKey && "Alt + "} - {shiftKey && "Shift + "} - {code} -
-
- {available === true &&
Keybind is free!
} - {available === false && ( -
-
- Keybind is already in use:
{inUseShortcuts.map((shortcut) => {shortcut.getOptions().label})}
+ +
+ + {shortcut?.getOptions().label} + + + Press the key you want to bind to this event + +
+
+ {ctrlKey && "Ctrl + "} + {altKey && "Alt + "} + {shiftKey && "Shift + "} + {code} +
+
+ {available === true &&
Keybind is free!
} + {available === false && ( +
+
+ Keybind is already in use:{" "} +
+ {inUseShortcuts.map((shortcut) => ( + {shortcut.getOptions().label} + ))}
-
A key combination can be assigned to multiple actions, and all bound actions will fire
- )} -
- -
- {shortcut && ( - - )} +
A key combination can be assigned to multiple actions, and all bound actions will fire
+
+ )} +
+
+ {shortcut && ( -
+ )} +
); diff --git a/frontend/react/src/ui/modals/loginmodal.tsx b/frontend/react/src/ui/modals/loginmodal.tsx index 6dd3546e..df5afb0f 100644 --- a/frontend/react/src/ui/modals/loginmodal.tsx +++ b/frontend/react/src/ui/modals/loginmodal.tsx @@ -80,346 +80,301 @@ export function LoginModal(props: { open: boolean }) { useEffect(subStateCallback, [subState]); return ( - - +
-
-
-
- {!checkingPassword ? ( - <> -
-
- Connect to -
-
- {window.location.toString()} -
+ {!checkingPassword ? ( + <> +
+
+ Connect to
- - - -
-

- DCS Olympus -

-
- - Version {VERSION} -
+ {window.location.toString()} +
+
+
+ + + +
+

+ DCS Olympus +

+
+ + Version {VERSION}
- {!loginError ? ( - <> - {subState === LoginSubState.CREDENTIALS && ( - <> -
- - setUsername(ev.currentTarget.value)} - className={` - block w-full max-w-80 rounded-lg border - border-gray-300 bg-gray-50 p-2.5 text-sm - text-gray-900 - dark:border-gray-600 dark:bg-gray-700 - dark:text-white dark:placeholder-gray-400 - dark:focus:border-blue-500 - dark:focus:ring-blue-500 - focus:border-blue-500 focus:ring-blue-500 - `} - placeholder="Enter display name" - value={username} - required - /> +
+ {!loginError ? ( + <> + {subState === LoginSubState.CREDENTIALS && ( + <> +
+ + setUsername(ev.currentTarget.value)} + className={` + block w-full max-w-80 rounded-lg border + border-gray-300 bg-gray-50 p-2.5 text-sm + text-gray-900 + dark:border-gray-600 dark:bg-gray-700 + dark:text-white dark:placeholder-gray-400 + dark:focus:border-blue-500 dark:focus:ring-blue-500 + focus:border-blue-500 focus:ring-blue-500 + `} + placeholder="Enter display name" + value={username} + required + /> - - setPassword(ev.currentTarget.value)} - className={` - block w-full max-w-80 rounded-lg border - border-gray-300 bg-gray-50 p-2.5 text-sm - text-gray-900 - dark:border-gray-600 dark:bg-gray-700 - dark:text-white dark:placeholder-gray-400 - dark:focus:border-blue-500 - dark:focus:ring-blue-500 - focus:border-blue-500 focus:ring-blue-500 - `} - placeholder="Enter password" - required - /> -
-
- - {/* + + setPassword(ev.currentTarget.value)} + className={` + block w-full max-w-80 rounded-lg border + border-gray-300 bg-gray-50 p-2.5 text-sm + text-gray-900 + dark:border-gray-600 dark:bg-gray-700 + dark:text-white dark:placeholder-gray-400 + dark:focus:border-blue-500 dark:focus:ring-blue-500 + focus:border-blue-500 focus:ring-blue-500 + `} + placeholder="Enter password" + required + /> +
+
+ + {/* */} -
- - )} - {subState === LoginSubState.COMMAND_MODE && ( - <> -
- - - {commandModes?.map((commandMode) => { - return setActiveCommandMode(commandMode)}>{commandMode}; - })} - -
-
- -
- - )} - - ) : ( - <> - -
- Still having issues? See our{" "} - - troubleshooting guide here - - . -
- - )} - - ) : ( -
- -
- )} -
-
- - + + )} + {subState === LoginSubState.COMMAND_MODE && ( + <> +
+ + + {commandModes?.map((commandMode) => { + return setActiveCommandMode(commandMode)}>{commandMode}; + })} + +
+
+ +
+ + )} + + ) : ( + <> + +
+ Still having issues? See our{" "} + + troubleshooting guide here + + . +
+ + )} + + ) : ( +
+
- YouTube Video Guide - -
-
- Check out our official video tutorial on how to get started with Olympus - so you can immediately start controlling the battlefield. -
- - - -
- Wiki Guide - -
-
- Find out more about Olympus through our online wiki guide. -
-
-
+ + + +
+ )}
- DCS Olympus (the "MATERIAL" or "Software") is provided completely free to users subject to the terms of the CC BY-NC-SA 4.0 Licence except where such - terms conflict with this disclaimer, in which case, the terms of this disclaimer shall prevail. Any party making use of the Software in any manner - agrees to be bound by the terms set out in the disclaimer. THIS MATERIAL IS NOT MADE OR SUPPORTED BY EAGLE DYNAMICS SA. + + +
+ YouTube Video Guide + +
+
+ Check out our official video tutorial on how to get started with Olympus - so you can immediately start controlling the battlefield. +
+
+ + +
+ Wiki Guide + +
+
+ Find out more about Olympus through our online wiki guide. +
+
+
+ DCS Olympus (the "MATERIAL" or "Software") is provided completely free to users subject to the terms of the CC BY-NC-SA 4.0 Licence except where such + terms conflict with this disclaimer, in which case, the terms of this disclaimer shall prevail. Any party making use of the Software in any manner + agrees to be bound by the terms set out in the disclaimer. THIS MATERIAL IS NOT MADE OR SUPPORTED BY EAGLE DYNAMICS SA. +
); } diff --git a/frontend/react/src/ui/modals/protectionpromptmodal.tsx b/frontend/react/src/ui/modals/protectionpromptmodal.tsx index d8d0d230..6017c8e8 100644 --- a/frontend/react/src/ui/modals/protectionpromptmodal.tsx +++ b/frontend/react/src/ui/modals/protectionpromptmodal.tsx @@ -10,14 +10,8 @@ export function ProtectionPromptModal(props: { open: boolean }) { return ( -
+
Pressing "Continue" will cause all DCS controlled units in the current selection to abort their mission and start following Olympus commands only. @@ -38,7 +32,7 @@ export function ProtectionPromptModal(props: { open: boolean }) { If you are trying to delete a human player unit, they will be killed and de-slotted. Be careful! @@ -46,7 +40,7 @@ export function ProtectionPromptModal(props: { open: boolean }) { To disable this warning, press on the{" "} diff --git a/frontend/react/src/ui/modals/trainingmodal.tsx b/frontend/react/src/ui/modals/trainingmodal.tsx new file mode 100644 index 00000000..ef55de5f --- /dev/null +++ b/frontend/react/src/ui/modals/trainingmodal.tsx @@ -0,0 +1,526 @@ +import React, { useState } from "react"; +import { Modal } from "./components/modal"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowLeft, faArrowRight } from "@fortawesome/free-solid-svg-icons"; +import { FaCaretRight, FaLink } from "react-icons/fa6"; +import { FaQuestionCircle } from "react-icons/fa"; + +const MAX_STEPS = 10; + +export function TrainingModal(props: { open: boolean }) { + const [step, setStep] = useState(0); + + return ( + +
+

DCS Olympus guided tour

+ +
+ + <> + {step === 0 && ( +
+ +
+

Home

+

+ Welcome to the Olympus quick start guide! This tour will guide you through the basics of DCS Olympus. You can navigate through the steps using + the "Next" and "Previous" buttons at the bottom of the screen, or select a topic from the list below. +

+
+ + + + +
+ Every panel has a dedicated integrated wiki. Click on the{" "} + + + {" "} + symbol to access it. Moreover, most clickable content has tooltips providing info about their function. +
+
+
+
+ )} + + + <> + {step === 1 && ( +
+ +
+

Main navbar

+

+ The main functions of DCS Olympus are accessible from the main navbar. You can access the spawn tool, the unit selection and control tool, the + drawings tool, the audio/radio tool, and the game master options from here. +

+

On the bottom left corner, you can find the DCS Olympus options tool.

+
+
+ )} + + + <> + {step === 2 && ( +
+ +
+

Spawning units (1 of 3)

+

+ To spawn a unit, click on the spawn tool icon on the main navbar. This will open the spawn tool. You can select the unit you want to spawn by + searching for it or by finding it in the list, which can be filtered by category. +

+

After selecting the unit you can edit its properties, like spawn altitude and heading, loadout, livery, skill level, and so on.

+

+ Once you are happy with your setup, click on the map to spawn the unit. You can click multiple times to spawn more units. When you are done, + double click to exit the spawning mode. +

+
+
+ )} + + + <> + {step === 3 && ( +
+ +
+

Spawning units (2 of 3)

+

+ You can also spawn units directly on the map by right clicking on it and selecting the unit you want to spawn. This will spawn the unit at the + clicked location. +

+

You can edit the unit properties like in the previous method.

+
+
+ )} + + + <> + {step === 4 && ( +
+ +
+

Spawning units (3 of 3)

+

+ If you plan on spawning many similar units throughout the mission, you can "star" a unit. This will save the unit in the starred units list, + which can be accessed from the spawn tool. This way you can quickly spawn the same unit multiple times without having to search for it. +

+

You can edit the unit properties like in the previous method.

+
+
+ )} + + + <> + {step === 5 && ( +
+ +
+

Controlling units (1 of 4)

+

+ {" "} + The most basic form of unit control is movement. A short right click on the map will add a destination point. If the ctrl key is held while + right clicking, the destination will be appended, creating a path. +

+

+ Previously created destinations can be moved by dragging the marker on the map. If multiple units are selected when creating the path, + destinations will be shared between them. +

+
+
+ )} + + + <> + {step === 6 && ( +
+ +
+

Controlling units (2 of 4)

+

+ To issue an instruction to a unit, long press the right mouse button on the map. This will allow you to select an action, depending on what you + clicked on. +

+

+
+
+ )} + + + <> + {step === 7 && ( +
+ +
+

Controlling units (3 of 4)

+

+ The same instructions can be issued using the unit control toolbar. First select the tool, then left click on the map to issue the instruction. + The tool will be active until deselected, either by double clicking on the map, or by clicking on the tool button again.{" "} +

+

+ Tools can be enabled using keyboard shortcuts. To learn what a tool does and what shortcut enables it, place your cursor over the corresponding + button.{" "} +

+
+
+ )} + + + <> + {step === 8 && ( +
+ +
+

Controlling units (4 of 4)

+

+ {" "} + Unit properties are set using the unit control menu, which opens automatically when a unit is selected. Here, depending on the selected unit, + you can set altitude and speed, Rules Of Engagement, reaction to threat, as well as advanced settings like AWACS frequencies and so on.{" "} +

+

+
+
+ )} + + + <> + {step === 9 && ( +
+ +
+

The unit marker (1 of 2)

+

+ The unit marker is a small icon that appears on the map. It shows the unit's type, coalition, and the name (for Mission Editor units and human + players only). It has the following parts: +

+
+
+

+

+ 1 +
+

Unit short label or type symbol

+

+
+
+

+

+ 2 +
+

Flight level

+

+
+
+

+

+ 3 +
+

Ground speed (knots)

+

+
+
+

+

+ 4 +
+

Bullseye position

+

+
+
+

+

+ 5 +
+

Fuel state (% of internal)

+

+
+
+

+

+ 6 +
+

A/A weapons (Fox 1/2/3 & guns)

+

+
+
+

+

+ 7 +
+

Current state

+

+
+
+

+ Most of these information is only available for air units. Ground units will show the type symbol, the name, and the coalition, and the fuel + level is replace by the unit's health (%). +

+
+
+ )} + + + <> + {step === 10 && ( +
+
+

The unit marker (2 of 2)

+

The unit marker has a symbol showing the unit state, i.e. what instruction it is performing. These are all the possible values:

+
+
+

+ +

Attacking unit or ground

+

+
+
+

+ +

Operating as AWACS

+

+
+
+

+ +

Under DCS control

+

+
+
+

+ +

Following unit

+

+
+
+

+ +

Human player

+

+
+
+

+ +

Idle, orbiting

+

+
+
+

+ +

Landing at point (helicopter)

+

+
+
+

+ +

Miss on purpose mode

+

+
+
+

+ +

No task, not controllable

+

+
+
+

+ +

Shut down

+

+
+
+

+ +

Refueling from tanker

+

+
+
+

+ +

RTB

+

+
+
+

+ +

Scenic AAA mode

+

+
+
+

+ +

Simulating fire fight

+

+
+
+

+ +

Operating as AAR tanker

+

+
+
+
+
+ )} + + +
+ {step > 0 ? ( + + ) : ( +
+ )} + + {step > 0 && ( +
+ {[...Array(MAX_STEPS).keys()].map((i) => ( +
+ ))} +
+ )} + + {step < MAX_STEPS ? ( + + ) : ( + + )} +
+ + ); +} diff --git a/frontend/react/src/ui/modals/warningmodal.tsx b/frontend/react/src/ui/modals/warningmodal.tsx index 2ee38610..99fc3f58 100644 --- a/frontend/react/src/ui/modals/warningmodal.tsx +++ b/frontend/react/src/ui/modals/warningmodal.tsx @@ -28,10 +28,11 @@ export function WarningModal(props: { open: boolean }) { warningText = (
Non-Google Chrome Browser Detected. + It appears you are using a browser other than Google Chrome. - It appears you are using a browser other than Google Chrome. + If you encounter any problems, we strongly suggest you use a Chrome based browser. Many features, especially advanced ones such as audio playback + and capture, were developed specifically for Chrome based browsers.{" "} - If you encounter any problems, we strongly suggest you use a Chrome based browser. Many features, especially advanced ones such as audio playback and capture, were developed specifically for Chrome based browsers.
Your connection to DCS Olympus is not secure. + To protect your personal data some advanced DCS Olympus features like the camera plugin or the audio backend have been disabled. - To protect your personal data some advanced DCS Olympus features like the camera plugin or the audio backend - have been disabled. - - - To solve this issue, DCS Olympus should be served using the https protocol. + To solve this issue, DCS Olympus should be served using the{" "} + + https + {" "} + protocol. To do so, we suggest using a dedicated server and a reverse proxy with SSL enabled.
@@ -77,38 +79,27 @@ export function WarningModal(props: { open: boolean }) { } return ( - -
-
- -
Warning
-
-
{warningText}
-
- -
+ +
+ +
Warning
+
+
{warningText}
+
+
); diff --git a/frontend/react/src/ui/panels/audiomenu.tsx b/frontend/react/src/ui/panels/audiomenu.tsx index 7adeb529..87614869 100644 --- a/frontend/react/src/ui/panels/audiomenu.tsx +++ b/frontend/react/src/ui/panels/audiomenu.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from "react"; import { Menu } from "./components/menu"; import { getApp } from "../../olympusapp"; -import { FaPlus, FaQuestionCircle } from "react-icons/fa"; +import { FaExclamation, FaExclamationCircle, FaPlus, FaQuestionCircle } from "react-icons/fa"; import { AudioSourcePanel } from "./components/sourcepanel"; import { AudioSource } from "../../audio/audiosource"; import { RadioSinkPanel } from "./components/radiosinkpanel"; @@ -117,15 +117,20 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children? return ( -
- The audio menu allows you to add and manage audio sources, connect them to unit loudspeakers and radios, and to tune radio frequencies. +
+
+ +
+
+ The audio menu allows you to add and manage audio sources, connect them to unit loudspeakers and radios, and to tune radio frequencies. +
<> {!audioManagerEnabled && ( -
-
- +
+
+
diff --git a/frontend/react/src/ui/panels/components/menu.tsx b/frontend/react/src/ui/panels/components/menu.tsx index 6f4d6271..2d7dec4f 100644 --- a/frontend/react/src/ui/panels/components/menu.tsx +++ b/frontend/react/src/ui/panels/components/menu.tsx @@ -1,4 +1,4 @@ -import { faArrowLeft, faClose } from "@fortawesome/free-solid-svg-icons"; +import { faArrowLeft, faCircleQuestion, faClose } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React, { useEffect, useState } from "react"; import { FaChevronDown, FaChevronUp } from "react-icons/fa"; @@ -11,18 +11,22 @@ export function Menu(props: { onBack?: () => void; showBackButton?: boolean; children?: JSX.Element | JSX.Element[]; + wiki?: () => (JSX.Element | JSX.Element[]); }) { const [hide, setHide] = useState(true); + const [wiki, setWiki] = useState(false); if (!props.open && hide) setHide(false); - + return (
setWiki(!wiki)} + icon={faCircleQuestion} className={` ml-auto flex cursor-pointer items-center justify-center rounded-md p-2 text-lg @@ -67,23 +71,46 @@ export function Menu(props: { hover:bg-gray-200 `} /> + -
- {props.children} +
+
+ {props.wiki ? props.wiki() :
Work in progress
} +
+
{props.children}
{props.canBeHidden == true && (
setHide(!hide)} > - {hide ? : } + {hide ? ( + + ) : ( + + )}
)}
diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index 99fb6a36..8f2f008b 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -1,6 +1,19 @@ import React, { useEffect, useRef, useState } from "react"; import { OlRoundStateButton, OlStateButton, OlLockStateButton } from "../components/olstatebutton"; -import { faSkull, faCamera, faFlag, faVolumeHigh, faDownload, faUpload, faDrawPolygon, faCircle, faTriangleExclamation, faWifi, faHourglass, faInfo } from "@fortawesome/free-solid-svg-icons"; +import { + faSkull, + faCamera, + faFlag, + faVolumeHigh, + faDownload, + faUpload, + faDrawPolygon, + faCircle, + faTriangleExclamation, + faWifi, + faHourglass, + faInfo, +} from "@fortawesome/free-solid-svg-icons"; import { OlDropdownItem, OlDropdown } from "../components/oldropdown"; import { OlLabelToggle } from "../components/ollabeltoggle"; import { getApp, IP } from "../../olympusapp"; @@ -35,7 +48,8 @@ import { RED_COMMANDER, } from "../../constants/constants"; import { OlympusConfig } from "../../interfaces"; -import { FaCheck, FaSave, FaSpinner } from "react-icons/fa"; +import { FaCheck, FaQuestionCircle, FaSave, FaSpinner } from "react-icons/fa"; +import { OlExpandingTooltip } from "../components/olexpandingtooltip"; export function Header() { const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS); @@ -84,10 +98,9 @@ export function Header() { } return ( -
+ { + getApp().getMap().setOption("showMissionDrawings", !mapOptions.showMissionDrawings); + }} + tooltip={() => ( + + )} + /> getApp().getMap().setOption("showUnitsEngagementRings", !mapOptions.showUnitsEngagementRings)} checked={mapOptions.showUnitsEngagementRings} @@ -294,6 +339,12 @@ export function Header() { .getMap() .setOption("cameraPluginMode", mapOptions.cameraPluginMode === "live" ? "map" : "live"); }} + tooltip={() => ( + + )} /> { getApp().getMap().setOption("cameraPluginEnabled", !mapOptions.cameraPluginEnabled); }} - tooltip="Activate/deactivate camera plugin" + tooltip={() => ( + + )} /> {mapSources.map((source) => { @@ -312,6 +368,10 @@ export function Header() { ); })} + getApp().setState(OlympusState.TRAINING)} + className={`cursor-pointer text-2xl text-white`} + />
{!scrolledRight && ( )} - +
); } diff --git a/frontend/react/src/ui/panels/spawnmenu.tsx b/frontend/react/src/ui/panels/spawnmenu.tsx index 555bb270..dc0c7001 100644 --- a/frontend/react/src/ui/panels/spawnmenu.tsx +++ b/frontend/react/src/ui/panels/spawnmenu.tsx @@ -119,6 +119,35 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? setBlueprint(null); setEffect(null); }} + wiki={() => { + return ( +
+

Spawn menu

+

The spawn menu allows you to spawn new units in the current mission.

+

Moreover, it allows you to spawn effects like smokes and explosions.

+

You can use the search bar to quickly find a specific unit. Otherwise, open the category you are interested in, and use the filters to refine your selection.

+ +
Click on a unit to enter the spawn properties menu. The menu is divided into multiple sections: +
    +
  • Unit name and short description
  • +
  • Quick access name
  • +
  • Spawn properties
  • +
  • Loadout description
  • +
+
+

To get more info on each control, hover your cursor on it.

+

Quick access

+

If you plan on reusing the same spawn multiple times during the mission, you can "star" the spawn properties. This will allow you to reuse them quickly multiple times. The starred spawn will save all settings, so you can create starred spawn with multiple variations, e.g. loadouts, or skill levels.

+ +
+ ); + }} > <> {blueprint === null && effect === null && ( diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index d8783d3a..56d810ba 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -47,7 +47,7 @@ import { } from "../components/olicons"; import { Coalition } from "../../types/types"; import { convertROE, deepCopyTable, ftToM, knotsToMs, mToFt, msToKnots } from "../../other/utils"; -import { FaCog, FaGasPump, FaSignal, FaTag } from "react-icons/fa"; +import { FaChevronLeft, FaCog, FaExclamationCircle, FaGasPump, FaQuestionCircle, FaSignal, FaTag } from "react-icons/fa"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { OlSearchBar } from "../components/olsearchbar"; import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; @@ -58,6 +58,9 @@ import { OlStringInput } from "../components/olstringinput"; import { OlFrequencyInput } from "../components/olfrequencyinput"; import { UnitSink } from "../../audio/unitsink"; import { AudioManagerStateChangedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent, UnitsUpdatedEvent } from "../../events"; +import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; +import { OlExpandingTooltip } from "../components/olexpandingtooltip"; +import { OlLocation } from "../components/ollocation"; export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { function initializeUnitsData() { @@ -125,6 +128,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { const [activeRadioSettings, setActiveRadioSettings] = useState(null as null | { radio: Radio; TACAN: TACAN }); const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | GeneralSettings); const [lastUpdateTime, setLastUpdateTime] = useState(0); + const [showScenicModes, setShowScenicModes] = useState(true); var searchBarRef = useRef(null); @@ -223,7 +227,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { const selectedCategories = getApp()?.getUnitsManager()?.getSelectedUnitsCategories() ?? []; const filteredUnits = Object.values(getApp()?.getUnitsManager()?.getUnits() ?? {}).filter( - (unit) => unit.getUnitName().toLowerCase().indexOf(filterString.toLowerCase()) >= 0 + (unit) => (unit.getUnitName().toLowerCase().indexOf(filterString.toLowerCase()) >= 0 || (unit.getBlueprint()?.label ?? "").toLowerCase()?.indexOf(filterString.toLowerCase()) >= 0 ) ); const everyUnitIsGround = selectedCategories.every((category) => { @@ -264,15 +268,39 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { title={selectedUnits.length > 0 ? `Units selected (x${selectedUnits.length})` : `No units selected`} onClose={props.onClose} canBeHidden={true} + wiki={() => { + return
+

Unit selection tool

+
+ The unit control menu serves two purposes. If no unit is currently selected, it allows you to select units based on their category, coalition, and control mode. You can also select units based on their specific type by using the search input. +
+

Unit control tool

+
If units are selected, the menu will display the selected units and allow you to control their altitude, speed, rules of engagement, and other settings.
+
The available controls depend on what type of unit is selected. Only controls applicable to every selected unit will be displayed, so make sure to refine your selection.
+
You will be able to inspect the current values of the controls, e.g. the desired altitude, rules of engagement and so on. However, if multiple units are selected, you will only see the values of controls that are set to be the same for each selected unit.
+
For example, if two airplanes are selected and they both have been instructed to fly at 1000ft, you will see the altitude slider set at that value. But if one airplane is set to fly at 1000ft and the other at 2000ft, you will see the slider display 'Different values'.
+
If at that point you move the slider, you will instruct both airplanes to fly at the same altitude.
+
If a single unit is selected, you will also be able to see additional info on the unit, like its fuel level, position and altitude, tasking, and available ammunition.
+
+ + }} > <> {/* ============== Selection tool START ============== */} {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. +
+
+ +
+
+ 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. +
{selectionID === null && ( @@ -328,16 +356,18 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { )} {selectionID === null && Object.entries({ - aircraft: olButtonsVisibilityAircraft, - helicopter: olButtonsVisibilityHelicopter, - "groundunit-sam": olButtonsVisibilityGroundunitSam, - groundunit: olButtonsVisibilityGroundunit, - navyunit: olButtonsVisibilityNavyunit, + aircraft: [olButtonsVisibilityAircraft, "Aircrafts"], + helicopter: [olButtonsVisibilityHelicopter, "Helicopters"], + "groundunit-sam": [olButtonsVisibilityGroundunitSam, "SAMs"], + groundunit: [olButtonsVisibilityGroundunit, "Ground units"], + navyunit: [olButtonsVisibilityNavyunit, "Navy units"], }).map((entry, idx) => { return ( - - + +
{entry[1][1] as string}
{["blue", "neutral", "red"].map((coalition) => { return ( @@ -420,8 +450,23 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { onClick={() => { setSelectionID(unit.ID); }} + > - {unit.getUnitName()} +
{ + unit.setHighlighted(true); + }} + onMouseLeave={() => { + unit.setHighlighted(false); + }} + >{unit.getUnitName()} ({unit.getBlueprint()?.label ?? ""})
); })} @@ -573,6 +618,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { }) ); }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} />
void }) { }) ); }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} /> )}
@@ -671,7 +730,71 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { > Rules of engagement - + ( + +
Sets the rule of engagement of the unit, in order:
+
+
+ {" "} + Hold fire: The unit will not shoot in + any circumstance +
+
+ {" "} + Return fire: The unit will not fire + unless fired upon +
+
+ {" "} + {" "} +
+ {" "} + Fire on target: The unit will not fire unless fired upon

or

ordered to do so{" "} +
+
+
+ {" "} + Free: The unit will fire at any + detected enemy in range +
+
+
+
+ +
+
+ Currently, DCS blue and red ground units do not respect{" "} + and{" "} + rules of engagement, so be careful, they + may start shooting when you don't want them to. Use neutral units for finer control. +
+
+
+ } + /> + )} + tooltipRelativeToParent={true} + > {[olButtonsRoeHold, olButtonsRoeReturn, olButtonsRoeDesignated, olButtonsRoeFree].map((icon, idx) => { return ( void }) { > Threat reaction - + ( + +
Sets the reaction to threat of the unit, in order:
+
+
+ {" "} + No reaction: The unit will not + react in any circumstance +
+
+ {" "} + Passive: The unit will use + counter-measures, but will not alter its course +
+
+ {" "} + Manouevre: The unit will try + to evade the threat using manoeuvres, but no counter-measures +
+
+ {" "} + Full evasion: the unit will try + to evade the threat both manoeuvering and using counter-measures +
+
+
+ } + /> + )} + tooltipRelativeToParent={true} + > {[olButtonsThreatNone, olButtonsThreatPassive, olButtonsThreatManoeuvre, olButtonsThreatEvade].map((icon, idx) => { return ( void }) { > Radar and ECM - + ( + +
Sets the units radar and Electronic Counter Measures (jamming) use policy, in order:
+
+
+ {" "} + Radio silence: No radar or + ECM will be used +
+
+ {" "} + Defensive: The unit will turn + radar and ECM on only when threatened +
+
+ {" "} + Attack: The unit will use + radar and ECM when engaging other units +
+
+ {" "} + Free: the unit will use the + radar and ECM all the time +
+
+
+ } + /> + )} + tooltipRelativeToParent={true} + tooltipPosition="above" + > {[olButtonsEmissionsSilent, olButtonsEmissionsDefend, olButtonsEmissionsAttack, olButtonsEmissionsFree].map((icon, idx) => { return ( void }) { }) ); }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} />
)} @@ -849,6 +1064,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { }) ); }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} />
)} @@ -909,153 +1131,221 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { flex flex-col gap-4 rounded-md bg-olympus-200/30 p-4 `} > - {/* ============== Scenic AAA toggle START ============== */} -
- - Scenic AAA mode - - { - getApp() - .getUnitsManager() - .scenicAAA(null, () => - setForcedUnitsData({ - ...forcedUnitsData, - scenicAAA: !selectedUnitsData.scenicAAA, - missOnPurpose: false, - }) - ); - }} - /> -
- {/* ============== Scenic AAA toggle END ============== */} - {/* ============== Miss on purpose toggle START ============== */} -
- - Miss on purpose mode - - { - getApp() - .getUnitsManager() - .missOnPurpose(null, () => - setForcedUnitsData({ - ...forcedUnitsData, - scenicAAA: false, - missOnPurpose: !selectedUnitsData.missOnPurpose, - }) - ); - }} - /> -
- {/* ============== Miss on purpose toggle END ============== */} -
- {/* ============== Shots scatter START ============== */} -
+
+
- Shots scatter + Scenic modes - - {[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => { - return ( - { - getApp() - .getUnitsManager() - .setShotsScatter(idx + 1, null, () => - setForcedUnitsData({ - ...forcedUnitsData, - 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 ( - { - getApp() - .getUnitsManager() - .setShotsIntensity(idx + 1, null, () => - setForcedUnitsData({ - ...forcedUnitsData, - shotsIntensity: idx + 1, - }) - ); - }} - active={selectedUnitsData.shotsIntensity === idx + 1} - icon={icon} - /> - ); - })} - -
- {/* ============== Shots intensity END ============== */} -
- {/* ============== Operate as toggle START ============== */} - {selectedUnits.every((unit) => unit.getCoalition() === "neutral") && ( -
- - Operate as - - { - getApp() - .getUnitsManager() - .setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue", null, () => - setForcedUnitsData({ - ...forcedUnitsData, - operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue", - }) - ); - }} + onClick={() => setShowScenicModes(!showScenicModes)} />
+ {showScenicModes && ( +
+
+
+ +
+
+ Currently, DCS blue and red ground units do not respect their rules of engagement, so be careful, they may start shooting when + you don't want them to. Use neutral units for finer control, then use the "Operate as" toggle to switch their "side". +
+
+
+ )} +
+ {showScenicModes && ( + <> + {/* ============== Scenic AAA toggle START ============== */} +
+ + Scenic AAA mode + + { + getApp() + .getUnitsManager() + .scenicAAA(null, () => + setForcedUnitsData({ + ...forcedUnitsData, + scenicAAA: !selectedUnitsData.scenicAAA, + missOnPurpose: false, + }) + ); + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + /> +
+ {/* ============== Scenic AAA toggle END ============== */} + {/* ============== Miss on purpose toggle START ============== */} +
+ + Miss on purpose mode + + { + getApp() + .getUnitsManager() + .missOnPurpose(null, () => + setForcedUnitsData({ + ...forcedUnitsData, + scenicAAA: false, + missOnPurpose: !selectedUnitsData.missOnPurpose, + }) + ); + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + /> +
+ {/* ============== Miss on purpose toggle END ============== */} +
+ {/* ============== Shots scatter START ============== */} +
+ + Shots scatter + + + {[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => { + return ( + { + getApp() + .getUnitsManager() + .setShotsScatter(idx + 1, null, () => + setForcedUnitsData({ + ...forcedUnitsData, + 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 ( + { + getApp() + .getUnitsManager() + .setShotsIntensity(idx + 1, null, () => + setForcedUnitsData({ + ...forcedUnitsData, + shotsIntensity: idx + 1, + }) + ); + }} + active={selectedUnitsData.shotsIntensity === idx + 1} + icon={icon} + /> + ); + })} + +
+ {/* ============== Shots intensity END ============== */} +
+ {/* ============== Operate as toggle START ============== */} + {selectedUnits.every((unit) => unit.getCoalition() === "neutral") && ( +
+ + Operate as + + { + getApp() + .getUnitsManager() + .setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue", null, () => + setForcedUnitsData({ + ...forcedUnitsData, + operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue", + }) + ); + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + tooltipPosition="above" + /> +
+ )} + {/* ============== Operate as toggle END ============== */} + )} - {/* ============== Operate as toggle END ============== */}
{/* ============== Follow roads toggle START ============== */}
@@ -1079,6 +1369,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { }) ); }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + tooltipPosition="above" />
{/* ============== Follow roads toggle END ============== */} @@ -1104,6 +1402,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { }) ); }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + tooltipPosition="above" />
{/* ============== Unit active toggle END ============== */} @@ -1146,6 +1452,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { } }); }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + tooltipPosition="above" /> ) : (
@@ -1261,9 +1575,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { value={activeRadioSettings ? activeRadioSettings.TACAN.channel : 1} > - + { @@ -1297,7 +1612,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { />
- Enabled{" "} + Enable TACAN{" "} { @@ -1464,8 +1779,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { hover:bg-gray-800 `} onClick={() => { - setActiveRadioSettings(null); - setShowRadioSettings(false); + setActiveAdvancedSettings(null); + setShowAdvancedSettings(false); }} > Cancel @@ -1485,20 +1800,39 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { bg-olympus-600 p-4 `} > -
-
40 && `bg-green-700`} - ${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && ` - bg-yellow-700 +
+
+
{selectedUnits[0].getUnitName()}
+
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() <= 10 && `bg-red-700`} - px-2 py-1 text-sm font-bold text-white - `} - > - - {selectedUnits[0].getFuel()}% + > + + {selectedUnits[0].getFuel()}% +
+
+ +
+ {selectedUnits[0].getTask()} +
+
+ +
{Math.round(mToFt(selectedUnits[0].getPosition().alt ?? 0))} ft
diff --git a/frontend/react/src/ui/panels/unitspawnmenu.tsx b/frontend/react/src/ui/panels/unitspawnmenu.tsx index 30d3f1a1..20ccda99 100644 --- a/frontend/react/src/ui/panels/unitspawnmenu.tsx +++ b/frontend/react/src/ui/panels/unitspawnmenu.tsx @@ -20,6 +20,7 @@ import { OlAccordion } from "../components/olaccordion"; import { AppStateChangedEvent, SpawnHeadingChangedEvent } from "../../events"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FaQuestionCircle } from "react-icons/fa"; +import { OlExpandingTooltip } from "../components/olexpandingtooltip"; export function UnitSpawnMenu(props: { visible: boolean; @@ -124,6 +125,11 @@ export function UnitSpawnMenu(props: { if (props.coalition) setSpawnCoalition(props.coalition); }, [props.coalition]); + /* Effect to update the initial altitude when the blueprint changes */ + useEffect(() => { + setSpawnAltitude((maxAltitude - minAltitude) / 2); + }, [props.blueprint]); + /* Heading compass */ const [compassAngle, setCompassAngle] = useState(0); const compassRef = useRef(null); @@ -185,7 +191,8 @@ export function UnitSpawnMenu(props: { }); /* Initialize the loadout */ - spawnLoadoutName === "" && loadouts.length > 0 && setSpawnLoadoutName(loadouts[0].name); + if (spawnLoadoutName === "" && loadouts.length > 0) + setSpawnLoadoutName(loadouts.find((loadout) => loadout.name !== "Empty loadout")?.name ?? loadouts[0].name); const spawnLoadout = props.blueprint?.loadouts?.find((loadout) => { return loadout.name === spawnLoadoutName; }); @@ -195,7 +202,7 @@ export function UnitSpawnMenu(props: { {props.compact ? ( <> {props.visible && ( -
+
( + + )} checked={key in props.starredSpawns} icon={faStar} > @@ -281,6 +293,13 @@ export function UnitSpawnMenu(props: { leftLabel={"AGL"} rightLabel={"ASL"} onClick={() => setSpawnAltitudeType(!spawnAltitudeType)} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} />
Role - + ( + + )} + tooltipRelativeToParent={true} + > {roles.map((role) => { return ( Weapons - + ( + + )} + tooltipRelativeToParent={true} + > {loadouts.map((loadout) => { return ( ( + + )} + tooltipRelativeToParent={true} > {props.blueprint?.liveries && Object.keys(props.blueprint?.liveries) @@ -428,7 +471,17 @@ export function UnitSpawnMenu(props: { > Skill - + ( + + )} + tooltipRelativeToParent={true} + > {["Average", "Good", "High", "Excellent"].map((skill) => { return ( ( + + )} + tooltipRelativeToParent={true} />
@@ -551,7 +611,7 @@ export function UnitSpawnMenu(props: { focus:outline-none focus:ring-4 `} onClick={() => { - if (spawnRequestTable){ + if (spawnRequestTable) { spawnRequestTable.unit.heading = deg2rad(compassAngle); getApp() .getUnitsManager() @@ -586,7 +646,7 @@ export function UnitSpawnMenu(props: { gap-2 `} > -
Quick access:
+
Quick access:
{ setQuickAccessName(e.target.value); @@ -595,11 +655,19 @@ export function UnitSpawnMenu(props: { /> { - if (spawnRequestTable) + if (spawnRequestTable) { + spawnRequestTable.unit.heading = compassAngle; if (key in props.starredSpawns) getApp().getMap().removeStarredSpawnRequestTable(key); else getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable, quickAccessName); + } }} - tooltip="Save this spawn for quick access" + tooltip={() => ( + + )} + tooltipRelativeToParent={true} checked={key in props.starredSpawns} icon={faStar} > @@ -611,17 +679,28 @@ export function UnitSpawnMenu(props: { `} > {!props.coalition && ( - { - spawnCoalition === "blue" && setSpawnCoalition("neutral"); - spawnCoalition === "neutral" && setSpawnCoalition("red"); - spawnCoalition === "red" && setSpawnCoalition("blue"); - }} - /> + <> +
Coalition:
+ { + spawnCoalition === "blue" && setSpawnCoalition("neutral"); + spawnCoalition === "neutral" && setSpawnCoalition("red"); + spawnCoalition === "red" && setSpawnCoalition("blue"); + }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + /> + )} +
Units:
{ !isNaN(Number(ev.target.value)) && setSpawnNumber(Math.max(minNumber, Math.min(maxNumber, Number(ev.target.value)))); }} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} />
@@ -668,6 +754,13 @@ export function UnitSpawnMenu(props: { leftLabel={"AGL"} rightLabel={"ASL"} onClick={() => setSpawnAltitudeType(!spawnAltitudeType)} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} />
Role - + ( + + )} + tooltipRelativeToParent={true} + > {roles.map((role) => { return ( Weapons - + ( + + )} + tooltipRelativeToParent={true} + > {loadouts.map((loadout) => { return ( ( + + )} + tooltipRelativeToParent={true} > {props.blueprint?.liveries && Object.keys(props.blueprint?.liveries) @@ -811,7 +928,17 @@ export function UnitSpawnMenu(props: { > Skill - + ( + + )} + tooltipRelativeToParent={true} + > {["Average", "Good", "High", "Excellent"].map((skill) => { return (
-
- Spawn heading -
-
Drag to change
-
-
- - { - setCompassAngle(Number(ev.target.value)); - }} - onDecrease={() => { - setCompassAngle(normalizeAngle(compassAngle - 1)); - }} - onIncrease={() => { - setCompassAngle(normalizeAngle(compassAngle + 1)); - }} - value={compassAngle} - /> - -
- - +
+ Spawn heading +
+
Drag to change
+ + { + setCompassAngle(Number(ev.target.value)); + }} + onDecrease={() => { + setCompassAngle(normalizeAngle(compassAngle - 1)); + }} + onIncrease={() => { + setCompassAngle(normalizeAngle(compassAngle + 1)); + }} + value={compassAngle} + tooltip={() => ( + + )} + tooltipRelativeToParent={true} + tooltipPosition="above" + /> + +
+ + +
+
{spawnLoadout && spawnLoadout.items.length > 0 && (
+ )} diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 6c8c7e2b..4f61d188 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -1724,8 +1724,6 @@ export abstract class Unit extends CustomMarker { } #drawRacetrack() { - this.#clearRacetrack(); - if (getApp().getMap().getOptions().showRacetracks) { let groundspeed = this.#speed; @@ -1805,6 +1803,8 @@ export abstract class Unit extends CustomMarker { } this.#racetrackArrow.setLatLng(pointArrow); this.#racetrackArrow.setBearing(this.#racetrackBearing); + } else { + this.#clearRacetrack(); } }