mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Added keybinding menu and server side profiles
This commit is contained in:
151
frontend/react/src/ui/modals/keybindmodal.tsx
Normal file
151
frontend/react/src/ui/modals/keybindmodal.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
import { Shortcut } from "../../shortcut/shortcut";
|
||||
import { BindShortcutRequestEvent, ShortcutsChangedEvent } from "../../events";
|
||||
|
||||
export function KeybindModal(props: { open: boolean }) {
|
||||
const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut });
|
||||
const [shortcut, setShortcut] = useState(null as null | Shortcut);
|
||||
const [code, setCode] = useState(null as null | string);
|
||||
const [shiftKey, setShiftKey] = useState(false);
|
||||
const [ctrlKey, setCtrlKey] = useState(false);
|
||||
const [altKey, setAltKey] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
ShortcutsChangedEvent.on((shortcuts) => setShortcuts({ ...shortcuts }));
|
||||
BindShortcutRequestEvent.on((shortcut) => setShortcut(shortcut));
|
||||
|
||||
document.addEventListener("keydown", (ev) => {
|
||||
setCode(ev.code);
|
||||
if (!(ev.code.indexOf("Shift") >= 0 || ev.code.indexOf("Alt") >= 0 || ev.code.indexOf("Control") >= 0)) {
|
||||
setShiftKey(ev.shiftKey);
|
||||
setAltKey(ev.altKey);
|
||||
setCtrlKey(ev.ctrlKey);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
let available: null | boolean = code ? true : null;
|
||||
let inUseShortcut: null | Shortcut = null;
|
||||
for (let id in shortcuts) {
|
||||
if (
|
||||
code === shortcuts[id].getOptions().code &&
|
||||
shiftKey == shortcuts[id].getOptions().shiftKey &&
|
||||
altKey == shortcuts[id].getOptions().altKey &&
|
||||
ctrlKey == shortcuts[id].getOptions().shiftKey
|
||||
) {
|
||||
available = false;
|
||||
inUseShortcut = shortcuts[id];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.open && (
|
||||
<>
|
||||
<Modal
|
||||
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 + " : ""}
|
||||
{shiftKey ? "Shift + " : ""}
|
||||
{altKey ? "Alt + " : ""}
|
||||
|
||||
{code}
|
||||
</div>
|
||||
<div className="text-white">
|
||||
{available === true && <div className="text-green-600">Keybind is free!</div>}
|
||||
{available === false && (
|
||||
<div>
|
||||
Keybind is already in use:{" "}
|
||||
<span
|
||||
className={`font-bold text-red-600`}
|
||||
>
|
||||
{inUseShortcut?.getOptions().label}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
{available && 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>
|
||||
)}
|
||||
<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>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}></div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,19 +4,54 @@ import { Card } from "./components/card";
|
||||
import { ErrorCallout } from "../../ui/components/olcallout";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { VERSION } from "../../olympusapp";
|
||||
import { getApp, VERSION } from "../../olympusapp";
|
||||
import { sha256 } from "js-sha256";
|
||||
import { BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMANDER } from "../../constants/constants";
|
||||
|
||||
export function LoginModal(props: {
|
||||
checkingPassword: boolean;
|
||||
loginError: boolean;
|
||||
commandMode: string | null;
|
||||
onLogin: (password: string) => void;
|
||||
onContinue: (username: string) => void;
|
||||
onBack: () => void;
|
||||
}) {
|
||||
export function LoginModal(props: {}) {
|
||||
// TODO: add warning if not in secure context and some features are disabled
|
||||
const [password, setPassword] = useState("");
|
||||
const [displayName, setDisplayName] = useState("Game Master");
|
||||
const [profileName, setProfileName] = useState("Game Master");
|
||||
const [checkingPassword, setCheckingPassword] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
const [commandMode, setCommandMode] = useState(null as null | string);
|
||||
|
||||
function checkPassword(password: string) {
|
||||
setCheckingPassword(true);
|
||||
var hash = sha256.create();
|
||||
getApp().getServerManager().setPassword(hash.update(password).hex());
|
||||
getApp()
|
||||
.getServerManager()
|
||||
.getMission(
|
||||
(response) => {
|
||||
const commandMode = response.mission.commandModeOptions.commandMode;
|
||||
try {
|
||||
[GAME_MASTER, BLUE_COMMANDER, RED_COMMANDER].includes(commandMode) ? setCommandMode(commandMode) : setLoginError(true);
|
||||
} catch {
|
||||
setLoginError(true);
|
||||
}
|
||||
setCheckingPassword(false);
|
||||
},
|
||||
() => {
|
||||
setLoginError(true);
|
||||
setCheckingPassword(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
getApp().getServerManager().setUsername(profileName);
|
||||
getApp().getServerManager().startUpdate();
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
|
||||
/* Set the profile name */
|
||||
getApp().setProfile(profileName);
|
||||
/* If no profile exists already with that name, create it from scratch from the defaults */
|
||||
if (getApp().getProfile() === null)
|
||||
getApp().saveProfile();
|
||||
/* Load the profile */
|
||||
getApp().loadProfile();
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -62,7 +97,7 @@ export function LoginModal(props: {
|
||||
max-lg:w-[100%]
|
||||
`}
|
||||
>
|
||||
{!props.checkingPassword ? (
|
||||
{!checkingPassword ? (
|
||||
<>
|
||||
<div className="flex flex-col items-start">
|
||||
<div
|
||||
@@ -115,9 +150,9 @@ export function LoginModal(props: {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!props.loginError ? (
|
||||
{!loginError ? (
|
||||
<>
|
||||
{props.commandMode === null ? (
|
||||
{commandMode === null ? (
|
||||
<>
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<label
|
||||
@@ -148,7 +183,7 @@ export function LoginModal(props: {
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onLogin(password)}
|
||||
onClick={() => checkPassword(password)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm
|
||||
@@ -172,24 +207,19 @@ export function LoginModal(props: {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col items-start
|
||||
gap-2
|
||||
`}
|
||||
>
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<label
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Set display name
|
||||
Set profile name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
onChange={(ev) => setDisplayName(ev.currentTarget.value)}
|
||||
onChange={(ev) => setProfileName(ev.currentTarget.value)}
|
||||
className={`
|
||||
block w-full max-w-80 rounded-lg border
|
||||
border-gray-300 bg-gray-50 p-2.5 text-sm
|
||||
@@ -201,14 +231,17 @@ export function LoginModal(props: {
|
||||
focus:border-blue-500 focus:ring-blue-500
|
||||
`}
|
||||
placeholder="Enter display name"
|
||||
value={displayName}
|
||||
value={profileName}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
The profile name you choose determines what keybinds/groups/options get loaded and edited. Be careful!
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onContinue(displayName)}
|
||||
onClick={() => 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
|
||||
@@ -249,11 +282,7 @@ export function LoginModal(props: {
|
||||
title="Server could not be reached"
|
||||
description="The Olympus Server at this address could not be reached. 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
|
||||
`}
|
||||
>
|
||||
<div className={`text-sm font-medium text-gray-200`}>
|
||||
Still having issues? See our
|
||||
<a
|
||||
href=""
|
||||
|
||||
@@ -2,100 +2,110 @@ import React from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Unit } from "../../unit/unit";
|
||||
import { FaLock } from "react-icons/fa6";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { OlympusState } from "../../constants/constants";
|
||||
|
||||
export function ProtectionPrompt(props: {}) {
|
||||
export function ProtectionPrompt(props: { open: boolean }) {
|
||||
return (
|
||||
<Modal
|
||||
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 flex-col items-start gap-2`}>
|
||||
<span
|
||||
<>
|
||||
{props.open && (
|
||||
<>
|
||||
<Modal
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
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
|
||||
`}
|
||||
>
|
||||
Your selection contains protected units, are you sure you want to continue?
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
Pressing "Continue" will cause all DCS controlled units in the current selection to abort their mission and start following Olympus commands only.
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
If you are trying to delete a human player unit, they will be killed and de-slotted. Be careful!
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
To disable this warning, press on the{" "}
|
||||
<span
|
||||
className={`
|
||||
inline-block translate-y-3 rounded-full border-[1px]
|
||||
border-gray-900 bg-red-500 p-2 text-olympus-900
|
||||
`}
|
||||
>
|
||||
<FaLock />
|
||||
</span>{" "}
|
||||
button
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
getApp().getUnitsManager().executeProtectionCallback();
|
||||
getApp().setState(OlympusState.UNIT_CONTROL);
|
||||
}}
|
||||
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 className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.UNIT_CONTROL)}
|
||||
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>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className="flex h-full w-full flex-col gap-12">
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Your selection contains protected units, are you sure you want to continue?
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
Pressing "Continue" will cause all DCS controlled units in the current selection to abort their mission and start following Olympus commands
|
||||
only.
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
If you are trying to delete a human player unit, they will be killed and de-slotted. Be careful!
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
To disable this warning, press on the{" "}
|
||||
<span
|
||||
className={`
|
||||
inline-block translate-y-3 rounded-full border-[1px]
|
||||
border-gray-900 bg-red-500 p-2 text-olympus-900
|
||||
`}
|
||||
>
|
||||
<FaLock />
|
||||
</span>{" "}
|
||||
button
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
getApp().getUnitsManager().executeProtectionCallback();
|
||||
getApp().setState(OlympusState.UNIT_CONTROL);
|
||||
}}
|
||||
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 className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().setState(OlympusState.UNIT_CONTROL)}
|
||||
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>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}></div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function Header() {
|
||||
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
|
||||
MapSourceChangedEvent.on((source) => setMapSource(source));
|
||||
ConfigLoadedEvent.on((config: OlympusConfig) => {
|
||||
var sources = Object.keys(config.mapMirrors).concat(Object.keys(config.mapLayers));
|
||||
var sources = Object.keys(config.frontend.mapMirrors).concat(Object.keys(config.frontend.mapLayers));
|
||||
setMapSources(sources);
|
||||
});
|
||||
CommandModeOptionsChangedEvent.on((commandModeOptions) => {
|
||||
@@ -112,9 +112,7 @@ export function Header() {
|
||||
</div>
|
||||
{commandModeOptions.commandMode === BLUE_COMMANDER && (
|
||||
<div
|
||||
className={`
|
||||
flex h-full rounded-md bg-blue-600 px-4 text-white
|
||||
`}
|
||||
className={`flex h-full rounded-md bg-blue-600 px-4 text-white`}
|
||||
>
|
||||
<span className="my-auto font-bold">BLUE Commander ({commandModeOptions.spawnPoints.blue} points)</span>
|
||||
</div>
|
||||
|
||||
@@ -4,14 +4,28 @@ import { OlCheckbox } from "../components/olcheckbox";
|
||||
import { OlRangeSlider } from "../components/olrangeslider";
|
||||
import { OlNumberInput } from "../components/olnumberinput";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
|
||||
import { MapOptionsChangedEvent } from "../../events";
|
||||
import { MAP_OPTIONS_DEFAULTS, OlympusState, OptionsSubstate } from "../../constants/constants";
|
||||
import { BindShortcutRequestEvent, MapOptionsChangedEvent, ShortcutsChangedEvent } from "../../events";
|
||||
import { OlAccordion } from "../components/olaccordion";
|
||||
import { Shortcut } from "../../shortcut/shortcut";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
|
||||
const enum Accordion {
|
||||
NONE,
|
||||
BINDINGS,
|
||||
MAP_OPTIONS,
|
||||
CAMERA_PLUGIN,
|
||||
}
|
||||
|
||||
export function OptionsMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
|
||||
const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut });
|
||||
const [openAccordion, setOpenAccordion] = useState(Accordion.NONE);
|
||||
const [filterString, setFilterString] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
|
||||
ShortcutsChangedEvent.on((shortcuts) => setShortcuts({ ...shortcuts }));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -22,226 +36,201 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("showUnitLabels", !mapOptions.showUnitLabels);
|
||||
}}
|
||||
<OlAccordion
|
||||
onClick={() =>
|
||||
setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.BINDINGS: Accordion.NONE )
|
||||
}
|
||||
open={openAccordion === Accordion.BINDINGS}
|
||||
title="Key bindings"
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitLabels} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Unit Labels</span>
|
||||
<kbd
|
||||
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
|
||||
<div
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
flex max-h-[450px] flex-col gap-1 overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
L
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("showUnitsEngagementRings", !mapOptions.showUnitsEngagementRings);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitsEngagementRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Threat Rings</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
Q
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("showUnitsAcquisitionRings", !mapOptions.showUnitsAcquisitionRings);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitsAcquisitionRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Detection rings</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
E
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("showUnitTargets", !mapOptions.showUnitTargets);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitTargets} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Detection lines</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
F
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("hideUnitsShortRangeRings", !mapOptions.hideUnitsShortRangeRings);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.hideUnitsShortRangeRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Hide Short range Rings</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
R
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("keepRelativePositions", !mapOptions.keepRelativePositions);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.keepRelativePositions} onChange={() => {}}></OlCheckbox>
|
||||
<span>Keep units relative positions</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
P
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("hideGroupMembers", !mapOptions.hideGroupMembers);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.hideGroupMembers} onChange={() => {}}></OlCheckbox>
|
||||
<span>Hide Group members</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
G
|
||||
</kbd>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer gap-4
|
||||
p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap);
|
||||
}}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showMinimap} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show minimap</span>
|
||||
<kbd
|
||||
className={`
|
||||
ml-auto rounded-lg border border-gray-200 bg-gray-100 px-2 py-1.5
|
||||
text-xs font-semibold text-gray-800
|
||||
dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100
|
||||
`}
|
||||
>
|
||||
?
|
||||
</kbd>
|
||||
</div>
|
||||
{Object.entries(shortcuts)
|
||||
.filter(([id, shortcut]) => shortcut.getOptions().label.toLowerCase().indexOf(filterString.toLowerCase()) >= 0)
|
||||
.map(([id, shortcut]) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
group relative mr-2 flex cursor-pointer select-none
|
||||
items-center justify-between rounded-sm px-2 py-2 text-sm
|
||||
dark:text-gray-300 dark:hover:bg-olympus-500
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().setState(OlympusState.OPTIONS, OptionsSubstate.KEYBIND);
|
||||
BindShortcutRequestEvent.dispatch(shortcut);
|
||||
}}
|
||||
>
|
||||
<span>{shortcut.getOptions().label}</span>
|
||||
<span>
|
||||
{shortcut.getOptions().altKey ? "Alt + " : ""}
|
||||
{shortcut.getOptions().ctrlKey ? "Ctrl + " : ""}
|
||||
{shortcut.getOptions().shiftKey ? "Shift + " : ""}
|
||||
{shortcut.getOptions().code}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
|
||||
|
||||
<hr className={`
|
||||
<OlAccordion onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.MAP_OPTIONS: Accordion.NONE )} open={openAccordion === Accordion.MAP_OPTIONS} title="Map options">
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer
|
||||
gap-4 p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("showUnitLabels", !mapOptions.showUnitLabels)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitLabels} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Unit Labels</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer
|
||||
gap-4 p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("showUnitsEngagementRings", !mapOptions.showUnitsEngagementRings)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitsEngagementRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Threat Rings</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer
|
||||
gap-4 p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("showUnitsAcquisitionRings", !mapOptions.showUnitsAcquisitionRings)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitsAcquisitionRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Detection rings</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer
|
||||
gap-4 p-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("showUnitTargets", !mapOptions.showUnitTargets)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showUnitTargets} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show Detection lines</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row gap-4 rounded-md justify-content
|
||||
cursor-pointer p-2 text-sm
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("hideUnitsShortRangeRings", !mapOptions.hideUnitsShortRangeRings)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.hideUnitsShortRangeRings} onChange={() => {}}></OlCheckbox>
|
||||
<span>Hide Short range Rings</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row gap-4 rounded-md justify-content
|
||||
cursor-pointer p-2 text-sm
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("keepRelativePositions", !mapOptions.keepRelativePositions)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.keepRelativePositions} onChange={() => {}}></OlCheckbox>
|
||||
<span>Keep units relative positions</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row gap-4 rounded-md justify-content
|
||||
cursor-pointer p-2 text-sm
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("hideGroupMembers", !mapOptions.hideGroupMembers)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.hideGroupMembers} onChange={() => {}}></OlCheckbox>
|
||||
<span>Hide Group members</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row gap-4 rounded-md justify-content
|
||||
cursor-pointer p-2 text-sm
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap)}
|
||||
>
|
||||
<OlCheckbox checked={mapOptions.showMinimap} onChange={() => {}}></OlCheckbox>
|
||||
<span>Show minimap</span>
|
||||
</div>
|
||||
</OlAccordion>
|
||||
|
||||
<OlAccordion onClick={() => setOpenAccordion(openAccordion === Accordion.NONE ? Accordion.CAMERA_PLUGIN: Accordion.NONE )} open={openAccordion === Accordion.CAMERA_PLUGIN} title="Camera plugin options">
|
||||
<hr
|
||||
className={`
|
||||
m-2 my-1 w-auto border-[1px] bg-gray-700
|
||||
dark:border-olympus-500
|
||||
`}></hr>
|
||||
<div className={`
|
||||
`}
|
||||
></hr>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center items-start justify-between gap-2 p-2
|
||||
`}>
|
||||
<div className="flex flex-col">
|
||||
<span className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}>DCS Camera Zoom Scaling</span>
|
||||
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
onChange={(ev) => {
|
||||
getApp().getMap().setOption("cameraPluginRatio", parseInt(ev.target.value))
|
||||
}}
|
||||
value={mapOptions.cameraPluginRatio}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<div className={`
|
||||
flex flex-col content-center items-start justify-between gap-2 p-2
|
||||
`}>
|
||||
<span className={`
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}>DCS Camera Port</span>
|
||||
<div className="flex">
|
||||
<OlNumberInput
|
||||
value={mapOptions.cameraPluginPort}
|
||||
min={0}
|
||||
max={9999}
|
||||
onDecrease={() => { getApp().getMap().setOption("cameraPluginPort", mapOptions.cameraPluginPort - 1) }}
|
||||
onIncrease={() => { getApp().getMap().setOption("cameraPluginPort", mapOptions.cameraPluginPort + 1) }}
|
||||
onChange={(ev) => { getApp().getMap().setOption("cameraPluginPort", ev.target.value)}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
DCS Camera Zoom Scaling
|
||||
</span>
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
onChange={(ev) => getApp().getMap().setOption("cameraPluginRatio", parseInt(ev.target.value))}
|
||||
value={mapOptions.cameraPluginRatio}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center items-start justify-between gap-2 p-2
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
DCS Camera Port
|
||||
</span>
|
||||
<div className="flex">
|
||||
<OlNumberInput
|
||||
value={mapOptions.cameraPluginPort}
|
||||
min={0}
|
||||
max={9999}
|
||||
onDecrease={() =>
|
||||
getApp()
|
||||
.getMap()
|
||||
.setOption("cameraPluginPort", mapOptions.cameraPluginPort - 1)
|
||||
}
|
||||
onIncrease={() =>
|
||||
getApp()
|
||||
.getMap()
|
||||
.setOption("cameraPluginPort", mapOptions.cameraPluginPort + 1)
|
||||
}
|
||||
onChange={(ev) => getApp().getMap().setOption("cameraPluginPort", ev.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OlAccordion>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -9,29 +9,25 @@ import { SideBar } from "./panels/sidebar";
|
||||
import { OptionsMenu } from "./panels/optionsmenu";
|
||||
import { MapHiddenTypes, MapOptions } from "../types/types";
|
||||
import {
|
||||
BLUE_COMMANDER,
|
||||
GAME_MASTER,
|
||||
MAP_OPTIONS_DEFAULTS,
|
||||
NO_SUBSTATE,
|
||||
OlympusState,
|
||||
OlympusSubState,
|
||||
RED_COMMANDER,
|
||||
OptionsSubstate,
|
||||
UnitControlSubState,
|
||||
} from "../constants/constants";
|
||||
import { getApp, setupApp } from "../olympusapp";
|
||||
import { LoginModal } from "./modals/login";
|
||||
import { sha256 } from "js-sha256";
|
||||
|
||||
import { MiniMapPanel } from "./panels/minimappanel";
|
||||
import { UnitControlBar } from "./panels/unitcontrolbar";
|
||||
import { DrawingMenu } from "./panels/drawingmenu";
|
||||
import { ControlsPanel } from "./panels/controlspanel";
|
||||
import { MapContextMenu } from "./contextmenus/mapcontextmenu";
|
||||
import { AirbaseMenu } from "./panels/airbasemenu";
|
||||
import { Airbase } from "../mission/airbase";
|
||||
import { AudioMenu } from "./panels/audiomenu";
|
||||
import { FormationMenu } from "./panels/formationmenu";
|
||||
import { Unit } from "../unit/unit";
|
||||
import { ProtectionPrompt } from "./modals/protectionprompt";
|
||||
import { KeybindModal } from "./modals/keybindmodal";
|
||||
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
|
||||
import { JTACMenu } from "./panels/jtacmenu";
|
||||
import { AppStateChangedEvent, MapOptionsChangedEvent } from "../events";
|
||||
@@ -54,10 +50,6 @@ export type OlympusUIState = {
|
||||
export function UI() {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
|
||||
|
||||
const [checkingPassword, setCheckingPassword] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
const [commandMode, setCommandMode] = useState(null as null | string);
|
||||
|
||||
useEffect(() => {
|
||||
AppStateChangedEvent.on((state, subState) => {
|
||||
@@ -66,34 +58,7 @@ export function UI() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
function checkPassword(password: string) {
|
||||
setCheckingPassword(true);
|
||||
var hash = sha256.create();
|
||||
getApp().getServerManager().setPassword(hash.update(password).hex());
|
||||
getApp()
|
||||
.getServerManager()
|
||||
.getMission(
|
||||
(response) => {
|
||||
const commandMode = response.mission.commandModeOptions.commandMode;
|
||||
try {
|
||||
[GAME_MASTER, BLUE_COMMANDER, RED_COMMANDER].includes(commandMode) ? setCommandMode(commandMode) : setLoginError(true);
|
||||
} catch {
|
||||
setLoginError(true);
|
||||
}
|
||||
setCheckingPassword(false);
|
||||
},
|
||||
() => {
|
||||
setLoginError(true);
|
||||
setCheckingPassword(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function connect(username: string) {
|
||||
getApp().getServerManager().setUsername(username);
|
||||
getApp().getServerManager().startUpdate();
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -111,36 +76,25 @@ export function UI() {
|
||||
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
|
||||
`}></div>
|
||||
<LoginModal
|
||||
onLogin={(password) => {
|
||||
checkPassword(password);
|
||||
}}
|
||||
onContinue={(username) => {
|
||||
connect(username);
|
||||
}}
|
||||
onBack={() => {
|
||||
setCommandMode(null);
|
||||
}}
|
||||
checkingPassword={checkingPassword}
|
||||
loginError={loginError}
|
||||
commandMode={commandMode}
|
||||
|
||||
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION && (
|
||||
<>
|
||||
<div className={`
|
||||
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
|
||||
`}></div>
|
||||
<ProtectionPrompt />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ProtectionPrompt open={appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION} />
|
||||
<KeybindModal open={appState === OlympusState.OPTIONS && appSubState === OptionsSubstate.KEYBIND} />
|
||||
|
||||
<div id="map-container" className="z-0 h-full w-screen" />
|
||||
<MainMenu open={appState === OlympusState.MAIN_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
<SpawnMenu open={appState === OlympusState.SPAWN} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
<OptionsMenu open={appState === OlympusState.OPTIONS} onClose={() => getApp().setState(OlympusState.IDLE)}/>
|
||||
<OptionsMenu open={appState === OlympusState.OPTIONS} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
|
||||
<UnitControlMenu
|
||||
open={appState === OlympusState.UNIT_CONTROL && ![UnitControlSubState.FORMATION, UnitControlSubState.UNIT_EXPLOSION_MENU].includes(appSubState as UnitControlSubState)}
|
||||
open={
|
||||
appState === OlympusState.UNIT_CONTROL &&
|
||||
![UnitControlSubState.FORMATION, UnitControlSubState.UNIT_EXPLOSION_MENU].includes(appSubState as UnitControlSubState)
|
||||
}
|
||||
onClose={() => getApp().setState(OlympusState.IDLE)}
|
||||
/>
|
||||
<FormationMenu
|
||||
@@ -149,11 +103,14 @@ export function UI() {
|
||||
/>
|
||||
|
||||
<DrawingMenu open={appState === OlympusState.DRAW} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)}/>
|
||||
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
<AudioMenu open={appState === OlympusState.AUDIO} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
<GameMasterMenu open={appState === OlympusState.GAME_MASTER} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
|
||||
<UnitExplosionMenu open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_EXPLOSION_MENU} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
<UnitExplosionMenu
|
||||
open={appState === OlympusState.UNIT_CONTROL && appSubState === UnitControlSubState.UNIT_EXPLOSION_MENU}
|
||||
onClose={() => getApp().setState(OlympusState.IDLE)}
|
||||
/>
|
||||
<JTACMenu open={appState === OlympusState.JTAC} onClose={() => getApp().setState(OlympusState.IDLE)} />
|
||||
|
||||
<MiniMapPanel />
|
||||
|
||||
Reference in New Issue
Block a user