feat: started working on wiki entries and tooltips, minor fixes and improvements to presentation

This commit is contained in:
Davide Passoni 2025-03-06 17:45:20 +01:00
parent 8aac6b7d7e
commit 8406619868
43 changed files with 2764 additions and 1178 deletions

View File

@ -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",

View File

@ -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" />
<path
d="M 20.752403,8.175778 9.0427604,31.040927 32.000076,30.985263 Z"
id="path1-9"
style="fill:none;stroke:#272727;stroke-width:3.39495;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
sodipodi:nodetypes="cccc" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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";

View File

@ -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) {

View File

@ -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 {

View File

@ -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<string | null> {
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<string | null> {
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;
}
}

View File

@ -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 <div className="inline-flex rounded-md shadow-sm">{props.children}</div>;
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 (
<>
<div
ref={buttonRef}
className="inline-flex rounded-md shadow-sm"
onMouseEnter={() => {
setHoverTimeout(
window.setTimeout(() => {
setHover(true);
setHoverTimeout(null);
}, 400)
);
}}
onMouseLeave={() => {
setHover(false);
if (hoverTimeout) {
window.clearTimeout(hoverTimeout);
setHoverTimeout(null);
}
}}
>
{props.children}
</div>
{hover && props.tooltip && (
<OlTooltip
buttonRef={buttonRef}
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
position={props.tooltipPosition}
relativeToParent={props.tooltipRelativeToParent}
/>
)}
</>
);
}
export function OlButtonGroupItem(props: { icon: IconProp; active: boolean; onClick: () => void }) {

View File

@ -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 (
<div className="inline-flex cursor-pointer items-center" onClick={props.onClick}>
<>
<div
className="inline-flex cursor-pointer items-center"
onClick={(e) => {
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}
>
<button className="peer sr-only" />
<div
data-flash={props.coalition === undefined}
data-coalition={props.coalition ?? "blue"}
className={`
${props.className ?? ""}
peer relative h-7 w-14 rounded-full bg-gray-200
after:absolute after:start-[4px] after:top-0.5 after:h-6 after:w-6
after:rounded-full after:border after:border-gray-300 after:bg-white
@ -37,6 +75,16 @@ export function OlCoalitionToggle(props: { coalition: Coalition | undefined; onC
{props.coalition ? `${props.coalition[0].toLocaleUpperCase() + props.coalition.substring(1)}` : "Diff. values"}
</span>
)}
</div>
{hover && props.tooltip && (
<OlTooltip
buttonRef={buttonRef}
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
position={props.tooltipPosition}
relativeToParent={props.tooltipRelativeToParent}
/>
)}
</>
);
}

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef, MutableRefObject } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { OlTooltip } from "./oltooltip";
export function OlDropdown(props: {
disableAutoClose?: boolean;
@ -11,8 +12,13 @@ export function OlDropdown(props: {
children?: JSX.Element | JSX.Element[];
buttonRef?: MutableRefObject<null> | null;
open?: boolean;
tooltip?: string | (() => JSX.Element | JSX.Element[]);
tooltipPosition?: string;
tooltipRelativeToParent?: boolean;
}) {
const [open, setOpen] = props.open !== undefined ? [props.open, () => {}] : useState(false);
const [hover, setHover] = useState(false);
const [hoverTimeout, setHoverTimeout] = useState(null as number | null);
var contentRef = useRef(null);
var buttonRef = props.buttonRef !== undefined ? props.buttonRef : useRef(null);
@ -88,67 +94,92 @@ export function OlDropdown(props: {
});
return (
<div className={props.className ?? ""}>
{props.buttonRef === undefined && (
<button
ref={buttonRef}
onClick={() => {
setOpen(!open);
}}
className={`
inline-flex w-full items-center justify-between rounded-lg border
bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white
dark:border-gray-700 dark:bg-gray-700 dark:text-gray-100
dark:hover:bg-gray-600
hover:bg-blue-800
`}
type="button"
>
{props.leftIcon && (
<FontAwesomeIcon
icon={props.leftIcon}
className={`mr-3`}
/>
)}
<span className="overflow-hidden text-ellipsis text-nowrap">{props.label ?? ""}</span>
<svg
<>
<div className={props.className ?? ""}>
{props.buttonRef === undefined && (
<button
ref={buttonRef}
onClick={() => {
setOpen(!open);
}}
className={`
ml-auto ms-3 h-2.5 w-2.5 flex-none transition-transform
data-[open='true']:-scale-y-100
inline-flex w-full items-center justify-between rounded-lg border
bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white
dark:border-gray-700 dark:bg-gray-700 dark:text-gray-100
dark:hover:bg-gray-600
hover:bg-blue-800
`}
data-open={open}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 10 6"
type="button"
onMouseEnter={() => {
setHoverTimeout(
window.setTimeout(() => {
setHover(true);
setHoverTimeout(null);
}, 400)
);
}}
onMouseLeave={() => {
setHover(false);
if (hoverTimeout) {
window.clearTimeout(hoverTimeout);
setHoverTimeout(null);
}
}}
>
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 4 4 4-4" />
</svg>
</button>
)}
{props.leftIcon && (
<FontAwesomeIcon
icon={props.leftIcon}
className={`mr-3`}
/>
)}
<span className="overflow-hidden text-ellipsis text-nowrap">{props.label ?? ""}</span>
<svg
className={`
ml-auto ms-3 h-2.5 w-2.5 flex-none transition-transform
data-[open='true']:-scale-y-100
`}
data-open={open}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 10 6"
>
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 4 4 4-4" />
</svg>
</button>
)}
<div
ref={contentRef}
data-open={open}
className={`
absolute z-40 divide-y divide-gray-100 overflow-y-scroll no-scrollbar
rounded-lg bg-white p-2 shadow
dark:bg-gray-700
data-[open='false']:hidden
`}
>
<div
ref={contentRef}
data-open={open}
className={`
h-fit w-full text-sm text-gray-700
dark:text-gray-200
absolute z-40 divide-y divide-gray-100 overflow-y-scroll
no-scrollbar rounded-lg bg-white p-2 shadow
dark:bg-gray-700
data-[open='false']:hidden
`}
onClick={() => {
props.disableAutoClose !== true && setOpen(false);
}}
>
{props.children}
<div
className={`
h-fit w-full text-sm text-gray-700
dark:text-gray-200
`}
onClick={() => {
props.disableAutoClose !== true && setOpen(false);
}}
>
{props.children}
</div>
</div>
</div>
</div>
{hover && !open && buttonRef && props.tooltip && (
<OlTooltip
buttonRef={buttonRef}
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
position={props.tooltipPosition}
relativeToParent={props.tooltipRelativeToParent}
/>
)}
</>
);
}

View File

@ -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 (
<div className="overflow-hidden" style={{ animationName: "tooltipFadeInHeight", animationDuration: "1s", animationFillMode: "forwards", height: "26px" }}>
<div className={`absolute left-0 top-0 h-full w-full bg-transparent`}>
<div
className="h-full bg-blue-500/5"
style={{ animationName: "loadingBar", animationDuration: "1s", animationFillMode: "forwards", width: "100%" }}
></div>
</div>
<div className="mb-4 flex h-6 content-center gap-2 px-2">
<div className="my-auto">
<FaQuestionCircle className={`text-gray-200`} />
</div>
<div className="my-auto">{props.title}</div>
</div>
<div
className={`
flex min-w-[350px] flex-col gap-2 text-wrap p-2 text-gray-400
`}
style={{ animationName: "tooltipFadeInWidth", animationDuration: "1s", animationFillMode: "forwards", width: "1px" }}
>
{props.content}
</div>
</div>
);
}

View File

@ -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 (
<button
onClick={(e) => {
e.stopPropagation();
props.onClick();
}}
className={`
relative flex h-10 w-[120px] flex-none cursor-pointer select-none
flex-row content-center justify-between rounded-md border px-1 py-[5px]
text-sm
dark:border-gray-600 dark:border-transparent dark:bg-gray-700
dark:hover:bg-gray-600 dark:focus:ring-blue-800
focus:outline-none focus:ring-2 focus:ring-blue-300
`}
>
<span
data-flash={props.toggled === undefined}
data-toggled={props.toggled ?? false}
<>
<button
onClick={(e) => {
e.stopPropagation();
props.onClick();
setHover(false);
}}
className={`
absolute my-auto h-[28px] w-[54px] rounded-md bg-blue-500
transition-transform
data-[flash='true']:animate-pulse
data-[toggled='true']:translate-x-14
`}
></span>
<span
data-active={!(props.toggled ?? false)}
className={`
absolute left-[17px] top-[8px] font-normal transition-colors
dark:data-[active='false']:text-gray-400
dark:data-[active='true']:text-white
relative flex h-10 w-[120px] flex-none cursor-pointer select-none
flex-row content-center justify-between rounded-md border px-1
py-[5px] text-sm
dark:border-gray-600 dark:border-transparent dark:bg-gray-700
dark:hover:bg-gray-600 dark:focus:ring-blue-800
focus:outline-none focus:ring-2 focus:ring-blue-300
`}
ref={buttonRef}
onMouseEnter={() => {
setHoverTimeout(
window.setTimeout(() => {
setHover(true);
setHoverTimeout(null);
}, 400)
);
}}
onMouseLeave={() => {
setHover(false);
if (hoverTimeout) {
window.clearTimeout(hoverTimeout);
setHoverTimeout(null);
}
}}
>
{props.leftLabel}
</span>
<span
data-active={props.toggled ?? false}
className={`
absolute right-[17px] top-[8px] font-normal transition-colors
dark:data-[active='false']:text-gray-400
dark:data-[active='true']:text-white
`}
>
{props.rightLabel}
</span>
</button>
<span
data-flash={props.toggled === undefined}
data-toggled={props.toggled ?? false}
className={`
absolute my-auto h-[28px] w-[54px] rounded-md bg-blue-500
transition-transform
data-[flash='true']:animate-pulse
data-[toggled='true']:translate-x-14
`}
></span>
<span
data-active={!(props.toggled ?? false)}
className={`
absolute left-[17px] top-[8px] font-normal transition-colors
dark:data-[active='false']:text-gray-400
dark:data-[active='true']:text-white
`}
>
{props.leftLabel}
</span>
<span
data-active={props.toggled ?? false}
className={`
absolute right-[17px] top-[8px] font-normal transition-colors
dark:data-[active='false']:text-gray-400
dark:data-[active='true']:text-white
`}
>
{props.rightLabel}
</span>
</button>
{hover && props.tooltip && (
<OlTooltip
buttonRef={buttonRef}
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
position={props.tooltipPosition}
relativeToParent={props.tooltipRelativeToParent}
/>
)}
</>
);
}

View File

@ -42,7 +42,7 @@ export function OlLocation(props: { location: LatLng; className?: string; refere
>
{props.location.lat >= 0 ? "N" : "S"}
</span>
{zeroAppend(props.location.lat, 3, true, 6)}
{zeroAppend(props.location.lat, 3, true, 6)}°
</div>
<div className="flex w-[50%] gap-2">
<span
@ -52,7 +52,7 @@ export function OlLocation(props: { location: LatLng; className?: string; refere
>
{props.location.lng >= 0 ? "E" : "W"}
</span>
{zeroAppend(props.location.lng, 3, true, 6)}
{zeroAppend(props.location.lng, 3, true, 6)}°
</div>
</div>
);

View File

@ -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<HTMLInputElement>) => void;
}) {
const [hover, setHover] = useState(false);
const [hoverTimeout, setHoverTimeout] = useState(null as number | null);
var buttonRef = useRef(null);
return (
<div
className={`
@ -18,12 +26,31 @@ export function OlNumberInput(props: {
min-w-32
`}
>
<div className="relative flex max-w-[8rem] items-center">
<div
className="relative flex max-w-[8rem] items-center"
ref={buttonRef}
onMouseEnter={() => {
setHoverTimeout(
window.setTimeout(() => {
setHover(true);
setHoverTimeout(null);
}, 400)
);
}}
onMouseLeave={() => {
setHover(false);
if (hoverTimeout) {
window.clearTimeout(hoverTimeout);
setHoverTimeout(null);
}
}}
>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
props.onDecrease();
setHover(false);
}}
className={`
h-10 rounded-s-lg bg-gray-100 p-3
@ -48,7 +75,10 @@ export function OlNumberInput(props: {
<input
type="text"
onChange={props.onChange}
onClick={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
setHover(false);
}}
min={props.min}
max={props.max}
className={`
@ -66,6 +96,7 @@ export function OlNumberInput(props: {
onClick={(e) => {
e.stopPropagation();
props.onIncrease();
setHover(false);
}}
className={`
h-10 rounded-e-lg bg-gray-100 p-3
@ -88,6 +119,14 @@ export function OlNumberInput(props: {
</svg>
</button>
</div>
{hover && props.tooltip && (
<OlTooltip
buttonRef={buttonRef}
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
position={props.tooltipPosition}
relativeToParent={props.tooltipRelativeToParent}
/>
)}
</div>
);
}

View File

@ -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);
}
}}
>
<div className={`m-auto flex w-fit content-center justify-center gap-2`}>
{props.icon && <FontAwesomeIcon icon={props.icon} data-bright={props.buttonColor && props.checked && computeBrightness(props.buttonColor) > 200} className={`
m-auto text-gray-200
data-[bright='true']:text-gray-800
`} />}
{props.icon && (
<FontAwesomeIcon
icon={props.icon}
data-bright={props.buttonColor && props.checked && computeBrightness(props.buttonColor) > 200}
className={`
m-auto text-gray-200
data-[bright='true']:text-gray-800
`}
/>
)}
{props.children}
</div>
</button>
{hover && props.tooltip && <OlTooltip buttonRef={buttonRef} content={typeof(props.tooltip) === "string" ? props.tooltip: props.tooltip()} position={props.tooltipPosition}/>}
{hover && props.tooltip && (
<OlTooltip
buttonRef={buttonRef}
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
position={props.tooltipPosition}
relativeToParent={props.tooltipRelativeToParent}
/>
)}
</>
);
}
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);
}
}}
>
<FontAwesomeIcon className="pt-[3px]" icon={props.icon} />
</button>
{hover && <OlTooltip buttonRef={buttonRef} content={props.tooltip} />}
{hover && props.tooltip && (
<OlTooltip buttonRef={buttonRef} content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()} position={props.tooltipPosition} />
)}
</>
);
}
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);
}
}}
>
<FontAwesomeIcon className="pt-[3px]" icon={props.checked == true ? faUnlockAlt : faLock} />
</button>
{hover && <OlTooltip buttonRef={buttonRef} content={props.tooltip} />}
{hover && props.tooltip && (
<OlTooltip buttonRef={buttonRef} content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()} position={props.tooltipPosition} />
)}
</>
);
}

View File

@ -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 (
<div
className="inline-flex cursor-pointer items-center"
onClick={(e) => {
e.stopPropagation();
props.onClick();
}}
>
<button className="peer sr-only" />
<>
<div
data-toggled={props.toggled === true ? "true" : props.toggled === undefined ? "undefined" : "false"}
className={`
peer relative h-7 w-14 rounded-full bg-gray-200
after:absolute after:start-[4px] after:top-0.5 after:h-6 after:w-6
after:rounded-full after:border after:border-gray-300 after:bg-white
after:transition-all after:content-['']
dark:border-gray-600 dark:peer-focus:ring-blue-800
dark:data-[toggled='true']:bg-blue-500
data-[toggled='false']:bg-gray-500
data-[toggled='true']:after:translate-x-full
data-[toggled='true']:after:border-white
data-[toggled='undefined']:bg-gray-800
data-[toggled='undefined']:after:translate-x-[50%]
peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300
rtl:data-[toggled='true']:after:-translate-x-full
`}
></div>
</div>
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);
}
}}
>
<button className="peer sr-only" />
<div
data-toggled={props.toggled === true ? "true" : props.toggled === undefined ? "undefined" : "false"}
className={`
peer relative h-7 w-14 rounded-full bg-gray-200
after:absolute after:start-[4px] after:top-0.5 after:h-6 after:w-6
after:rounded-full after:border after:border-gray-300 after:bg-white
after:transition-all after:content-['']
dark:border-gray-600 dark:peer-focus:ring-blue-800
dark:data-[toggled='true']:bg-blue-500
data-[toggled='false']:bg-gray-500
data-[toggled='true']:after:translate-x-full
data-[toggled='true']:after:border-white
data-[toggled='undefined']:bg-gray-800
data-[toggled='undefined']:after:translate-x-[50%]
peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300
rtl:data-[toggled='true']:after:-translate-x-full
`}
></div>
</div>
{hover && props.tooltip && (
<OlTooltip
buttonRef={buttonRef}
content={typeof props.tooltip === "string" ? props.tooltip : props.tooltip()}
position={props.tooltipPosition}
relativeToParent={props.tooltipRelativeToParent}
/>
)}
</>
);
}

View File

@ -1,6 +1,11 @@
import React, { useEffect, useRef, useState } from "react";
export function OlTooltip(props: { content: string | JSX.Element | JSX.Element[]; buttonRef: React.MutableRefObject<null>; position?: string }) {
export function OlTooltip(props: {
content: string | JSX.Element | JSX.Element[];
buttonRef: React.MutableRefObject<null>;
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[]
<div
ref={contentRef}
className={`
absolute z-50 whitespace-nowrap rounded-lg bg-gray-900 px-3 py-2
text-sm font-medium text-white shadow-sm
dark:bg-gray-700
pointer-events-none absolute z-50 whitespace-nowrap rounded-lg px-3
py-2 text-sm font-medium text-white shadow-md backdrop-blur-sm
backdrop-grayscale transition-transform no-scrollbar bg-olympus-800/90
`}
>
{props.content}

View File

@ -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 (
<div
data-coalition={props.coalition}
@ -14,34 +29,55 @@ export function OlUnitSummary(props: { blueprint: UnitBlueprint; coalition: Coal
data-[coalition='neutral']:border-gray-400
data-[coalition='red']:border-red-500
`}
onMouseEnter={() => {
setHoverTimeout(
window.setTimeout(() => {
setHover(true);
setHoverTimeout(null);
}, 400)
);
}}
onMouseLeave={() => {
setHover(false);
if (hoverTimeout) {
window.clearTimeout(hoverTimeout);
setHoverTimeout(null);
}
}}
>
<div className="flex flex-row content-center gap-2">
<img
className={`
absolute right-5 top-0 h-full object-cover opacity-10 invert
`}
src={"vite./images/units/" + props.blueprint.filename}
alt=""
/>
{imageUrl && hover && <img className={``} src={imageUrl} alt="" />}
<div
className={`
flex w-full flex-row content-center justify-between gap-2
ol-panel-container
`}
>
<div
className={`
my-auto ml-2 w-full font-semibold tracking-tight text-gray-900
my-auto ml-2 flex w-full justify-between text-nowrap font-semibold
tracking-tight text-gray-900
dark:text-white
`}
>
{props.blueprint.label}
</div>
{imageUrl && (
<div className="flex w-full min-w-0 gap-1 text-sm text-gray-500">
<FaQuestionCircle
className={`my-auto min-w-4`}
/>
<div className={`my-auto max-w-full truncate`}>Hover to show image</div>
</div>
)}
</div>
<div
className={`flex h-fit flex-col justify-between px-2 leading-normal`}
>
<div className={`flex h-fit flex-col justify-between px-2 leading-normal`}>
<p
className={`
mb-1 text-sm text-gray-700
dark:text-gray-400
`}
>
{props.blueprint.description}
{summary ?? props.blueprint.description}
</p>
</div>
<div className="flex flex-row gap-1 px-2">

View File

@ -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
<div className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}></div>
<div
className={`
${props.className}
fixed left-[50%] top-[50%] z-40 translate-x-[-50%]
translate-y-[-50%] rounded-xl border-[1px] border-solid
border-gray-700 drop-shadow-md
fixed left-[50%] top-[50%] z-40 inline-flex h-[75%] max-h-[600px]
w-[80%] max-w-[1100px] translate-x-[-50%] translate-y-[-50%]
overflow-y-auto scroll-smooth rounded-xl border-[1px] border-solid
border-gray-700 bg-white drop-shadow-md
dark:bg-olympus-800
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
max-md:border-none
`}
>
{props.children}
<img
src={`images/splash/${splash}.jpg`}
className={`contents-center w-full object-cover opacity-[7%]`}
></img>
<div className="fixed left-0 top-0 h-full w-full">
<div
className={`
absolute h-full w-full bg-gradient-to-r from-blue-200/25
to-transparent
`}
></div>
<div
className={`
absolute h-full w-full bg-gradient-to-t from-olympus-800
to-transparent
`}
></div>
<div
className={`
absolute flex h-full w-full flex-col gap-8 p-16
max-lg:p-8
`}
>
{props.children}
<div
className={`
absolute right-5 top-5 cursor-pointer text-xl text-white
`}
>
<FaXmark
onClick={() => {
getApp().setState(OlympusState.IDLE);
}}
/>{" "}
</div>
</div>
</div>
</div>
</>
)}

View File

@ -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 (
<Modal
open={props.open}
className={`
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white p-10
dark:bg-olympus-800
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
max-md:border-none
`}
>
<div className="flex h-full w-full flex-col gap-4">
<div className={`flex flex-col items-start gap-2`}>
<span
<Modal open={props.open} className={``}>
<div className={`flex flex-col justify-between gap-2`}>
<span
className={`
text-gray-800 text-md
dark:text-white
`}
>
{appSubState === ImportExportSubstate.EXPORT ? "Export to file" : "Import from file"}
</span>
<span className="text-gray-400">
{appSubState === ImportExportSubstate.EXPORT ? <>Select what units you want to export to file using the toggles below</> : <></>}
</span>
<div className="flex w-full flex-col gap-2">
<div
className={`
text-gray-800 text-md
dark:text-white
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
`}
>
{appSubState === ImportExportSubstate.EXPORT ? "Export to file" : "Import from file"}
</span>
Control mode
</div>
<span className="text-gray-400">
{appSubState === ImportExportSubstate.EXPORT ? <>Select what units you want to export to file using the toggles below</> : <></>}
</span>
<div className="flex flex-col justify-start gap-2">
{Object.entries({
olympus: ["Olympus controlled", olButtonsVisibilityOlympus],
dcs: ["From DCS mission", olButtonsVisibilityDcs],
}).map((entry, idx) => {
return (
<div className="flex justify-between" key={idx}>
<span className="font-light text-white">{entry[1][0] as string}</span>
<OlToggle
key={entry[0]}
onClick={() => {
selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]];
setSelectionFilter(deepCopyTable(selectionFilter));
}}
toggled={selectionFilter["control"][entry[0]]}
/>
</div>
);
})}
</div>
<div className="flex w-full flex-col gap-2">
<div
className={`
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
`}
>
Control mode
</div>
<div
className={`
text-bold mt-5 border-b-2 border-b-white/10 pb-2 text-gray-400
`}
>
Types and coalitions
</div>
<div className="flex flex-col justify-start gap-2">
<table className="mr-16">
<tbody>
<tr>
<td></td>
<td className="pb-4 text-center font-bold text-blue-500">BLUE</td>
<td className="pb-4 text-center font-bold text-gray-500">NEUTRAL</td>
<td className="pb-4 text-center font-bold text-red-500">RED</td>
</tr>
{Object.entries({
olympus: ["Olympus controlled", olButtonsVisibilityOlympus],
dcs: ["From DCS mission", olButtonsVisibilityDcs],
"groundunit-sam": olButtonsVisibilityGroundunitSam,
groundunit: olButtonsVisibilityGroundunit,
navyunit: olButtonsVisibilityNavyunit,
}).map((entry, idx) => {
return (
<div className="flex justify-between" key={idx}>
<span className="font-light text-white">{entry[1][0] as string}</span>
<OlToggle
key={entry[0]}
onClick={() => {
selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]];
setSelectionFilter(deepCopyTable(selectionFilter));
}}
toggled={selectionFilter["control"][entry[0]]}
/>
</div>
<tr key={idx}>
<td className="w-16 text-lg text-gray-200">
<FontAwesomeIcon icon={entry[1]} />
</td>
{["blue", "neutral", "red"].map((coalition) => {
return (
<td className="w-32 text-center" key={coalition}>
<OlCheckbox
checked={
(appSubState === ImportExportSubstate.EXPORT &&
selectionFilter[coalition][entry[0]] &&
selectableUnits.find((unit) => 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));
}}
/>
<span className="absolute ml-2 text-white">
{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{" "}
</span>
</td>
);
})}
</tr>
);
})}
</div>
<div
className={`
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
`}
>
Types and coalitions
</div>
<table className="mr-16">
<tbody>
{
<tr>
<td></td>
<td className="pb-4 text-center font-bold text-blue-500">BLUE</td>
<td className="pb-4 text-center font-bold text-gray-500">NEUTRAL</td>
<td className="pb-4 text-center font-bold text-red-500">RED</td>
<td className="text-gray-200"></td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["blue"]).some((value) => value)}
onChange={() => {
const newValue = !Object.values(selectionFilter["blue"]).some((value) => value);
Object.keys(selectionFilter["blue"]).forEach((key) => {
selectionFilter["blue"][key] = newValue;
});
setSelectionFilter(deepCopyTable(selectionFilter));
}}
/>
</td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["neutral"]).some((value) => value)}
onChange={() => {
const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value);
Object.keys(selectionFilter["neutral"]).forEach((key) => {
selectionFilter["neutral"][key] = newValue;
});
setSelectionFilter(deepCopyTable(selectionFilter));
}}
/>
</td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["red"]).some((value) => value)}
onChange={() => {
const newValue = !Object.values(selectionFilter["red"]).some((value) => value);
Object.keys(selectionFilter["red"]).forEach((key) => {
selectionFilter["red"][key] = newValue;
});
setSelectionFilter(deepCopyTable(selectionFilter));
}}
/>
</td>
</tr>
{Object.entries({
"groundunit-sam": olButtonsVisibilityGroundunitSam,
groundunit: olButtonsVisibilityGroundunit,
navyunit: olButtonsVisibilityNavyunit,
}).map((entry, idx) => {
return (
<tr key={idx}>
<td className="w-16 text-lg text-gray-200">
<FontAwesomeIcon icon={entry[1]} />
</td>
{["blue", "neutral", "red"].map((coalition) => {
return (
<td className="w-32 text-center" key={coalition}>
<OlCheckbox
checked={
(appSubState === ImportExportSubstate.EXPORT &&
selectionFilter[coalition][entry[0]] &&
selectableUnits.find((unit) => 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));
}}
/>
<span className="absolute ml-2 text-white">
{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{" "}
</span>
</td>
);
})}
</tr>
);
})}
{
<tr>
<td className="text-gray-200"></td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["blue"]).some((value) => value)}
onChange={() => {
const newValue = !Object.values(selectionFilter["blue"]).some((value) => value);
Object.keys(selectionFilter["blue"]).forEach((key) => {
selectionFilter["blue"][key] = newValue;
});
setSelectionFilter(deepCopyTable(selectionFilter));
}}
/>
</td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["neutral"]).some((value) => value)}
onChange={() => {
const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value);
Object.keys(selectionFilter["neutral"]).forEach((key) => {
selectionFilter["neutral"][key] = newValue;
});
setSelectionFilter(deepCopyTable(selectionFilter));
}}
/>
</td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["red"]).some((value) => value)}
onChange={() => {
const newValue = !Object.values(selectionFilter["red"]).some((value) => value);
Object.keys(selectionFilter["red"]).forEach((key) => {
selectionFilter["red"][key] = newValue;
});
setSelectionFilter(deepCopyTable(selectionFilter));
}}
/>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</tbody>
</table>
</div>
</div>
<div className="flex justify-end">
<button
type="button"
onClick={() => {
if (appSubState === ImportExportSubstate.EXPORT) {
var unitsToExport: { [key: string]: any } = {};
selectableUnits
.filter((unit) => selectionFilter[unit.getCoalition()][unit.getMarkerCategory()])
.forEach((unit: Unit) => {
var data: any = unit.getData();
if (unit.getGroupName() in unitsToExport) unitsToExport[unit.getGroupName()].push(data);
else unitsToExport[unit.getGroupName()] = [data];
});
<div className="flex justify-end">
<button
type="button"
onClick={() => {
if (appSubState === ImportExportSubstate.EXPORT) {
var unitsToExport: { [key: string]: any } = {};
selectableUnits
.filter((unit) => selectionFilter[unit.getCoalition()][unit.getMarkerCategory()])
.forEach((unit: Unit) => {
var data: any = unit.getData();
if (unit.getGroupName() in unitsToExport) unitsToExport[unit.getGroupName()].push(data);
else unitsToExport[unit.getGroupName()] = [data];
});
const file = new Blob([JSON.stringify(unitsToExport)], {
type: "text/plain",
});
const defaultName = "olympus_export_" + missionData.mission.theatre + "_" + missionData.sessionHash + "_" + missionData.time + ".json";
//@ts-ignore TODO
if (window.showSaveFilePicker) {
const opts = {
types: [
{
description: "DCS Olympus export file",
accept: { "text/plain": [".json"] },
},
],
suggestedName: defaultName,
};
//@ts-ignore TODO
showSaveFilePicker(opts)
.then((handle) => handle.createWritable())
.then((writeable) => {
getApp().setState(OlympusState.IDLE);
getApp().addInfoMessage("Exporting data please wait...");
writeable
.write(file)
.then(() => writeable.close())
.catch(() => getApp().addInfoMessage("An error occurred while exporting the data"));
})
.then(() => getApp().addInfoMessage("Data exported correctly"))
.catch((err) => getApp().addInfoMessage("An error occurred while exporting the data"));
} else {
const a = document.createElement("a");
const file = new Blob([JSON.stringify(unitsToExport)], {
type: "text/plain",
});
a.href = URL.createObjectURL(file);
const defaultName = "olympus_export_" + missionData.mission.theatre + "_" + missionData.sessionHash + "_" + missionData.time + ".json";
//@ts-ignore TODO
if (window.showSaveFilePicker) {
const opts = {
types: [
{
description: "DCS Olympus export file",
accept: { "text/plain": [".json"] },
},
],
suggestedName: defaultName,
};
//@ts-ignore TODO
showSaveFilePicker(opts)
.then((handle) => handle.createWritable())
.then((writeable) => {
getApp().setState(OlympusState.IDLE);
getApp().addInfoMessage("Exporting data please wait...");
writeable
.write(file)
.then(() => writeable.close())
.catch(() => getApp().addInfoMessage("An error occurred while exporting the data"));
})
.then(() => getApp().addInfoMessage("Data exported correctly"))
.catch((err) => getApp().addInfoMessage("An error occurred while exporting the data"));
} else {
const a = document.createElement("a");
const file = new Blob([JSON.stringify(unitsToExport)], {
type: "text/plain",
});
a.href = URL.createObjectURL(file);
var filename = defaultName;
a.download = filename;
a.click();
}
} 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);
var filename = defaultName;
a.download = filename;
a.click();
}
}}
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
<FontAwesomeIcon icon={faArrowRight} />
</button>
} else {
for (const [groupName, groupData] of Object.entries(importData)) {
if (groupName === "" || groupData.length === 0 || !groupData.some((unitData: UnitData) => unitData.alive)) continue;
<button
type="button"
onClick={() => getApp().setState(OlympusState.IDLE)}
className={`
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
text-white
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
dark:hover:bg-gray-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
Back
</button>
</div>
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
<FontAwesomeIcon icon={faArrowRight} />
</button>
<button
type="button"
onClick={() => getApp().setState(OlympusState.IDLE)}
className={`
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
dark:hover:bg-gray-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
Back
</button>
</div>
</Modal>
);

View File

@ -61,99 +61,91 @@ export function KeybindModal(props: { open: boolean }) {
}
return (
<Modal
open={props.open}
className={`
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white p-10
dark:bg-olympus-800
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
max-md:border-none
`}
>
<div className="flex h-full w-full flex-col gap-4">
<div className={`flex flex-col items-start gap-2`}>
<span
className={`
text-gray-800 text-md
dark:text-white
`}
>
{shortcut?.getOptions().label}
</span>
<span
className={`
text-gray-800 text-md
dark:text-gray-500
`}
>
Press the key you want to bind to this event
</span>
</div>
<div className="w-full text-center text-white">
{ctrlKey && "Ctrl + "}
{altKey && "Alt + "}
{shiftKey && "Shift + "}
{code}
</div>
<div className="text-white">
{available === true && <div className="text-green-600">Keybind is free!</div>}
{available === false && (
<div className="flex flex-col gap-2">
<div className="flex gap-2">
Keybind is already in use: <div className={`
flex flex-wrap gap-2 font-bold text-orange-600
`}>{inUseShortcuts.map((shortcut) => <span>{shortcut.getOptions().label}</span>)}</div>
<Modal open={props.open}>
<div className={`flex flex-col gap-2`}>
<span
className={`
text-gray-800 text-md
dark:text-white
`}
>
{shortcut?.getOptions().label}
</span>
<span
className={`
text-gray-800 text-md
dark:text-gray-400
`}
>
Press the key you want to bind to this event
</span>
</div>
<div className="w-full text-center text-white">
{ctrlKey && "Ctrl + "}
{altKey && "Alt + "}
{shiftKey && "Shift + "}
{code}
</div>
<div className="text-white">
{available === true && <div className="text-green-600">Keybind is free!</div>}
{available === false && (
<div className="flex flex-col gap-2">
<div className="flex gap-2">
Keybind is already in use:{" "}
<div
className={`flex flex-wrap gap-2 font-bold text-orange-600`}
>
{inUseShortcuts.map((shortcut) => (
<span>{shortcut.getOptions().label}</span>
))}
</div>
<div className="text-gray-500">A key combination can be assigned to multiple actions, and all bound actions will fire</div>
</div>
)}
</div>
<div className="flex justify-end">
{shortcut && (
<button
type="button"
onClick={() => {
if (shortcut && code) {
let options = shortcut.getOptions();
options.code = code;
options.altKey = altKey;
options.shiftKey = shiftKey;
options.ctrlKey = ctrlKey;
getApp().getShortcutManager().setShortcutOption(shortcut.getId(), options);
getApp().setState(OlympusState.OPTIONS);
}
}}
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
<FontAwesomeIcon icon={faArrowRight} />
</button>
)}
<div className="text-gray-400">A key combination can be assigned to multiple actions, and all bound actions will fire</div>
</div>
)}
</div>
<div className="flex justify-end">
{shortcut && (
<button
type="button"
onClick={() => getApp().setState(OlympusState.OPTIONS)}
onClick={() => {
if (shortcut && code) {
let options = shortcut.getOptions();
options.code = code;
options.altKey = altKey;
options.shiftKey = shiftKey;
options.ctrlKey = ctrlKey;
getApp().getShortcutManager().setShortcutOption(shortcut.getId(), options);
getApp().setState(OlympusState.OPTIONS);
}
}}
className={`
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
text-white
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
dark:hover:bg-gray-700 dark:focus:ring-blue-800
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
`}
>
Back
Continue
<FontAwesomeIcon icon={faArrowRight} />
</button>
</div>
)}
<button
type="button"
onClick={() => getApp().setState(OlympusState.OPTIONS)}
className={`
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
dark:hover:bg-gray-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
Back
</button>
</div>
</Modal>
);

View File

@ -80,346 +80,301 @@ export function LoginModal(props: { open: boolean }) {
useEffect(subStateCallback, [subState]);
return (
<Modal
open={props.open}
className={`
inline-flex h-[75%] max-h-[600px] w-[80%] max-w-[1100px] overflow-y-auto
scroll-smooth bg-white
dark:bg-olympus-800
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
max-md:border-none
`}
>
<img
src="images/splash/1.jpg"
className={`contents-center w-full object-cover opacity-[7%]`}
></img>
<Modal open={props.open}>
<div
className={`
absolute h-full w-full bg-gradient-to-r from-blue-200/25
to-transparent
`}
></div>
<div
className={`
absolute h-full w-full bg-gradient-to-t from-olympus-800
to-transparent
`}
></div>
<div
className={`
absolute flex w-full flex-col gap-8 p-16
max-lg:p-8
flex w-full flex-row gap-6
max-lg:flex-col
`}
>
<div
className={`
flex w-full flex-row gap-6
max-lg:flex-col
flex w-[40%] flex-grow flex-col content-center justify-start gap-5
max-lg:w-[100%]
`}
>
<div
className={`
flex w-[40%] flex-grow flex-col content-center justify-start gap-5
max-lg:w-[100%]
`}
>
{!checkingPassword ? (
<>
<div className="flex flex-col items-start">
<div
className={`
pt-1 text-xs text-gray-800
dark:text-gray-400
`}
>
Connect to
</div>
<div
className={`
flex items-center justify-center gap-2 text-gray-800
text-md font-bold
dark:text-gray-200
`}
>
{window.location.toString()}
</div>
{!checkingPassword ? (
<>
<div className="flex flex-col items-start">
<div
className={`
pt-1 text-xs text-gray-800
dark:text-gray-400
`}
>
Connect to
</div>
<div
className={`
flex w-[100%] flex-row content-center items-center gap-2
flex items-center justify-center gap-2 text-gray-800 text-md
font-bold
dark:text-gray-200
`}
>
<span className="size-[80px] min-w-14">
<img
src="images/olympus-500x500.png"
className={`flex w-full`}
></img>
</span>
<div className={`flex flex-col items-start gap-1`}>
<h1
className={`
flex text-wrap text-4xl font-bold text-gray-800
dark:text-white
`}
>
DCS Olympus
</h1>
<div
className={`
flex select-none content-center gap-2 rounded-sm text-sm
font-semibold text-green-700
dark:text-green-400
`}
>
<FontAwesomeIcon icon={faCheckCircle} className={`my-auto`} />
Version {VERSION}
</div>
{window.location.toString()}
</div>
</div>
<div
className={`
flex w-[100%] flex-row content-center items-center gap-2
`}
>
<span className="size-[80px] min-w-14">
<img src="images/olympus-500x500.png" className={`flex w-full`}></img>
</span>
<div className={`flex flex-col items-start gap-1`}>
<h1
className={`
flex text-wrap text-4xl font-bold text-gray-800
dark:text-white
`}
>
DCS Olympus
</h1>
<div
className={`
flex select-none content-center gap-2 rounded-sm text-sm
font-semibold text-green-700
dark:text-green-400
`}
>
<FontAwesomeIcon icon={faCheckCircle} className={`my-auto`} />
Version {VERSION}
</div>
</div>
{!loginError ? (
<>
{subState === LoginSubState.CREDENTIALS && (
<>
<div className={`flex flex-col items-start gap-2`}>
<label
className={`
text-gray-800 text-md
dark:text-white
`}
>
Username
</label>
<input
type="text"
autoComplete="username"
onChange={(ev) => 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
/>
</div>
{!loginError ? (
<>
{subState === LoginSubState.CREDENTIALS && (
<>
<div className={`flex flex-col items-start gap-2`}>
<label
className={`
text-gray-800 text-md
dark:text-white
`}
>
Username
</label>
<input
type="text"
autoComplete="username"
onChange={(ev) => 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
/>
<label
className={`
text-gray-800 text-md
dark:text-white
`}
>
Password
</label>
<input
type="password"
onChange={(ev) => 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
/>
</div>
<div className="flex">
<button
type="button"
onClick={() => getApp().setState(OlympusState.LOGIN, LoginSubState.COMMAND_MODE)}
className={`
mb-2 me-2 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
`}
>
Login
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
</button>
{/*
<label
className={`
text-gray-800 text-md
dark:text-white
`}
>
Password
</label>
<input
type="password"
onChange={(ev) => 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
/>
</div>
<div className="flex">
<button
type="button"
onClick={() => getApp().setState(OlympusState.LOGIN, LoginSubState.COMMAND_MODE)}
className={`
mb-2 me-2 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
`}
>
Login
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
</button>
{/*
<button type="button" className="flex content-center items-center gap-2 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-blue-800 rounded-sm dark:border-gray-600 border-[1px] dark:text-gray-400">
View Guide <FontAwesomeIcon className="my-auto text-xs" icon={faExternalLink} />
</button>
*/}
</div>
</>
)}
{subState === LoginSubState.COMMAND_MODE && (
<>
<div className={`flex flex-col items-start gap-2`}>
<label
className={`
text-gray-800 text-md
dark:text-white
`}
>
Choose your role
</label>
<OlDropdown
label={activeCommandMode ?? ""}
className={`w-48`}
>
{commandModes?.map((commandMode) => {
return <OlDropdownItem onClick={() => setActiveCommandMode(commandMode)}>{commandMode}</OlDropdownItem>;
})}
</OlDropdown>
</div>
<div className="flex">
<button
type="button"
onClick={() => getApp().setState(OlympusState.LOGIN, LoginSubState.CONNECT)}
className={`
mb-2 me-2 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
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
</button>
</div>
</>
)}
</>
) : (
<>
<ErrorCallout
title="Server could not be reached or password is incorrect"
description="The Olympus Server at this address could not be reached or the password is incorrect. Check your password. If correct, check the address is correct, restart the Olympus server or reinstall Olympus. Ensure the ports set are not already used."
></ErrorCallout>
<div className={`text-sm font-medium text-gray-200`}>
Still having issues? See our{" "}
<a
href=""
className={`
text-blue-300 underline
hover:no-underline
`}
>
troubleshooting guide here
</a>
.
</div>
</>
)}
</>
) : (
<div>
<svg
aria-hidden="true"
className={`
mx-auto my-auto w-40 animate-spin fill-blue-600
text-gray-200
dark:text-gray-600
`}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
)}
</div>
<div
className={`
flex flex-grow flex-row content-end justify-center gap-3
overflow-hidden
max-lg:justify-start
max-md:flex-col
`}
>
<Card className="flex">
<img
src="images/splash/1.jpg"
</div>
</>
)}
{subState === LoginSubState.COMMAND_MODE && (
<>
<div className={`flex flex-col items-start gap-2`}>
<label
className={`
text-gray-800 text-md
dark:text-white
`}
>
Choose your role
</label>
<OlDropdown label={activeCommandMode ?? ""} className={`
w-48
`}>
{commandModes?.map((commandMode) => {
return <OlDropdownItem onClick={() => setActiveCommandMode(commandMode)}>{commandMode}</OlDropdownItem>;
})}
</OlDropdown>
</div>
<div className="flex">
<button
type="button"
onClick={() => getApp().setState(OlympusState.LOGIN, LoginSubState.CONNECT)}
className={`
mb-2 me-2 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
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
</button>
</div>
</>
)}
</>
) : (
<>
<ErrorCallout
title="Server could not be reached or password is incorrect"
description="The Olympus Server at this address could not be reached or the password is incorrect. Check your password. If correct, check the address is correct, restart the Olympus server or reinstall Olympus. Ensure the ports set are not already used."
></ErrorCallout>
<div className={`text-sm font-medium text-gray-200`}>
Still having issues? See our{" "}
<a
href=""
className={`
text-blue-300 underline
hover:no-underline
`}
>
troubleshooting guide here
</a>
.
</div>
</>
)}
</>
) : (
<div>
<svg
aria-hidden="true"
className={`
h-[40%] max-h-[120px] contents-center w-full rounded-md
object-cover
`}
></img>
<div
className={`
mt-2 flex content-center items-center gap-2 font-bold
mx-auto my-auto w-40 animate-spin fill-blue-600 text-gray-200
dark:text-gray-600
`}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
YouTube Video Guide
<FontAwesomeIcon className={`my-auto text-xs text-gray-400`} icon={faExternalLink} />
</div>
<div
className={`
overflow-hidden text-ellipsis text-xs text-black
dark:text-gray-400
`}
>
Check out our official video tutorial on how to get started with Olympus - so you can immediately start controlling the battlefield.
</div>
</Card>
<Card className="flex">
<img
src="images/splash/1.jpg"
className={`
h-[40%] max-h-[120px] contents-center w-full rounded-md
object-cover
`}
></img>
<div
className={`
mt-2 flex content-center items-center gap-2 font-bold
`}
>
Wiki Guide
<FontAwesomeIcon className={`my-auto text-xs text-gray-400`} icon={faExternalLink} />
</div>
<div
className={`
overflow-hidden text-ellipsis text-xs text-black
dark:text-gray-400
`}
>
Find out more about Olympus through our online wiki guide.
</div>
</Card>
</div>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
)}
</div>
<div
className={`
flex h-full w-full text-xs font-light text-gray-600
max-lg:flex-col
flex flex-grow flex-row content-end justify-center gap-3
overflow-hidden
max-lg:justify-start
max-md:flex-col
`}
>
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.
<Card className="flex">
<img
src="images/splash/1.jpg"
className={`
h-[40%] max-h-[120px] contents-center w-full rounded-md
object-cover
`}
></img>
<div
className={`mt-2 flex content-center items-center gap-2 font-bold`}
>
YouTube Video Guide
<FontAwesomeIcon className={`my-auto text-xs text-gray-400`} icon={faExternalLink} />
</div>
<div
className={`
overflow-hidden text-ellipsis text-xs text-black
dark:text-gray-400
`}
>
Check out our official video tutorial on how to get started with Olympus - so you can immediately start controlling the battlefield.
</div>
</Card>
<Card className="flex">
<img
src="images/splash/1.jpg"
className={`
h-[40%] max-h-[120px] contents-center w-full rounded-md
object-cover
`}
></img>
<div
className={`mt-2 flex content-center items-center gap-2 font-bold`}
>
Wiki Guide
<FontAwesomeIcon className={`my-auto text-xs text-gray-400`} icon={faExternalLink} />
</div>
<div
className={`
overflow-hidden text-ellipsis text-xs text-black
dark:text-gray-400
`}
>
Find out more about Olympus through our online wiki guide.
</div>
</Card>
</div>
</div>
<div
className={`
flex h-full w-full text-xs font-light text-gray-600
max-lg:flex-col
`}
>
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.
</div>
</Modal>
);
}

View File

@ -10,14 +10,8 @@ export function ProtectionPromptModal(props: { open: boolean }) {
return (
<Modal
open={props.open}
className={`
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white p-10
dark:bg-olympus-800
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
max-md:border-none
`}
>
<div className="flex h-full w-full flex-col gap-12">
<div className="flex h-full w-full flex-col justify-between gap-12">
<div className={`flex flex-col items-start gap-2`}>
<span
className={`
@ -30,7 +24,7 @@ export function ProtectionPromptModal(props: { open: boolean }) {
<span
className={`
text-gray-800 text-md
dark:text-gray-500
dark:text-gray-400
`}
>
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 }) {
<span
className={`
text-gray-800 text-md
dark:text-gray-500
dark:text-gray-400
`}
>
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 }) {
<span
className={`
text-gray-800 text-md
dark:text-gray-500
dark:text-gray-400
`}
>
To disable this warning, press on the{" "}

View File

@ -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 (
<Modal open={props.open}>
<div>
<h1 className={`text-2xl font-semibold text-white`}>DCS Olympus guided tour</h1>
</div>
<>
{step === 0 && (
<div className="flex gap-16">
<img
src="images/olympus-500x500.png"
className={`my-auto h-40 w-40 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Home</h2>
<p>
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.
</p>
<div className="flex flex-col flex-wrap gap-2">
<div className="flex gap-2">
<FaLink className="my-auto" />
<a href="#" className={`text-blue-400`} onClick={() => setStep(1)}>
Main navbar
</a>
</div>
<div className="flex gap-2">
<FaLink className="my-auto" />
<a href="#" className={`text-blue-400`} onClick={() => setStep(2)}>
Spawning units
</a>
</div>
<div className="flex gap-2">
<FaLink className="my-auto" />
<a href="#" className={`text-blue-400`} onClick={() => setStep(5)}>
Controlling units
</a>
</div>
<div className="flex gap-2">
<FaLink className="my-auto" />
<a href="#" className={`text-blue-400`} onClick={() => setStep(9)}>
The unit marker
</a>
</div>
<div>
Every panel has a dedicated integrated wiki. Click on the{" "}
<span
className={`
mt-[-7px] inline-block translate-y-2 rounded-full p-1
`}
>
<FaQuestionCircle />
</span>{" "}
symbol to access it. Moreover, most clickable content has tooltips providing info about their function.
</div>
</div>
</div>
</div>
)}
</>
<>
{step === 1 && (
<div className="flex gap-16">
<img
src="images/training/step1.gif"
className={`h-72 w-72 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Main navbar</h2>
<p>
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.
</p>
<p>On the bottom left corner, you can find the DCS Olympus options tool.</p>
</div>
</div>
)}
</>
<>
{step === 2 && (
<div className="flex gap-16">
<img
src="images/training/step2.gif"
className={`h-72 w-72 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Spawning units (1 of 3)</h2>
<p>
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.
</p>
<p>After selecting the unit you can edit its properties, like spawn altitude and heading, loadout, livery, skill level, and so on.</p>
<p>
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.
</p>
</div>
</div>
)}
</>
<>
{step === 3 && (
<div className="flex gap-16">
<img
src="images/training/step3.gif"
className={`h-72 w-72 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Spawning units (2 of 3)</h2>
<p>
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.
</p>
<p>You can edit the unit properties like in the previous method.</p>
</div>
</div>
)}
</>
<>
{step === 4 && (
<div className="flex gap-16">
<img
src="images/training/step4.gif"
className={`h-72 w-72 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Spawning units (3 of 3)</h2>
<p>
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.
</p>
<p>You can edit the unit properties like in the previous method.</p>
</div>
</div>
)}
</>
<>
{step === 5 && (
<div className="flex gap-16">
<img
src="images/training/step5.gif"
className={`h-72 w-72 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Controlling units (1 of 4)</h2>
<p>
{" "}
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.
</p>
<p>
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.
</p>
</div>
</div>
)}
</>
<>
{step === 6 && (
<div className="flex gap-16">
<img
src="images/training/step6.gif"
className={`h-72 w-72 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Controlling units (2 of 4)</h2>
<p>
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.
</p>
<p></p>
</div>
</div>
)}
</>
<>
{step === 7 && (
<div className="flex gap-16">
<img
src="images/training/step7.gif"
className={`h-72 w-72 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Controlling units (3 of 4)</h2>
<p>
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.{" "}
</p>
<p>
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.{" "}
</p>
</div>
</div>
)}
</>
<>
{step === 8 && (
<div className="flex gap-16">
<img
src="images/training/step8.gif"
className={`h-72 w-72 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Controlling units (4 of 4)</h2>
<p>
{" "}
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.{" "}
</p>
<p> </p>
</div>
</div>
)}
</>
<>
{step === 9 && (
<div className="flex gap-16">
<img
src="images/training/unitmarker.png"
className={`max-h-34 max-w-34 my-auto rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>The unit marker (1 of 2)</h2>
<p>
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:
</p>
<div className="flex flex-wrap gap-4">
<div className="flex flex-col">
<p className="flex gap-4">
<div
className={`
rounded-full bg-gray-400 px-3 py-1 font-bold
text-olympus-900
`}
>
1
</div>
<p className="my-auto">Unit short label or type symbol</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-4">
<div
className={`
rounded-full bg-gray-400 px-3 py-1 font-bold
text-olympus-900
`}
>
2
</div>
<p className="my-auto">Flight level</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-4">
<div
className={`
rounded-full bg-gray-400 px-3 py-1 font-bold
text-olympus-900
`}
>
3
</div>
<p className="my-auto">Ground speed (knots)</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-4">
<div
className={`
rounded-full bg-gray-400 px-3 py-1 font-bold
text-olympus-900
`}
>
4
</div>
<p className="my-auto">Bullseye position</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-4">
<div
className={`
rounded-full bg-gray-400 px-3 py-1 font-bold
text-olympus-900
`}
>
5
</div>
<p className="my-auto">Fuel state (% of internal)</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-4">
<div
className={`
rounded-full bg-gray-400 px-3 py-1 font-bold
text-olympus-900
`}
>
6
</div>
<p className="my-auto">A/A weapons (Fox 1/2/3 & guns)</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-4">
<div
className={`
rounded-full bg-gray-400 px-3 py-1 font-bold
text-olympus-900
`}
>
7
</div>
<p className="my-auto">Current state</p>
</p>
</div>
</div>
<p>
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 (%).
</p>
</div>
</div>
)}
</>
<>
{step === 10 && (
<div className="flex gap-16">
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>The unit marker (2 of 2)</h2>
<p>The unit marker has a symbol showing the unit state, i.e. what instruction it is performing. These are all the possible values:</p>
<div className="flex max-h-40 flex-col flex-wrap gap-4">
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/attack.svg" />
<p className={`my-auto`}>Attacking unit or ground</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/awacs.svg" />
<p className={`my-auto`}>Operating as AWACS</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/dcs.svg" />
<p className={`my-auto`}>Under DCS control</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/follow.svg" />
<p className={`my-auto`}>Following unit</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/human.svg" />
<p className={`my-auto`}>Human player</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/idle.svg" />
<p className={`my-auto`}>Idle, orbiting</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/land-at-point.svg" />
<p className={`my-auto`}>Landing at point (helicopter)</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/miss-on-purpose.svg" />
<p className={`my-auto`}>Miss on purpose mode</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/no-task.svg" />
<p className={`my-auto`}>No task, not controllable</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/off.svg" />
<p className={`my-auto`}>Shut down</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/refuel.svg" />
<p className={`my-auto`}>Refueling from tanker</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/rtb.svg" />
<p className={`my-auto`}>RTB</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/scenic-aaa.svg" />
<p className={`my-auto`}>Scenic AAA mode</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/simulate-fire-fight.svg" />
<p className={`my-auto`}>Simulating fire fight</p>
</p>
</div>
<div className="flex flex-col">
<p className="flex gap-2">
<img src="images/states/tanker.svg" />
<p className={`my-auto`}>Operating as AAR tanker </p>
</p>
</div>
</div>
</div>
</div>
)}
</>
<div className="mt-auto flex justify-between">
{step > 0 ? (
<button
type="button"
onClick={() => setStep(step - 1)}
className={`
mb-2 me-2 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
`}
>
<FontAwesomeIcon className={`my-auto`} icon={faArrowLeft} />
Previous
</button>
) : (
<div />
)}
{step > 0 && (
<div className="my-auto flex gap-2">
{[...Array(MAX_STEPS).keys()].map((i) => (
<div
key={i + 1}
className={`
h-4 w-4 rounded-full
${i + 1 === step ? "bg-blue-700 shadow-white" : `
bg-gray-300/10
`}
`}
/>
))}
</div>
)}
{step < MAX_STEPS ? (
<button
type="button"
onClick={() => setStep(step + 1)}
className={`
mb-2 me-2 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
`}
>
Next
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
</button>
) : (
<button
type="button"
onClick={() => {}}
className={`
mb-2 me-2 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
`}
>
Finish
</button>
)}
</div>
</Modal>
);
}

View File

@ -28,10 +28,11 @@ export function WarningModal(props: { open: boolean }) {
warningText = (
<div className="flex flex-col gap-2 text-gray-400">
<span>Non-Google Chrome Browser Detected.</span>
<span>It appears you are using a browser other than Google Chrome.</span>
<span>
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.{" "}
</span>
<span>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. </span>
<div className="mt-5 flex gap-4">
<OlCheckbox
checked={mapOptions.hideChromeWarning}
@ -49,14 +50,15 @@ export function WarningModal(props: { open: boolean }) {
warningText = (
<div className="flex flex-col gap-2 text-gray-400">
<span>Your connection to DCS Olympus is not secure.</span>
<span>To protect your personal data some advanced DCS Olympus features like the camera plugin or the audio backend have been disabled.</span>
<span>
To protect your personal data some advanced DCS Olympus features like the camera plugin or the audio backend
have been disabled.
</span>
<span>
To solve this issue, DCS Olympus should be served using the <span className={`
italic
`}>https</span> protocol.
To solve this issue, DCS Olympus should be served using the{" "}
<span
className={`italic`}
>
https
</span>{" "}
protocol.
</span>
<span>To do so, we suggest using a dedicated server and a reverse proxy with SSL enabled.</span>
<div className="mt-5 flex gap-4">
@ -77,38 +79,27 @@ export function WarningModal(props: { open: boolean }) {
}
return (
<Modal
open={props.open}
className={`
inline-flex h-[50%] max-h-[600px] w-[40%] max-w-[1100px] overflow-y-auto
scroll-smooth bg-white
dark:bg-olympus-800
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
max-md:border-none
`}
>
<div className="flex h-full w-full flex-col p-14">
<div className="flex gap-2 text-xl text-white">
<FaExclamationTriangle className={`my-auto text-4xl text-yellow-300`} />
<div className="my-auto">Warning</div>
</div>
<div className="mt-10 text-white">{warningText}</div>
<div className="ml-auto mt-auto flex">
<button
type="button"
onClick={() => getApp().setState(OlympusState.IDLE)}
className={`
mb-2 me-2 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
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
</button>
</div>
<Modal open={props.open}>
<div className="flex gap-2 text-xl text-white">
<FaExclamationTriangle className={`my-auto text-4xl text-yellow-300`} />
<div className="my-auto">Warning</div>
</div>
<div className="mt-10 text-white">{warningText}</div>
<div className="ml-auto mt-auto flex">
<button
type="button"
onClick={() => getApp().setState(OlympusState.IDLE)}
className={`
mb-2 me-2 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
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
</button>
</div>
</Modal>
);

View File

@ -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 (
<Menu title="Audio menu" open={props.open} showBackButton={false} onClose={props.onClose}>
<div className="p-4 text-sm text-gray-400">
The audio menu allows you to add and manage audio sources, connect them to unit loudspeakers and radios, and to tune radio frequencies.
<div className="flex content-center gap-4 p-4">
<div className="my-auto text-gray-400">
<FaQuestionCircle />
</div>
<div className="text-sm text-gray-400">
The audio menu allows you to add and manage audio sources, connect them to unit loudspeakers and radios, and to tune radio frequencies.
</div>
</div>
<>
{!audioManagerEnabled && (
<div className="mx-6 flex rounded-lg bg-olympus-400 p-4 text-sm">
<div>
<FaQuestionCircle className="my-4 ml-2 mr-6 text-gray-400" />
<div className="mx-4 flex gap-4 rounded-lg bg-olympus-400 p-4 text-sm">
<div className="my-auto animate-bounce text-xl">
<FaExclamationCircle className="text-gray-400" />
</div>
<div className="flex flex-col gap-1">
<div className="text-gray-100">

View File

@ -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 (
<div
data-open={props.open}
data-wiki={wiki}
className={`
pointer-events-none absolute left-16 right-0 top-[58px] z-10
h-[calc(100vh-58px)] bg-transparent transition-transform
h-[calc(100vh-58px)] bg-transparent transition-all ol-panel-container
data-[open='false']:-translate-x-full
data-[wiki='true']:w-[calc(100%-58px)] data-[wiki='true']:lg:w-[800px]
sm:w-[400px]
`}
tabIndex={-1}
@ -58,8 +62,8 @@ export function Menu(props: {
)}
{props.title}
<FontAwesomeIcon
onClick={props.onClose}
icon={faClose}
onClick={() => 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
`}
/>
<FontAwesomeIcon
onClick={props.onClose}
icon={faClose}
className={`
flex cursor-pointer items-center justify-center rounded-md p-2
text-lg
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
hover:bg-gray-200
`}
/>
</h5>
<div className="h-[calc(100%-3rem)]">
{props.children}
<div className="flex h-[calc(100%-3rem)]">
<div data-wiki={wiki} className={`
w-0 overflow-hidden transition-all
data-[wiki='true']:w-[50%]
`}>
{props.wiki ? props.wiki() : <div className={`p-4 text-gray-200`}>Work in progress</div>}
</div>
<div data-wiki={wiki} className={`
w-full
sm:w-[400px]
`}>{props.children}</div>
</div>
</div>
{props.canBeHidden == true && (
<div
className={`
pointer-events-auto flex h-8 justify-center backdrop-blur-lg
backdrop-grayscale
dark:bg-olympus-800/90
pointer-events-auto flex h-8 cursor-pointer justify-center
bg-olympus-800/90 backdrop-blur-lg backdrop-grayscale
hover:bg-olympus-400/90
`}
onClick={() => setHide(!hide)}
>
{hide ? <FaChevronUp className="mx-auto my-auto text-gray-400" /> : <FaChevronDown className={`
mx-auto my-auto text-gray-400
`} />}
{hide ? (
<FaChevronUp className="mx-auto my-auto text-gray-400" />
) : (
<FaChevronDown
className={`mx-auto my-auto text-gray-400`}
/>
)}
</div>
)}
</div>

View File

@ -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 (
<nav
<div
className={`
z-10 flex w-full gap-4 border-gray-200 bg-gray-300 px-3 drop-shadow-md
align-center
z-10 flex w-full gap-4 border-gray-200 bg-gray-300 px-3 align-center
dark:border-gray-800 dark:bg-olympus-900
`}
>
@ -138,6 +151,13 @@ export function Header() {
getApp().setState(OlympusState.IMPORT_EXPORT, ImportExportSubstate.EXPORT);
}}
checked={false}
tooltip={() => (
<OlExpandingTooltip
title="Export scenario from file"
content="Selectively export the current scenario to a file. This file can be shared with other users or imported later. Currently, only ground and naval units can be exported."
/>
)}
tooltipRelativeToParent={true}
/>
<OlStateButton
icon={faUpload}
@ -145,6 +165,12 @@ export function Header() {
getApp().setState(OlympusState.IMPORT_EXPORT, ImportExportSubstate.IMPORT);
}}
checked={false}
tooltip={() => (
<OlExpandingTooltip
title="Import scenario from file"
content="Import a scenario from a previously exported file. This will add the imported units to the current scenario, so make sure to delete any unwanted units before importing."
/>
)}
/>
{savingSessionData ? (
<div className="text-white">
@ -174,20 +200,19 @@ export function Header() {
</div>
)}
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
<OlRoundStateButton
icon={faDrawPolygon}
checked={mapOptions.showMissionDrawings}
onClick={() => {
getApp().getMap().setOption("showMissionDrawings", !mapOptions.showMissionDrawings);
}}
tooltip="Show/Hide mission drawings"
/>
<OlLockStateButton
checked={!mapOptions.protectDCSUnits}
onClick={() => {
getApp().getMap().setOption("protectDCSUnits", !mapOptions.protectDCSUnits);
}}
tooltip="Lock/unlock protected units (from scripted mission)"
tooltip={() => (
<OlExpandingTooltip
title="Lock/unlock protected units"
content={<><p>By default, Mission Editor units are protected from being commanded or deleted. This option allows you to unlock them, so they can be commanded or deleted like any other unit. </p>
<p>If units are protected, you will still be able to control them, but a prompt will be shown to require your confirmation. </p>
<p>Once a unit has been commanded, it will be unlocked and will become an Olympus unit, completely abandoning its previuos mission. </p></>}
/>
)}
/>
<OlRoundStateButton
checked={audioEnabled}
@ -195,7 +220,14 @@ export function Header() {
audioEnabled ? getApp().getAudioManager().stop() : getApp().getAudioManager().start();
setAudioEnabled(!audioEnabled);
}}
tooltip="Enable/disable audio and radio backend"
tooltip={() => (
<OlExpandingTooltip
title="Enable/disable audio"
content={<><p>If this option is enabled, you will be able to access the radio and audio features of DCS Olympus. </p>
<p>For this to work, a SRS Server need to be installed and running on the same machine on which the DCS Olympus server is running.</p>
<p>For security reasons, this feature will only work if a secure connection (i.e., using https) is established with the server. It is also suggested to use Google Chrome for optimal compatibility. </p></>}
/>
)}
icon={faVolumeHigh}
/>
</div>
@ -269,6 +301,19 @@ export function Header() {
</div>
<div className={`h-8 w-0 border-l-[2px] border-gray-700`}></div>
<div className={`flex h-fit flex-row items-center justify-start gap-1`}>
<OlRoundStateButton
icon={faDrawPolygon}
checked={mapOptions.showMissionDrawings}
onClick={() => {
getApp().getMap().setOption("showMissionDrawings", !mapOptions.showMissionDrawings);
}}
tooltip={() => (
<OlExpandingTooltip
title="Hide/Show mission drawings"
content="To filter the visibile drawings and change their opacity, use the drawings menu on the left sidebar."
/>
)}
/>
<OlRoundStateButton
onClick={() => 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={() => (
<OlExpandingTooltip
title="Switch between live and map camera"
content="When the camera plugin is enabled, you can switch between the live camera view and the map view. These are equivalent to the F9 and F10 views in DCS."
/>
)}
/>
<OlStateButton
checked={mapOptions.cameraPluginEnabled}
@ -301,7 +352,12 @@ export function Header() {
onClick={() => {
getApp().getMap().setOption("cameraPluginEnabled", !mapOptions.cameraPluginEnabled);
}}
tooltip="Activate/deactivate camera plugin"
tooltip={() => (
<OlExpandingTooltip
title="Activate/deactivate camera plugin"
content="The camera plugin allows to tie the position of the map to the position of the camera in DCS. This is useful to check exactly how things look from the players perspective. Check the in-game wiki for more information." //TODO add link to wiki
/>
)}
/>
<OlDropdown label={mapSource} className="w-60">
{mapSources.map((source) => {
@ -312,6 +368,10 @@ export function Header() {
);
})}
</OlDropdown>
<FaQuestionCircle
onClick={() => getApp().setState(OlympusState.TRAINING)}
className={`cursor-pointer text-2xl text-white`}
/>
</div>
{!scrolledRight && (
<FaChevronRight
@ -321,6 +381,6 @@ export function Header() {
`}
/>
)}
</nav>
</div>
);
}

View File

@ -119,6 +119,35 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
setBlueprint(null);
setEffect(null);
}}
wiki={() => {
return (
<div className="h-full overflow-auto p-4 text-gray-400 no-scrollbar">
<h2 className="mb-4 font-bold">Spawn menu</h2>
<p>The spawn menu allows you to spawn new units in the current mission.</p>
<p>Moreover, it allows you to spawn effects like smokes and explosions.</p>
<p className="mt-2">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. </p>
<img src="images/training/unitfilter.png" className={`
mx-auto my-4 w-[80%] rounded-lg
drop-shadow-[0_0px_7px_rgba(255,255,255,0.07)]
`} />
<div className="mt-2">Click on a unit to enter the spawn properties menu. The menu is divided into multiple sections:
<ul className="ml-4 mt-2 list-inside list-decimal">
<li>Unit name and short description</li>
<li>Quick access name </li>
<li>Spawn properties</li>
<li>Loadout description</li>
</ul>
</div>
<p>To get more info on each control, hover your cursor on it.</p>
<h2 className="my-4 font-bold">Quick access</h2>
<p>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.</p>
<img src="images/training/starred.png" className={`
mx-auto my-4 w-[80%] rounded-lg
drop-shadow-[0_0px_7px_rgba(255,255,255,0.07)]
`} />
</div>
);
}}
>
<>
{blueprint === null && effect === null && (

View File

@ -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 <div className={`
h-full flex-col overflow-auto p-4 text-gray-400 no-scrollbar flex
gap-2
`}>
<h2 className="mb-4 font-bold">Unit selection tool</h2>
<div>
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.
</div>
<h2 className="my-4 font-bold">Unit control tool</h2>
<div>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.</div>
<div>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. </div>
<div> 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.</div>
<div> 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'.</div>
<div> If at that point you move the slider, you will instruct both airplanes to fly at the same altitude.</div>
<div> 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. </div>
</div>
}}
>
<>
{/* ============== Selection tool START ============== */}
{selectedUnits.length == 0 && (
<div className="flex flex-col gap-4 p-4">
<div className="text-lg text-bold text-gray-200">Selection tool</div>
<div className="text-sm text-gray-400">
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.
<div className="flex content-center gap-4">
<div className="my-auto text-gray-400">
<FaQuestionCircle />
</div>
<div className="text-sm text-gray-400">
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.
</div>
</div>
<div className="flex flex-col gap-4 rounded-lg bg-olympus-600 p-4">
{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 (
<tr key={idx}>
<td className="text-lg text-gray-200">
<FontAwesomeIcon icon={entry[1]} />
<td className="flex gap-2 text-lg text-gray-200">
<FontAwesomeIcon icon={entry[1][0] as IconDefinition} /> <div className={`
text-sm text-gray-400
`}>{entry[1][1] as string}</div>
</td>
{["blue", "neutral", "red"].map((coalition) => {
return (
@ -420,8 +450,23 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
onClick={() => {
setSelectionID(unit.ID);
}}
>
{unit.getUnitName()}
<div data-coalition={unit.getCoalition()}
className={`
flex content-center justify-between border-l-4
pl-2
data-[coalition='blue']:border-blue-500
data-[coalition='neutral']:border-gray-500
data-[coalition='red']:border-red-500
`}
onMouseEnter={() => {
unit.setHighlighted(true);
}}
onMouseLeave={() => {
unit.setHighlighted(false);
}}
>{unit.getUnitName()} ({unit.getBlueprint()?.label ?? ""})</div>
</OlDropdownItem>
);
})}
@ -573,6 +618,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
})
);
}}
tooltip={() => (
<OlExpandingTooltip
title="Altitude type"
content="Sets wether the unit will hold the selected altitude as Above Ground Level or Above Sea Level"
/>
)}
tooltipRelativeToParent={true}
/>
</div>
<OlRangeSlider
@ -638,6 +690,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
})
);
}}
tooltip={() => (
<OlExpandingTooltip
title="Airspeed type"
content="Sets wether the unit will hold the selected airspeed as Calibrated Air Speed or Ground Speed"
/>
)}
tooltipRelativeToParent={true}
/>
)}
</div>
@ -671,7 +730,71 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
>
Rules of engagement
</span>
<OlButtonGroup>
<OlButtonGroup
tooltip={() => (
<OlExpandingTooltip
title="Rules of engagement"
content={
<div className="flex flex-col gap-2">
<div>Sets the rule of engagement of the unit, in order:</div>
<div className="flex flex-col gap-2 px-2">
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsRoeHold} className={`
my-auto min-w-8 text-white
`} /> Hold fire: The unit will not shoot in
any circumstance
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsRoeReturn} className={`
my-auto min-w-8 text-white
`} /> Return fire: The unit will not fire
unless fired upon
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsRoeDesignated} className={`
my-auto min-w-8 text-white
`} />{" "}
<div>
{" "}
Fire on target: The unit will not fire unless fired upon <p className={`
inline font-bold
`}>or</p> ordered to do so{" "}
</div>
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsRoeFree} className={`
my-auto min-w-8 text-white
`} /> Free: The unit will fire at any
detected enemy in range
</div>
</div>
<div className="flex gap-4">
<div className="my-auto">
<FaExclamationCircle className={`
animate-bounce text-xl
`} />
</div>
<div>
Currently, DCS blue and red ground units do not respect{" "}
<FontAwesomeIcon icon={olButtonsRoeReturn} className={`
my-auto text-white
`} /> and{" "}
<FontAwesomeIcon icon={olButtonsRoeDesignated} className={`
my-auto text-white
`} /> rules of engagement, so be careful, they
may start shooting when you don't want them to. Use neutral units for finer control.
</div>
</div>
</div>
}
/>
)}
tooltipRelativeToParent={true}
>
{[olButtonsRoeHold, olButtonsRoeReturn, olButtonsRoeDesignated, olButtonsRoeFree].map((icon, idx) => {
return (
<OlButtonGroupItem
@ -709,7 +832,49 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
>
Threat reaction
</span>
<OlButtonGroup>
<OlButtonGroup
tooltip={() => (
<OlExpandingTooltip
title="Reaction to threat"
content={
<div className="flex flex-col gap-2">
<div>Sets the reaction to threat of the unit, in order:</div>
<div className="flex flex-col gap-2 px-2">
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsThreatNone} className={`
my-auto min-w-8 text-white
`} /> No reaction: The unit will not
react in any circumstance
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsThreatPassive} className={`
my-auto min-w-8 text-white
`} /> Passive: The unit will use
counter-measures, but will not alter its course
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsThreatManoeuvre} className={`
my-auto min-w-8 text-white
`} /> Manouevre: The unit will try
to evade the threat using manoeuvres, but no counter-measures
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsThreatEvade} className={`
my-auto min-w-8 text-white
`} /> Full evasion: the unit will try
to evade the threat both manoeuvering and using counter-measures
</div>
</div>
</div>
}
/>
)}
tooltipRelativeToParent={true}
>
{[olButtonsThreatNone, olButtonsThreatPassive, olButtonsThreatManoeuvre, olButtonsThreatEvade].map((icon, idx) => {
return (
<OlButtonGroupItem
@ -742,7 +907,50 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
>
Radar and ECM
</span>
<OlButtonGroup>
<OlButtonGroup
tooltip={() => (
<OlExpandingTooltip
title="Radar and ECM"
content={
<div className="flex flex-col gap-2">
<div>Sets the units radar and Electronic Counter Measures (jamming) use policy, in order:</div>
<div className="flex flex-col gap-2 px-2">
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsEmissionsSilent} className={`
my-auto min-w-8 text-white
`} /> Radio silence: No radar or
ECM will be used
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsEmissionsDefend} className={`
my-auto min-w-8 text-white
`} /> Defensive: The unit will turn
radar and ECM on only when threatened
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsEmissionsAttack} className={`
my-auto min-w-8 text-white
`} /> Attack: The unit will use
radar and ECM when engaging other units
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsEmissionsFree} className={`
my-auto min-w-8 text-white
`} /> Free: the unit will use the
radar and ECM all the time
</div>
</div>
</div>
}
/>
)}
tooltipRelativeToParent={true}
tooltipPosition="above"
>
{[olButtonsEmissionsSilent, olButtonsEmissionsDefend, olButtonsEmissionsAttack, olButtonsEmissionsFree].map((icon, idx) => {
return (
<OlButtonGroupItem
@ -807,6 +1015,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
})
);
}}
tooltip={() => (
<OlExpandingTooltip
title="Make AAR tanker available"
content="This option allows you to make the unit available for refuelling other planes. You can keep moving the unit around while being available as tanker, however this may cause refuelling players to disconnect. If possible, try to avoid issuing commands to the unit while it is refuelling human players. Change the tanker settings to turn the tanker TACAN on or to change the frequency on which it will respond to refuelling requests."
/>
)}
tooltipRelativeToParent={true}
/>
</div>
)}
@ -849,6 +1064,13 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
})
);
}}
tooltip={() => (
<OlExpandingTooltip
title="Make AWACS available"
content="This option allows you to make the unit available for AWACS task. It will provide bogey dopes and picture calls on the assigned frequency, which you can change in the AWACS settings."
/>
)}
tooltipRelativeToParent={true}
/>
</div>
)}
@ -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 ============== */}
<div className="flex content-center justify-between">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Scenic AAA mode
</span>
<OlToggle
toggled={selectedUnitsData.scenicAAA}
onClick={() => {
getApp()
.getUnitsManager()
.scenicAAA(null, () =>
setForcedUnitsData({
...forcedUnitsData,
scenicAAA: !selectedUnitsData.scenicAAA,
missOnPurpose: false,
})
);
}}
/>
</div>
{/* ============== Scenic AAA toggle END ============== */}
{/* ============== Miss on purpose toggle START ============== */}
<div className="flex content-center justify-between">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Miss on purpose mode
</span>
<OlToggle
toggled={selectedUnitsData.missOnPurpose}
onClick={() => {
getApp()
.getUnitsManager()
.missOnPurpose(null, () =>
setForcedUnitsData({
...forcedUnitsData,
scenicAAA: false,
missOnPurpose: !selectedUnitsData.missOnPurpose,
})
);
}}
/>
</div>
{/* ============== Miss on purpose toggle END ============== */}
<div className="flex gap-4">
{/* ============== Shots scatter START ============== */}
<div className={`flex flex-col gap-2`}>
<div className="flex flex-col gap-2">
<div className="flex justify-between">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Shots scatter
Scenic modes
</span>
<OlButtonGroup>
{[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => {
return (
<OlButtonGroupItem
key={idx}
onClick={() => {
getApp()
.getUnitsManager()
.setShotsScatter(idx + 1, null, () =>
setForcedUnitsData({
...forcedUnitsData,
shotsScatter: idx + 1,
})
);
}}
active={selectedUnitsData.shotsScatter === idx + 1}
icon={icon}
/>
);
})}
</OlButtonGroup>
</div>
{/* ============== Shots scatter END ============== */}
{/* ============== Shots intensity START ============== */}
<div className="flex flex-col gap-2">
<span
<FaChevronLeft
data-open={showScenicModes}
className={`
my-auto font-normal
dark:text-white
my-auto cursor-pointer text-gray-200
transition-transform
data-[open='true']:-rotate-90
`}
>
Shots intensity
</span>
<OlButtonGroup>
{[olButtonsIntensity1, olButtonsIntensity2, olButtonsIntensity3].map((icon, idx) => {
return (
<OlButtonGroupItem
key={idx}
onClick={() => {
getApp()
.getUnitsManager()
.setShotsIntensity(idx + 1, null, () =>
setForcedUnitsData({
...forcedUnitsData,
shotsIntensity: idx + 1,
})
);
}}
active={selectedUnitsData.shotsIntensity === idx + 1}
icon={icon}
/>
);
})}
</OlButtonGroup>
</div>
{/* ============== Shots intensity END ============== */}
</div>
{/* ============== Operate as toggle START ============== */}
{selectedUnits.every((unit) => unit.getCoalition() === "neutral") && (
<div className={`flex content-center justify-between`}>
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Operate as
</span>
<OlCoalitionToggle
coalition={selectedUnitsData.operateAs as Coalition}
onClick={() => {
getApp()
.getUnitsManager()
.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue", null, () =>
setForcedUnitsData({
...forcedUnitsData,
operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue",
})
);
}}
onClick={() => setShowScenicModes(!showScenicModes)}
/>
</div>
{showScenicModes && (
<div
className={`
flex flex-col gap-2 text-sm text-gray-400
`}
>
<div className="flex gap-4">
<div className="my-auto">
<FaExclamationCircle className={`
animate-bounce text-xl
`} />
</div>
<div>
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".
</div>
</div>
</div>
)}
</div>
{showScenicModes && (
<>
{/* ============== Scenic AAA toggle START ============== */}
<div className="flex content-center justify-between">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Scenic AAA mode
</span>
<OlToggle
toggled={selectedUnitsData.scenicAAA}
onClick={() => {
getApp()
.getUnitsManager()
.scenicAAA(null, () =>
setForcedUnitsData({
...forcedUnitsData,
scenicAAA: !selectedUnitsData.scenicAAA,
missOnPurpose: false,
})
);
}}
tooltip={() => (
<OlExpandingTooltip
title="Enable scenic AAA mode"
content="This mode will make the unit fire in the air any time an enemy unit is nearby. This can help Game Masters create a more immersive scenario without increasing its difficulty."
/>
)}
tooltipRelativeToParent={true}
/>
</div>
{/* ============== Scenic AAA toggle END ============== */}
{/* ============== Miss on purpose toggle START ============== */}
<div className="flex content-center justify-between">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Miss on purpose mode
</span>
<OlToggle
toggled={selectedUnitsData.missOnPurpose}
onClick={() => {
getApp()
.getUnitsManager()
.missOnPurpose(null, () =>
setForcedUnitsData({
...forcedUnitsData,
scenicAAA: false,
missOnPurpose: !selectedUnitsData.missOnPurpose,
})
);
}}
tooltip={() => (
<OlExpandingTooltip
title="Enable scenic miss on purpose mode"
content="This mode will make the unit fire in the direction of nearby enemy units, without actively aiming at them. It represents a sort of unguided firing, which can help Game Masters create a more immersive scenario without increasing its difficulty."
/>
)}
tooltipRelativeToParent={true}
/>
</div>
{/* ============== Miss on purpose toggle END ============== */}
<div className="flex gap-4">
{/* ============== Shots scatter START ============== */}
<div className={`flex flex-col gap-2`}>
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Shots scatter
</span>
<OlButtonGroup>
{[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => {
return (
<OlButtonGroupItem
key={idx}
onClick={() => {
getApp()
.getUnitsManager()
.setShotsScatter(idx + 1, null, () =>
setForcedUnitsData({
...forcedUnitsData,
shotsScatter: idx + 1,
})
);
}}
active={selectedUnitsData.shotsScatter === idx + 1}
icon={icon}
/>
);
})}
</OlButtonGroup>
</div>
{/* ============== Shots scatter END ============== */}
{/* ============== Shots intensity START ============== */}
<div className="flex flex-col gap-2">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Shots intensity
</span>
<OlButtonGroup>
{[olButtonsIntensity1, olButtonsIntensity2, olButtonsIntensity3].map((icon, idx) => {
return (
<OlButtonGroupItem
key={idx}
onClick={() => {
getApp()
.getUnitsManager()
.setShotsIntensity(idx + 1, null, () =>
setForcedUnitsData({
...forcedUnitsData,
shotsIntensity: idx + 1,
})
);
}}
active={selectedUnitsData.shotsIntensity === idx + 1}
icon={icon}
/>
);
})}
</OlButtonGroup>
</div>
{/* ============== Shots intensity END ============== */}
</div>
{/* ============== Operate as toggle START ============== */}
{selectedUnits.every((unit) => unit.getCoalition() === "neutral") && (
<div
className={`flex content-center justify-between`}
>
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Operate as
</span>
<OlCoalitionToggle
coalition={selectedUnitsData.operateAs as Coalition}
onClick={() => {
getApp()
.getUnitsManager()
.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue", null, () =>
setForcedUnitsData({
...forcedUnitsData,
operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue",
})
);
}}
tooltip={() => (
<OlExpandingTooltip
title="Unit operate as coalition"
content="This option is only available for neutral units and it allows you to change what coalition the unit will 'operate as' when performing scenic tasks. For example, a 'red' neutral unit tasked to perform miss on purpose will shoot in the direction of blue units. "
/>
)}
tooltipRelativeToParent={true}
tooltipPosition="above"
/>
</div>
)}
{/* ============== Operate as toggle END ============== */}
</>
)}
{/* ============== Operate as toggle END ============== */}
</div>
{/* ============== Follow roads toggle START ============== */}
<div className="flex content-center justify-between">
@ -1079,6 +1369,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
})
);
}}
tooltip={() => (
<OlExpandingTooltip
title="Follow roads when moving"
content="If enabled, this option will force the unit to stay on roads when moving to a new location. This can be useful to simulate convoys or to make the unit follow a specific path."
/>
)}
tooltipRelativeToParent={true}
tooltipPosition="above"
/>
</div>
{/* ============== Follow roads toggle END ============== */}
@ -1104,6 +1402,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
})
);
}}
tooltip={() => (
<OlExpandingTooltip
title="Turn unit off"
content="When enabled, this option will turn the unit completely off, making it inactive. This can be useful to control when a unit starts engaging the enemy."
/>
)}
tooltipRelativeToParent={true}
tooltipPosition="above"
/>
</div>
{/* ============== Unit active toggle END ============== */}
@ -1146,6 +1452,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
}
});
}}
tooltip={() => (
<OlExpandingTooltip
title="Make the unit emit sounds"
content="This option allows the unit to emit sounds as if it had loudspeakers. Turn this on to enable the option, then open the audio menu to connect a sound source to the unit. This is useful to simulate 5MC calls on the carrier, or attach sirens to unit. "
/>
)}
tooltipRelativeToParent={true}
tooltipPosition="above"
/>
) : (
<div className="text-white">
@ -1261,9 +1575,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
value={activeRadioSettings ? activeRadioSettings.TACAN.channel : 1}
></OlNumberInput>
<OlDropdown label={activeRadioSettings ? activeRadioSettings.TACAN.XY : "X"} className={`
my-auto w-20
`}>
<OlDropdown
label={activeRadioSettings ? activeRadioSettings.TACAN.XY : "X"}
className={`my-auto w-20`}
>
<OlDropdownItem
key={"X"}
onClick={() => {
@ -1297,7 +1612,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
/>
</div>
<div className="flex content-center gap-2">
<span className="my-auto text-sm">Enabled</span>{" "}
<span className="my-auto text-sm">Enable TACAN</span>{" "}
<OlToggle
toggled={activeRadioSettings ? activeRadioSettings.TACAN.isOn : false}
onClick={() => {
@ -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
`}
>
<div className="flex border-b-2 border-b-white/10 pb-2">
<div
className={`
flex content-center gap-2 rounded-full
${selectedUnits[0].getFuel() > 40 && `bg-green-700`}
${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && `
bg-yellow-700
<div
className={`
flex flex-col gap-2 border-b-2 border-b-white/10 pb-2
`}
>
<div className={`flex justify-between`}>
<div className="my-auto text-white">{selectedUnits[0].getUnitName()}</div>
<div
className={`
flex content-center gap-2 rounded-full
${selectedUnits[0].getFuel() > 40 && `bg-green-700`}
${
selectedUnits[0].getFuel() > 10 &&
selectedUnits[0].getFuel() <= 40 &&
`bg-yellow-700`
}
${selectedUnits[0].getFuel() <= 10 && `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
`}
>
<FaGasPump className="my-auto" />
{selectedUnits[0].getFuel()}%
>
<FaGasPump className="my-auto" />
{selectedUnits[0].getFuel()}%
</div>
</div>
<div className="my-auto text-sm text-gray-400">
{selectedUnits[0].getTask()}
</div>
<div className="flex content-center gap-2">
<OlLocation location={selectedUnits[0].getPosition()} className={`
w-[280px] text-sm
`}/>
<div className="my-auto text-gray-200">{Math.round(mToFt(selectedUnits[0].getPosition().alt ?? 0))} ft</div>
</div>
</div>

View File

@ -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<HTMLImageElement>(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 && (
<div className="flex max-h-[800px] flex-col overflow-auto">
<div className={`flex max-h-[800px] flex-col overflow-auto`}>
<div className="flex h-fit flex-col gap-3">
<div className="flex">
<FontAwesomeIcon
@ -245,7 +252,12 @@ export function UnitSpawnMenu(props: {
else getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable, quickAccessName);
}
}}
tooltip="Save this spawn for quick access"
tooltip={() => (
<OlExpandingTooltip
title="Add this spawn to quick access"
content="Enter a name and click on the button to later be able to quickly respawn a unit with the same configuration."
/>
)}
checked={key in props.starredSpawns}
icon={faStar}
></OlStateButton>
@ -281,6 +293,13 @@ export function UnitSpawnMenu(props: {
leftLabel={"AGL"}
rightLabel={"ASL"}
onClick={() => setSpawnAltitudeType(!spawnAltitudeType)}
tooltip={() => (
<OlExpandingTooltip
title="Select altitude type"
content="If AGL is selected, the aircraft will be spawned at the selected altitude above the ground. If ASL is selected, the aircraft will be spawned at the selected altitude above sea level."
/>
)}
tooltipRelativeToParent={true}
/>
</div>
<OlRangeSlider
@ -301,7 +320,17 @@ export function UnitSpawnMenu(props: {
>
Role
</span>
<OlDropdown label={spawnRole} className="w-64">
<OlDropdown
label={spawnRole}
className="w-64"
tooltip={() => (
<OlExpandingTooltip
title="Role of the spawned unit"
content="This selection has no effect on what the spawned unit will actually perform, and you will have total control on it. However, it is used to filter what loadouts are available."
/>
)}
tooltipRelativeToParent={true}
>
{roles.map((role) => {
return (
<OlDropdownItem
@ -326,7 +355,17 @@ export function UnitSpawnMenu(props: {
>
Weapons
</span>
<OlDropdown label={spawnLoadoutName} className={`w-64`}>
<OlDropdown
label={spawnLoadoutName}
className={`w-64`}
tooltip={() => (
<OlExpandingTooltip
title="Unit loadout"
content="This will be the loadout that the unit will have when spawned. Look at the bottom of the page to check the exact type and number of items."
/>
)}
tooltipRelativeToParent={true}
>
{loadouts.map((loadout) => {
return (
<OlDropdownItem
@ -370,6 +409,10 @@ export function UnitSpawnMenu(props: {
<OlDropdown
label={props.blueprint?.liveries ? (props.blueprint?.liveries[spawnLiveryID]?.name ?? "Default") : "No livery"}
className={`w-64`}
tooltip={() => (
<OlExpandingTooltip title="Unit livery" content="Selects the livery of the spawned unit. This is a purely cosmetic option." />
)}
tooltipRelativeToParent={true}
>
{props.blueprint?.liveries &&
Object.keys(props.blueprint?.liveries)
@ -428,7 +471,17 @@ export function UnitSpawnMenu(props: {
>
Skill
</span>
<OlDropdown label={spawnSkill} className={`w-64`}>
<OlDropdown
label={spawnSkill}
className={`w-64`}
tooltip={() => (
<OlExpandingTooltip
title="Unit skill"
content="Selects the skill of the spawned unit. Depending on the selection, the unit will be more precise and effective at its mission. Usually a lower level is selected to generate a more forgiving mission."
/>
)}
tooltipRelativeToParent={true}
>
{["Average", "Good", "High", "Excellent"].map((skill) => {
return (
<OlDropdownItem
@ -476,6 +529,13 @@ export function UnitSpawnMenu(props: {
setCompassAngle(normalizeAngle(compassAngle + 1));
}}
value={compassAngle}
tooltip={() => (
<OlExpandingTooltip
title="Spawn heading"
content="This controls the direction the unit will face when spanwned. This is important for units that take longer to change direction, like ships. Air units and helicopters will enter a orbit with its major axis aligned with the spawn heading. Drag the compass to change the heading."
/>
)}
tooltipRelativeToParent={true}
/>
<div className={`relative mr-3 h-[60px] w-[60px]`}>
@ -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
`}
>
<div className="my-auto text-sm text-white">Quick access: </div>
<div className="my-auto text-white">Quick access: </div>
<OlStringInput
onChange={(e) => {
setQuickAccessName(e.target.value);
@ -595,11 +655,19 @@ export function UnitSpawnMenu(props: {
/>
<OlStateButton
onClick={() => {
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={() => (
<OlExpandingTooltip
title="Add this spawn to quick access"
content="Enter a name and click on the button to later be able to quickly respawn a unit with the same configuration."
/>
)}
tooltipRelativeToParent={true}
checked={key in props.starredSpawns}
icon={faStar}
></OlStateButton>
@ -611,17 +679,28 @@ export function UnitSpawnMenu(props: {
`}
>
{!props.coalition && (
<OlCoalitionToggle
coalition={spawnCoalition}
onClick={() => {
spawnCoalition === "blue" && setSpawnCoalition("neutral");
spawnCoalition === "neutral" && setSpawnCoalition("red");
spawnCoalition === "red" && setSpawnCoalition("blue");
}}
/>
<>
<div className="my-auto mr-2 text-white">Coalition:</div>
<OlCoalitionToggle
coalition={spawnCoalition}
onClick={() => {
spawnCoalition === "blue" && setSpawnCoalition("neutral");
spawnCoalition === "neutral" && setSpawnCoalition("red");
spawnCoalition === "red" && setSpawnCoalition("blue");
}}
tooltip={() => (
<OlExpandingTooltip
title="Unit coalition"
content="Toggle between blue, neutral and red coalitions. Neutral coalition must be used to employ scenic functions like miss on purpose."
/>
)}
tooltipRelativeToParent={true}
/>
</>
)}
<div className="my-auto ml-auto text-white">Units: </div>
<OlNumberInput
className={"ml-auto"}
className={"ml-2"}
value={spawnNumber}
min={minNumber}
max={maxNumber}
@ -634,6 +713,13 @@ export function UnitSpawnMenu(props: {
onChange={(ev) => {
!isNaN(Number(ev.target.value)) && setSpawnNumber(Math.max(minNumber, Math.min(maxNumber, Number(ev.target.value))));
}}
tooltip={() => (
<OlExpandingTooltip
title="Select number of units"
content="This is how many units of this type will be spawned. If more than one unit is spawned, a DCS group will be created: this means that the units will be spawned in a formation, and you will not be able to control them singularly. The entire group will act as a single entity."
/>
)}
tooltipRelativeToParent={true}
/>
</div>
@ -668,6 +754,13 @@ export function UnitSpawnMenu(props: {
leftLabel={"AGL"}
rightLabel={"ASL"}
onClick={() => setSpawnAltitudeType(!spawnAltitudeType)}
tooltip={() => (
<OlExpandingTooltip
title="Select altitude type"
content="If AGL is selected, the aircraft will be spawned at the selected altitude above the ground. If ASL is selected, the aircraft will be spawned at the selected altitude above sea level."
/>
)}
tooltipRelativeToParent={true}
/>
</div>
<OlRangeSlider
@ -688,7 +781,17 @@ export function UnitSpawnMenu(props: {
>
Role
</span>
<OlDropdown label={spawnRole} className="w-64">
<OlDropdown
label={spawnRole}
className="w-64"
tooltip={() => (
<OlExpandingTooltip
title="Role of the spawned unit"
content="This selection has no effect on what the spawned unit will actually perform, and you will have total control on it. However, it is used to filter what loadouts are available."
/>
)}
tooltipRelativeToParent={true}
>
{roles.map((role) => {
return (
<OlDropdownItem
@ -714,7 +817,17 @@ export function UnitSpawnMenu(props: {
>
Weapons
</span>
<OlDropdown label={spawnLoadoutName} className={`w-64`}>
<OlDropdown
label={spawnLoadoutName}
className={`w-64`}
tooltip={() => (
<OlExpandingTooltip
title="Unit loadout"
content="This will be the loadout that the unit will have when spawned. Look at the bottom of the page to check the exact type and number of items."
/>
)}
tooltipRelativeToParent={true}
>
{loadouts.map((loadout) => {
return (
<OlDropdownItem
@ -751,6 +864,10 @@ export function UnitSpawnMenu(props: {
<OlDropdown
label={props.blueprint?.liveries ? (props.blueprint?.liveries[spawnLiveryID]?.name ?? "Default") : "No livery"}
className={`w-64`}
tooltip={() => (
<OlExpandingTooltip title="Unit livery" content="Selects the livery of the spawned unit. This is a purely cosmetic option." />
)}
tooltipRelativeToParent={true}
>
{props.blueprint?.liveries &&
Object.keys(props.blueprint?.liveries)
@ -811,7 +928,17 @@ export function UnitSpawnMenu(props: {
>
Skill
</span>
<OlDropdown label={spawnSkill} className={`w-64`}>
<OlDropdown
label={spawnSkill}
className={`w-64`}
tooltip={() => (
<OlExpandingTooltip
title="Unit skill"
content="Selects the skill of the spawned unit. Depending on the selection, the unit will be more precise and effective at its mission. Usually a lower level is selected to generate a more forgiving mission."
/>
)}
tooltipRelativeToParent={true}
>
{["Average", "Good", "High", "Excellent"].map((skill) => {
return (
<OlDropdownItem
@ -836,47 +963,55 @@ export function UnitSpawnMenu(props: {
</OlDropdown>
</div>
<div className="my-5 flex justify-between">
<div className="my-auto flex flex-col gap-2">
<span className="text-white">Spawn heading</span>
<div className="flex gap-1 text-sm text-gray-400">
<FaQuestionCircle className={`my-auto`} /> <div className={`
my-auto
`}>Drag to change</div>
</div>
</div>
<OlNumberInput
className={"my-auto"}
min={0}
max={360}
onChange={(ev) => {
setCompassAngle(Number(ev.target.value));
}}
onDecrease={() => {
setCompassAngle(normalizeAngle(compassAngle - 1));
}}
onIncrease={() => {
setCompassAngle(normalizeAngle(compassAngle + 1));
}}
value={compassAngle}
/>
<div className={`relative mr-3 h-[60px] w-[60px]`}>
<img className="absolute" ref={compassRef} onMouseDown={handleMouseDown} src={"/images/others/arrow_background.png"}></img>
<img
className="absolute left-0"
ref={compassRef}
onMouseDown={handleMouseDown}
src={"/images/others/arrow.png"}
style={{
width: "60px",
height: "60px",
transform: `rotate(${compassAngle}deg)`,
cursor: "pointer",
}}
></img>
<div className="my-auto flex flex-col gap-2">
<span className="text-white">Spawn heading</span>
<div className="flex gap-1 text-sm text-gray-400">
<FaQuestionCircle className={`my-auto`} /> <div className={`
my-auto
`}>Drag to change</div>
</div>
</div>
<OlNumberInput
className={"my-auto"}
min={0}
max={360}
onChange={(ev) => {
setCompassAngle(Number(ev.target.value));
}}
onDecrease={() => {
setCompassAngle(normalizeAngle(compassAngle - 1));
}}
onIncrease={() => {
setCompassAngle(normalizeAngle(compassAngle + 1));
}}
value={compassAngle}
tooltip={() => (
<OlExpandingTooltip
title="Spawn heading"
content="This controls the direction the unit will face when spanwned. This is important for units that take longer to change direction, like ships. Air units and helicopters will enter a orbit with its major axis aligned with the spawn heading. Drag the compass to change the heading."
/>
)}
tooltipRelativeToParent={true}
tooltipPosition="above"
/>
<div className={`relative mr-3 h-[60px] w-[60px]`}>
<img className="absolute" ref={compassRef} onMouseDown={handleMouseDown} src={"/images/others/arrow_background.png"}></img>
<img
className="absolute left-0"
ref={compassRef}
onMouseDown={handleMouseDown}
src={"/images/others/arrow.png"}
style={{
width: "60px",
height: "60px",
transform: `rotate(${compassAngle}deg)`,
cursor: "pointer",
}}
></img>
</div>
</div>
</div>
{spawnLoadout && spawnLoadout.items.length > 0 && (
<div

View File

@ -123,6 +123,15 @@ input[type="range"]:focus::-moz-range-thumb {
}
}
@keyframes loadingBar {
0% {
width: 0;
}
100% {
width: 100%;
}
}
.bouncing-ball {
position: relative;
width: 100px;

View File

@ -7,13 +7,11 @@ import { UnitControlMenu } from "./panels/unitcontrolmenu";
import { MainMenu } from "./panels/mainmenu";
import { SideBar } from "./panels/sidebar";
import { OptionsMenu } from "./panels/optionsmenu";
import { MapHiddenTypes, MapOptions } from "../types/types";
import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, UnitControlSubState } from "../constants/constants";
import { getApp, setupApp } from "../olympusapp";
import { LoginModal } from "./modals/loginmodal";
import { MiniMapPanel } from "./panels/minimappanel";
import { MapToolBar } from "./panels/maptoolbar";
import { DrawingMenu } from "./panels/drawingmenu";
import { ControlsPanel } from "./panels/controlspanel";
import { MapContextMenu } from "./contextmenus/mapcontextmenu";
@ -23,7 +21,6 @@ import { FormationMenu } from "./panels/formationmenu";
import { ProtectionPromptModal } from "./modals/protectionpromptmodal";
import { KeybindModal } from "./modals/keybindmodal";
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
import { JTACMenu } from "./panels/jtacmenu";
import { AppStateChangedEvent, ServerStatusUpdatedEvent } from "../events";
import { GameMasterMenu } from "./panels/gamemastermenu";
import { InfoBar } from "./panels/infobar";
@ -31,23 +28,10 @@ import { HotGroupBar } from "./panels/hotgroupsbar";
import { SpawnContextMenu } from "./contextmenus/spawncontextmenu";
import { CoordinatesPanel } from "./panels/coordinatespanel";
import { RadiosSummaryPanel } from "./panels/radiossummarypanel";
import { AWACSMenu } from "./panels/awacsmenu";
import { ServerOverlay } from "./serveroverlay";
import { ImportExportModal } from "./modals/importexportmodal";
import { WarningModal } from "./modals/warningmodal";
import { ServerStatus } from "../interfaces";
export type OlympusUIState = {
mainMenuVisible: boolean;
spawnMenuVisible: boolean;
unitControlMenuVisible: boolean;
measureMenuVisible: boolean;
drawingMenuVisible: boolean;
optionsMenuVisible: boolean;
airbaseMenuVisible: boolean;
mapHiddenTypes: MapHiddenTypes;
mapOptions: MapOptions;
};
import { TrainingModal } from "./modals/trainingmodal";
export function UI() {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
@ -90,6 +74,7 @@ export function UI() {
<ImportExportModal open={appState === OlympusState.IMPORT_EXPORT} />
<LoginModal open={appState === OlympusState.LOGIN} />
<WarningModal open={appState === OlympusState.WARNING} />
<TrainingModal open={appState === OlympusState.TRAINING} />
</>
)}

View File

@ -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();
}
}