Merge remote-tracking branch 'origin/release-candidate' into features/redgreen-unit

This commit is contained in:
MarcoJayUsai
2025-03-21 16:47:47 +01:00
82 changed files with 3832 additions and 1048 deletions

View File

@@ -73,7 +73,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
}
return (
<Menu title={airbase?.getName() ?? "No airbase selected"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
<Menu title={airbase?.getName() ?? "No airbase selected"} open={props.open} onClose={props.onClose} showBackButton={false}>
<div
className={`
flex flex-col gap-2 font-normal text-gray-800
@@ -117,10 +117,9 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
<div key={idx}>
{Object.keys(runway.headings[0]).map((runwayName) => {
return (
<div
key={`${idx}-${runwayName}`}
className={`flex w-full justify-between`}
>
<div key={`${idx}-${runwayName}`} className={`
flex w-full justify-between
`}>
<span>
{" "}
<span className="text-gray-400">RWY</span> {runwayName}
@@ -213,6 +212,9 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
/>
);
})}
{filteredBlueprints.filter((blueprint) => blueprint.category === "aircraft").length === 0 && (
<span className={`text-gray-400`}>No aircraft available</span>
)}
</div>
</OlAccordion>
<OlAccordion
@@ -264,6 +266,9 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
/>
);
})}
{filteredBlueprints.filter((blueprint) => blueprint.category === "helicopter").length === 0 && (
<span className={`text-gray-400`}>No helicopter available</span>
)}
</div>
</OlAccordion>
</div>

View File

@@ -116,7 +116,82 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
const lineDistance = (paddingRight - 40) / lineCounters[lineCounters.length - 1];
return (
<Menu title="Audio menu" open={props.open} showBackButton={false} onClose={props.onClose}>
<Menu
title="Audio menu"
open={props.open}
showBackButton={false}
onClose={props.onClose}
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">Audio menu</h2>
<div>
The audio menu allows you to add and manage audio sources, connect them to unit loudspeakers and radios, and to tune radio frequencies using the SRS integration. In other words, it allows you to communicate over SRS without the need of using the SRS client.
</div>
<div>
Because of the limitations of the browser, you need to enable the audio backend by clicking on the volume icon in the navigation header. Moreover, you need to allow the browser to access your microphone and speakers. It may take a couple of seconds for the audio backend to start.
</div>
<div className="text-red-500">
For security reasons, the audio backend will only work if the page is served over HTTPS.
</div>
<h2 className="my-4 font-bold">Managing the audio backend</h2>
<div>
You can select the input and output devices for the audio backend. The input device is the microphone that will be used to transmit your voice. The output device is the speaker that will be used to play the audio from the other players.
</div>
<div>
You can also select the radio coalition. This will determine the default coalition for the radios you create. If you are in command mode, you can change the radio
coalition by clicking on the coalition toggle button. This will have no effect if radio coalition enforcing is not enabled in the SRS server.
</div>
<h2 className="my-4 font-bold">Creating audio sources</h2>
<div>
You can add audio sources by clicking on the "Add audio source" button. By default, a microphone and a text to speech source are created, but you can add file sources as well, which allow to play audio files such as music, prerecorded messages, or background noise, such as gunfire or engine sounds.
</div>
<div>
The text to speech generation works using the Google Cloud speech API and by default it works in English. For it to work, a valid Google Cloud API key must be installed on the Olympus backend server machine. See the backend documentation for more information. {/* TODO: put link here */}
</div>
<div>
Text to speech and file sources can be set to operate in loop mode, which will make them repeat the audio indefinitely. This is useful for background noise or music. Moreover, you can set the volume of the audio sources.
</div>
<h2 className="my-4 font-bold">Creating radios and loudspeakers</h2>
<div>
By default, two radios are created, but you can add more by clicking on the "Add radio" button. Radios can be tuned to different frequencies, and they can be set to operate in AM or FM mode. You can also set the volume of the radios, and change the balance between the left and right channels.
</div>
<div>
When a new radio is created, it will NOT be in "listen" mode, so you will need to click on the "Tune to radio" button to start listening.
</div>
<div>
You have three options to transmit on the radio:
<div>
<li>By clicking on the "Talk on frequency" button on the radio panel. This will enable continuous transmission and will remain "on" until clicked again.</li>
<li>By clicking on the "Push to talk" button located over the mouse coordinates panel, on the bottom right corner of the map.</li>
<li>By using the "Push to talk" keyboard shortcuts, which can be edited in the options menu.</li>
</div>
</div>
<div>
Loudspeakers can be used to simulate environmental sounds, like 5MC calls on the carrier, or sirens. To create a loudspeaker, click on the unit that should broadcast the sound, and then click on the "Loudspeakers" button. PTT buttons for loudspeakers operate in the same way as radios.
</div>
<div className="text-red-500">
The loudspeakers system uses the SRS integration, so it will only work if other players' SRS clients are running and connected to the same server as Olympus. Moreover, the loudspeaker system operates using the INTERCOM radio in SRS, and for the time being it will only work for those radios that have the INTERCOM radio enabled (i.e. usually multicrew aircraft).
</div>
<h2 className="my-4 font-bold">Connecting sources and radios/loudspeakers</h2>
<div>
Each source can be connected to one or more radios or loudspeakers. To connect a source to a radio or loudspeaker, click on the "+" button on the right of the source panel, then click on the equivalent button on the desired radio/loudspeaker. To disconnect a source from a radio or loudspeaker, click on the "-" button next to the radio/loudspeaker.
</div>
<div>
The connection lines will show the connections between the sources and the radios/loudspeakers. The color of the line is randomly generated and will be different for each source.
</div>
<div>
By connecting multiple sources to the same radio/loudspeaker, you can create complex audio setups, like playing background music while transmitting on the radio.
</div>
</div>
);
}}
>
<div className="flex content-center gap-4 p-4">
<div className="my-auto text-gray-400">
<FaQuestionCircle />

View File

@@ -32,7 +32,7 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
}, []);
return (
<Menu title={"AWACS Tools"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
<Menu title={"AWACS Tools"} open={props.open} onClose={props.onClose} showBackButton={false}>
<div
className={`
flex flex-col gap-4 p-4 font-normal text-gray-800

View File

@@ -1,46 +1,66 @@
import { faArrowLeft, faCircleQuestion, faClose } from "@fortawesome/free-solid-svg-icons";
import { faArrowLeft, faCircleQuestion, faClose, faEyeSlash } 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";
import { FaChevronRight } from "react-icons/fa6";
export function Menu(props: {
title: string;
open: boolean;
onClose: () => void;
canBeHidden?: boolean;
onBack?: () => void;
showBackButton?: boolean;
children?: JSX.Element | JSX.Element[];
wiki?: () => (JSX.Element | JSX.Element[]);
autohide?: boolean;
wiki?: () => JSX.Element | JSX.Element[];
wikiDisabled?: boolean;
}) {
const [hide, setHide] = useState(true);
const [wiki, setWiki] = useState(false);
if (!props.open && hide) setHide(false);
useEffect(() => {
if (props.autohide) {
if (window.innerWidth > 640) setHide(false);
if (!props.open) setHide(true);
} else {
setHide(false);
}
}, [props.open]);
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-all ol-panel-container
pointer-events-none absolute left-16 right-0 top-[58px] z-10 flex
h-[calc(100vh-58px)] 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}
>
{props.open && (
<div className="absolute flex h-full w-[30px]">
<div
className={`
pointer-events-auto my-auto flex h-[80px] w-full cursor-pointer
justify-center rounded-r-lg bg-olympus-800/90 backdrop-blur-lg
backdrop-grayscale
hover:bg-olympus-400/90
`}
onClick={() => setHide(!hide)}
>
<FaChevronRight className={`my-auto text-gray-400`} />
</div>
</div>
)}
<div
data-hide={hide}
data-canbehidden={props.canBeHidden}
className={`
pointer-events-auto h-[calc(100vh-58px)] overflow-y-auto
overflow-x-hidden backdrop-blur-lg backdrop-grayscale
transition-transform no-scrollbar
pointer-events-auto h-[calc(100vh-58px)] w-full backdrop-blur-lg
backdrop-grayscale transition-transform
dark:bg-olympus-800/90
data-[canbehidden='true']:h-[calc(100vh-58px-2rem)]
data-[hide='true']:translate-y-[calc(100vh-58px)]
data-[hide='true']:-translate-x-full
`}
>
<h5
@@ -61,12 +81,24 @@ export function Menu(props: {
/>
)}
{props.title}
{!(props.wikiDisabled === true) && (
<FontAwesomeIcon
onClick={() => setWiki(!wiki)}
icon={faCircleQuestion}
className={`
ml-auto 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
`}
/>
)}
<FontAwesomeIcon
onClick={() => setWiki(!wiki)}
icon={faCircleQuestion}
onClick={() => setHide(true)}
icon={faEyeSlash}
className={`
ml-auto flex cursor-pointer items-center justify-center rounded-md
p-2 text-lg
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
`}
@@ -82,37 +114,33 @@ export function Menu(props: {
`}
/>
</h5>
<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 cursor-pointer justify-center
bg-olympus-800/90 backdrop-blur-lg backdrop-grayscale
hover:bg-olympus-400/90
flex h-[calc(100%-3rem)] w-full flex-col
sm:flex-row
`}
onClick={() => setHide(!hide)}
>
{hide ? (
<FaChevronUp className="mx-auto my-auto text-gray-400" />
) : (
<FaChevronDown
className={`mx-auto my-auto text-gray-400`}
/>
)}
<div
data-wiki={wiki}
className={`
h-0 w-0 overflow-hidden transition-all
data-[wiki='true']:min-h-[50%] data-[wiki='true']:min-w-full
sm:data-[wiki='true']:min-h-full sm:data-[wiki='true']:min-w-[50%]
`}
>
{props.wiki ? props.wiki() : <div className={`p-4 text-gray-200`}>Work in progress</div>}
</div>
<div
data-wiki={wiki}
className={`
relative overflow-y-auto overflow-x-hidden no-scrollbar
sm:w-[400px]
`}
>
{props.children}
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { DrawSubState, MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusS
import { AppStateChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent, ShortcutsChangedEvent } from "../../events";
import { ContextActionSet } from "../../unit/contextactionset";
import { MapToolBar } from "./maptoolbar";
import { CoordinatesPanel } from "./coordinatespanel";
export function ControlsPanel(props: {}) {
const [controls, setControls] = useState(
@@ -200,12 +201,12 @@ export function ControlsPanel(props: {}) {
className={`
absolute right-[0px] top-16
${mapOptions.showMinimap ? `bottom-[233px]` : `bottom-[65px]`}
pointer-events-none flex w-[310px] flex-col items-center justify-between
pointer-events-none flex w-[288px] flex-col items-center justify-between
gap-1 p-3 text-sm
`}
>
<MapToolBar />
{controls?.map((control) => {
{/*controls?.map((control) => {
return (
<div
key={control.text}
@@ -246,7 +247,7 @@ export function ControlsPanel(props: {}) {
</div>
</div>
);
})}
})*/}
</div>
);
}

View File

@@ -12,6 +12,7 @@ export function CoordinatesPanel(props: {}) {
const [elevation, setElevation] = useState(0);
const [bullseyes, setBullseyes] = useState(null as null | { [name: string]: Bullseye });
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
const [open, setOpen] = useState(true);
useEffect(() => {
MouseMovedEvent.on((latlng, elevation) => {
@@ -27,18 +28,30 @@ export function CoordinatesPanel(props: {}) {
return (
<div
className={`
absolute bottom-[20px] right-[310px] flex min-h-12 w-[380px] flex-col
items-center justify-between gap-2 rounded-lg bg-gray-200 px-3 py-3
text-sm backdrop-blur-lg backdrop-grayscale
flex w-full flex-col items-center justify-between gap-2 rounded-lg
bg-gray-200 px-3 py-3 text-sm backdrop-blur-lg backdrop-grayscale
dark:bg-olympus-800/90 dark:text-gray-200
`}
onClick={() => setOpen(!open)}
>
{bullseyes && (
<div className="flex w-full items-center justify-start">
<div className="absolute right-[12px] top-[15px]">
{open ? (
<FaChevronDown className="cursor-pointer" />
) : (
<FaChevronUp
className={`cursor-pointer`}
/>
)}
</div>
{open && bullseyes && (
<div
className={`
flex w-full flex-col items-start justify-start gap-2
`}
>
<div
className={`
mr-[11px] flex min-w-64 max-w-64 items-center justify-between
gap-2
flex flex min-w-64 max-w-64 items-start justify-between gap-2
`}
>
{bullseyes[2] && (
@@ -84,18 +97,27 @@ export function CoordinatesPanel(props: {}) {
</div>
)}
<div className="flex w-full items-center justify-between">
<div
className={`
flex w-full items-center justify-between pointer-events-all
`}
>
<OlLocation className="!min-w-64 !max-w-64 bg-transparent !p-0" location={latlng} />
<span
className={`
mr-2 rounded-sm bg-white px-1 py-1 text-center font-bold
text-olympus-700
`}
>
<FaMountain />
</span>
<div className="min-w-12">{mToFt(elevation).toFixed()}ft</div>
</div>
{open && (
<div className="flex w-full items-center justify-start">
<span
className={`
mr-2 rounded-sm bg-white px-1 py-1 text-center font-bold
text-olympus-700
`}
>
<FaMountain />
</span>
<div className="min-w-12">{mToFt(elevation).toFixed()}ft</div>
</div>
)}
</div>
);
}

View File

@@ -92,7 +92,11 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
hover:scale-125 hover:text-gray-200
`}
onClick={() => {
container.setVisibility(!container.getVisibility(), true);
if (container === mainDrawingsContainer.container) {
getApp().getMap().setOption("showMissionDrawings", !getApp().getMap().getOptions().showMissionDrawings);
} else {
container.setVisibility(!container.getVisibility(), true);
}
}}
/>
<div
@@ -147,12 +151,60 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
open={props.open}
title="Draw"
onClose={props.onClose}
canBeHidden={true}
showBackButton={appSubState !== DrawSubState.NO_SUBSTATE}
onBack={() => {
getApp().getCoalitionAreasManager().setSelectedArea(null);
getApp().setState(OlympusState.DRAW, DrawSubState.NO_SUBSTATE);
}}
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">Drawing menu</h2>
<div>
The drawing menu allows you to create and manage custom drawings, such as polygons and circles, and to generate IADS (Integrated Air Defense
System) areas. Moreover, you can manage the visibility and opacity of mission drawings, i.e. drawings from the Mission Editor.
</div>
<h2 className="my-4 font-bold">Custom drawings and IADS</h2>
<div>
To create a custom drawing, click on the 'Add polygon' or 'Add circle' buttons, then click on the map to add polygons or to move the drawing.
Double-click on the map to finish your creation. You can then edit the drawing by clicking on it. You can also move it up or down in the list, or
delete it.
</div>
<div>
You can change the name and the coalition of the area. You can also generate an IADS area by selecting the types, eras, and ranges of units you
want to include in the area. You can also set the density and distribution of the IADS. If you check the 'Force coalition appropriate units' box,
the IADS will only include units that are appropriate for the coalition of the area (e.g. Hawk SAMs for {""}
<span className="text-blue-500">blue</span> and SA-6 SAMs for{" "}
<span
className={`text-red-500`}
>
red
</span>
).
</div>
<div>
The IADS generator will create a random distribution of units in the area, based on the density and distribution you set. Units will be
concentrated around cities, and airbases that belong to the selected coalition.
</div>
<h2 className="my-4 font-bold">Mission drawings</h2>
<div>
You can manage the visibility and opacity of mission drawings by clicking on the eye icon. Moreover, you can change the opacity of the drawing by
using the slider. You can also hide or show all the drawings in a container.
</div>
<div>
You can search for a specific drawing by typing in the search bar. The search is case-insensitive and will match any part of the drawing name.
</div>
<div>
Any change you make is persistent and will be saved for the next time you reload Olympus, as long as the DCS mission was not restarted.
</div>
</div>
);
}}
>
<>
{appState === OlympusState.DRAW && appSubState === DrawSubState.NO_SUBSTATE && (
@@ -297,12 +349,10 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
bg-olympus-600 p-5
`}
>
<div
className={`border-b-2 border-b-olympus-100 pb-4 text-gray-300`}
>
Automatic IADS generation
</div>
<OlDropdown className="" label="Units types">
<div className={`
border-b-2 border-b-olympus-100 pb-4 text-gray-300
`}>Automatic IADS generation</div>
<OlDropdown className="" label="Units types" disableAutoClose={true}>
{types.map((type, idx) => {
if (!(type in typesSelection)) {
typesSelection[type] = true;
@@ -323,7 +373,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
);
})}
</OlDropdown>
<OlDropdown className="" label="Units eras">
<OlDropdown className="" label="Units eras" disableAutoClose={true}>
{eras.map((era) => {
if (!(era in erasSelection)) {
erasSelection[era] = true;
@@ -344,7 +394,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
);
})}
</OlDropdown>
<OlDropdown className="" label="Units ranges">
<OlDropdown className="" label="Units ranges" disableAutoClose={true}>
{["Short range", "Medium range", "Long range"].map((range) => {
if (!(range in rangesSelection)) {
rangesSelection[range] = true;

View File

@@ -30,11 +30,17 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff
type: props.effect,
explosionType,
});
else if (props.effect === "smoke")
else if (props.effect === "smoke") {
let colorName = "white";
if (smokeColor === colors.BLUE) colorName = "blue";
else if (smokeColor === colors.RED) colorName = "red";
else if (smokeColor === colors.GREEN) colorName = "green";
else if (smokeColor === colors.ORANGE) colorName = "orange";
getApp()?.getMap()?.setEffectRequestTable({
type: props.effect,
smokeColor,
smokeColor: colorName,
});
}
getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_EFFECT);
} else {
if (appState === OlympusState.SPAWN && appSubState === SpawnSubState.SPAWN_EFFECT) getApp().setState(OlympusState.IDLE);
@@ -129,7 +135,13 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff
else if (explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", props.latlng);
getApp().getMap().addExplosionMarker(props.latlng);
} else if (props.effect === "smoke") {
getApp().getServerManager().spawnSmoke(smokeColor, props.latlng);
/* Find the name of the color */
let colorName = "white";
if (smokeColor === colors.BLUE) colorName = "blue";
else if (smokeColor === colors.RED) colorName = "red";
else if (smokeColor === colors.GREEN) colorName = "green";
else if (smokeColor === colors.ORANGE) colorName = "orange";
getApp().getServerManager().spawnSmoke(colorName, props.latlng);
getApp()
.getMap()
.addSmokeMarker(props.latlng, smokeColor ?? colors.WHITE);

View File

@@ -6,6 +6,9 @@ import { getApp } from "../../olympusapp";
import { ServerStatus } from "../../interfaces";
import { CommandModeOptionsChangedEvent, ServerStatusUpdatedEvent } from "../../events";
import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, ERAS_ORDER, GAME_MASTER, RED_COMMANDER } from "../../constants/constants";
import { secondsToTimeString } from "../../other/utils";
import { FaQuestionCircle } from "react-icons/fa";
import { FaMinus, FaPlus } from "react-icons/fa6";
export function GameMasterMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS);
@@ -21,53 +24,131 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil
}, []);
return (
<Menu title="Game Master options" open={props.open} showBackButton={false} onClose={props.onClose}>
<Menu title="Game Master options" open={props.open} showBackButton={false} onClose={props.onClose} 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">Game Master menu</h2>
<div>
The Game Master menu allows the Game Master to set up the game session for the Real Time Strategy game mode of DCS Olympus.
</div>
<div>
In this mode, commanders can play against eachother in a real-time strategy game, where they can spawn a limited amount of units. Each commander can only control units belonging to their coalition. Moreover, they can only see enemy units if detected, so proper placement of radars is crucial.
</div>
<div>
The Game Master can set up the game session by restricting the unit spawns, setting the setup time, and restricting the eras of the units that can be spawned. Moreover, the Game Master can set the amount of spawn points available for each coalition.
</div>
<div>
During the setup time, commanders can prepare the battlefield. As long as they have sufficient spawn points, they can place units anywhere on the map. After the setup time ends, the game starts and the restrictions are enforced.
</div>
<div>
When restrictions are enforced, commanders will no longer be able to spawn ground units, and air units can only be spawned from airfields.
</div>
<div>
There are multiple additional modes of play. You can disable the spawn restrictions to allow commanders to spawn units freely, but can only see detected units, or you can set the spawn points to 0 to disable unit spawns entirely and force commanders to only use the units they have at the start of the game or that you provide.
</div>
</div>
);
}}>
<div
className={`
flex flex-col gap-2 p-5 font-normal text-gray-800
dark:text-white
`}
>
You are operating as:
{commandModeOptions.commandMode === GAME_MASTER && (
<div
className={`
w-full rounded-md bg-olympus-400 p-2 text-center font-bold
`}
>
GAME MASTER
</div>
)}
{commandModeOptions.commandMode === BLUE_COMMANDER && <div className={`
w-full rounded-md bg-blue-600 p-2 text-center font-bold
`}>BLUE COMMANDER</div>}
{commandModeOptions.commandMode === RED_COMMANDER && <div className={`
w-full rounded-md bg-red-700 p-2 text-center font-bold
`}>RED COMMANDER</div>}
{serverStatus.elapsedTime > currentSetupTime && (
<div
className={`
w-full rounded-md bg-orange-600 p-2 text-center font-bold
`}
>
Setup time has ended
</div>
)}
{serverStatus.elapsedTime <= currentSetupTime && (
<div
className={`
w-full rounded-md bg-green-700 p-2 text-center font-bold
`}
>
SETUP ends in {(currentSetupTime - serverStatus.elapsedTime)?.toFixed()} seconds
{commandModeOptions.restrictSpawns ? (
<>
<div className="mb-4 flex content-center gap-4">
<div className="my-auto text-gray-400">
<FaQuestionCircle />
</div>
<div className="text-sm text-gray-400">
Unit spawns are restricted. During the SETUP phase, commanders can spawn units according to the settings below. After the SETUP phase ends,
ground/navy units and air spawns are disabled, and commanders can spawn aircraft/helicopters only from airfields.
</div>
</div>
<div className="flex">
{commandModeOptions.commandMode === GAME_MASTER && (
<button
className={`
h-10 rounded-s-lg bg-gray-100 p-3
dark:bg-gray-700 dark:hover:bg-gray-600
dark:focus:ring-blue-700
focus:outline-none focus:ring-2 focus:ring-gray-100
hover:bg-gray-200
`}
onClick={() => {
const newCommandModeOptions = { ...commandModeOptions };
newCommandModeOptions.setupTime = Math.max(serverStatus.elapsedTime, newCommandModeOptions.setupTime - 60);
if (commandModeOptions.commandMode !== GAME_MASTER) return;
setCommandModeOptions(newCommandModeOptions);
setCurrentSetupTime(newCommandModeOptions.setupTime);
getApp().getServerManager().setCommandModeOptions(newCommandModeOptions);
}}
>
<FaMinus className="my-auto" />
</button>
)}
<div className={`
relative z-[-1] flex h-10 w-[360px] bg-olympus-600
`}>
<div
className={`
absolute my-auto w-full text-center before
before:absolute before:left-0 before:z-[-1] before:h-10
before:w-full before:bg-olympus-400 before:content-['']
`}
style={{ width: `${Math.min(100, 100 - ((currentSetupTime - serverStatus.elapsedTime) / currentSetupTime) * 100)}%` }}
></div>
{currentSetupTime - serverStatus.elapsedTime > 0 ? (
<div className="mx-auto my-auto">SETUP ends in {secondsToTimeString(currentSetupTime - serverStatus.elapsedTime)}</div>
) : (
<div className="mx-auto my-auto animate-pulse">SETUP ended, restrictions active</div>
)}
</div>
{commandModeOptions.commandMode === GAME_MASTER && (
<button
className={`
h-10 rounded-e-lg bg-gray-100 p-3
dark:bg-gray-700 dark:hover:bg-gray-600
dark:focus:ring-blue-700
focus:outline-none focus:ring-2 focus:ring-gray-100
hover:bg-gray-200
`}
onClick={() => {
const newCommandModeOptions = { ...commandModeOptions };
newCommandModeOptions.setupTime = Math.max(serverStatus.elapsedTime + 60, newCommandModeOptions.setupTime + 60);
if (commandModeOptions.commandMode !== GAME_MASTER) return;
setCommandModeOptions(newCommandModeOptions);
setCurrentSetupTime(newCommandModeOptions.setupTime);
getApp().getServerManager().setCommandModeOptions(newCommandModeOptions);
}}
>
<FaPlus className="my-auto" />
</button>
)}
</div>
</>
) : (
<div className="flex content-center gap-4">
<div className="my-auto text-gray-400">
<FaQuestionCircle />
</div>
<div className="text-sm text-gray-400">
Unit spawns are NOT restricted, therefore no setup time is enforced and commanders can spawn units as desired. Only unit detection is enforced.
</div>
</div>
)}
<span className="mt-5">Options: </span>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-3">
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer
gap-4 p-2
gap-4 px-2
dark:hover:bg-olympus-400
`}
onClick={() => {
@@ -82,13 +163,13 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil
data-disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER}
className={`data-[disabled='true']:text-gray-400`}
>
Restrict unit spanws
Restrict unit spawns
</span>
</div>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer
gap-4 p-2
gap-4 px-2
dark:hover:bg-olympus-400
`}
onClick={() => {
@@ -120,7 +201,7 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil
key={era}
className={`
group flex flex-row rounded-md justify-content
cursor-pointer gap-4 p-2
cursor-pointer gap-4 px-2
dark:hover:bg-olympus-400
`}
onClick={() => {
@@ -224,56 +305,6 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil
}}
></OlNumberInput>
</div>
<div
className={`
group flex flex-row rounded-md justify-content gap-4 px-4 py-2
`}
>
<span
data-disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER}
className={`
my-auto mr-auto
data-[disabled='true']:text-gray-400
`}
>
Setup time (seconds)
</span>
<OlNumberInput
min={0}
max={6000}
value={commandModeOptions.setupTime}
onChange={(e) => {
if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return;
const newCommandModeOptions = { ...commandModeOptions };
newCommandModeOptions.setupTime = parseInt(e.target.value);
setCommandModeOptions(newCommandModeOptions);
}}
onIncrease={() => {
if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return;
const newCommandModeOptions = { ...commandModeOptions };
newCommandModeOptions.setupTime = Math.min(newCommandModeOptions.setupTime + 10, 6000);
setCommandModeOptions(newCommandModeOptions);
}}
onDecrease={() => {
if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return;
const newCommandModeOptions = { ...commandModeOptions };
newCommandModeOptions.setupTime = Math.max(newCommandModeOptions.setupTime - 10, 0);
setCommandModeOptions(newCommandModeOptions);
}}
></OlNumberInput>
</div>
<div
className={`
group flex flex-row rounded-md justify-content gap-4 px-4 py-2
`}
>
<span className="mr-auto">Elapsed time (seconds)</span>{" "}
<span
className={`w-32 text-center`}
>
{serverStatus.elapsedTime?.toFixed()}
</span>
</div>
{commandModeOptions.commandMode === GAME_MASTER && (
<button
type="button"

View File

@@ -50,6 +50,8 @@ import {
import { OlympusConfig } from "../../interfaces";
import { FaCheck, FaQuestionCircle, FaSave, FaSpinner } from "react-icons/fa";
import { OlExpandingTooltip } from "../components/olexpandingtooltip";
import { ftToM } from "../../other/utils";
import { LatLng } from "leaflet";
export function Header() {
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
@@ -64,7 +66,9 @@ export function Header() {
useEffect(() => {
HiddenTypesChangedEvent.on((hiddenTypes) => setMapHiddenTypes({ ...hiddenTypes }));
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
MapOptionsChangedEvent.on((mapOptions) => {
setMapOptions({ ...mapOptions })
});
MapSourceChangedEvent.on((source) => setMapSource(source));
ConfigLoadedEvent.on((config: OlympusConfig) => {
// Timeout needed to make sure the map configuration has updated
@@ -100,9 +104,16 @@ export function Header() {
return (
<div
className={`
z-10 flex w-full gap-4 border-gray-200 bg-gray-300 px-3 align-center
relative 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
`}
onWheel={(e) => {
if (scrollRef.current) {
if (e.deltaY > 0) (scrollRef.current as HTMLElement).scrollLeft += 100;
else (scrollRef.current as HTMLElement).scrollLeft -= 100;
}
}}
>
<img src="images/icon.png" className={`my-auto h-10 w-10 rounded-md p-0`}></img>
{!scrolledLeft && (
@@ -145,21 +156,23 @@ export function Header() {
{IP}
</div>
</div>
{savingSessionData ? (
<div className="text-white">
<FaSpinner className={`animate-spin text-2xl`} />
</div>
) : (
<div className={`relative text-white`}>
<FaFloppyDisk className={`absolute -top-3 text-2xl`} />
<FaCheck
className={`
<div className="w-8">
{savingSessionData ? (
<div className="text-white">
<FaSpinner className={`animate-spin text-2xl`} />
</div>
) : (
<div className={`relative text-white`}>
<FaFloppyDisk className={`absolute -top-3 text-2xl`} />
<FaCheck
className={`
absolute left-[9px] top-[-6px] text-2xl text-olympus-900
`}
/>
<FaCheck className={`absolute left-3 top-0 text-green-500`} />
</div>
)}
/>
<FaCheck className={`absolute left-3 top-0 text-green-500`} />
</div>
)}
</div>
</div>
{commandModeOptions.commandMode === BLUE_COMMANDER && (

View File

@@ -60,7 +60,7 @@ export function JTACMenu(props: { open: boolean; onClose: () => void; children?:
let targetPosition = (targetUnit ? targetUnit.getPosition() : targetLocation) ?? new LatLng(0, 0);
return (
<Menu title={"JTAC Tools"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
<Menu title={"JTAC Tools"} open={props.open} onClose={props.onClose} showBackButton={false}>
<div
className={`
flex flex-col gap-2 p-4 font-normal text-gray-800

View File

@@ -5,6 +5,8 @@ import { getApp } from "../../olympusapp";
import { FaChevronDown, FaChevronUp } from "react-icons/fa6";
import { MapOptionsChangedEvent, ServerStatusUpdatedEvent } from "../../events";
import { colors, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
import { CoordinatesPanel } from "./coordinatespanel";
import { RadiosSummaryPanel } from "./radiossummarypanel";
export function MiniMapPanel(props: {}) {
const [serverStatus, setServerStatus] = useState({} as ServerStatus);
@@ -54,47 +56,65 @@ export function MiniMapPanel(props: {}) {
className={`
absolute right-[10px]
${mapOptions.showMinimap ? `bottom-[188px]` : `bottom-[20px]`}
flex w-[288px] items-center justify-between
${mapOptions.showMinimap ? `rounded-t-lg` : `rounded-lg`}
h-12 bg-gray-200 px-3 text-sm backdrop-blur-lg backdrop-grayscale
dark:bg-olympus-800/90 dark:text-gray-200
flex w-[288px] cursor-pointer flex-col items-center justify-between
gap-2 text-sm backdrop-blur-lg
`}
>
{!serverStatus.connected ? (
<div className={`flex animate-pulse items-center gap-2 font-semibold`}>
<div className={`relative h-4 w-4 rounded-full bg-[#F05252]`}></div>
Server disconnected
</div>
) : serverStatus.paused ? (
<div className={`flex animate-pulse items-center gap-2 font-semibold`}>
<div className={`relative h-4 w-4 rounded-full bg-[#FF9900]`}></div>
Server paused
</div>
) : (
<>
<div className="flex gap-2 font-semibold">
FPS:
<span style={{ color: frameRateColor }} className={`font-semibold`}>
{serverStatus.frameRate}
</span>
<RadiosSummaryPanel />
<CoordinatesPanel />
<div className={`
flex h-12 w-full items-center justify-between gap-2 px-3
backdrop-grayscale
dark:bg-olympus-800/90 dark:text-gray-200
${mapOptions.showMinimap ? `rounded-t-lg` : `rounded-lg`}
`}
onClick={(ev) => {
getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap);
}}>
{!serverStatus.connected ? (
<div className={`flex animate-pulse items-center gap-2 font-semibold`}>
<div className={`relative h-4 w-4 rounded-full bg-[#F05252]`}></div>
Server disconnected
</div>
<div className="flex gap-2 font-semibold">
Load:
<span style={{ color: loadColor }} className={`font-semibold`}>
{serverStatus.load}
</span>
) : serverStatus.paused ? (
<div className={`flex animate-pulse items-center gap-2 font-semibold`}>
<div className={`relative h-4 w-4 rounded-full bg-[#FF9900]`}></div>
Server paused
</div>
<div className="flex cursor-pointer gap-2 font-semibold" onClick={() => setShowMissionTime(!showMissionTime)}>
{showMissionTime ? "MT" : "ET"}: {timeString}
</div>
<div className={`relative h-4 w-4 rounded-full bg-[#8BFF63]`}></div>
</>
)}
{mapOptions.showMinimap ? (
<FaChevronDown className="cursor-pointer" onClick={() => getApp().getMap().setOption("showMinimap", false)} />
) : (
<FaChevronUp className="cursor-pointer" onClick={() => getApp().getMap().setOption("showMinimap", true)} />
)}
) : (
<>
<div className="flex w-16 gap-1 font-semibold">
FPS:
<span style={{ color: frameRateColor }} className={`font-semibold`}>
{serverStatus.frameRate}
</span>
</div>
<div className="flex gap-1 font-semibold">
Load:
<span style={{ color: loadColor }} className={`font-semibold`}>
{serverStatus.load}
</span>
</div>
<div
className="ml-auto flex w-24 cursor-pointer gap-2 font-semibold"
onClick={(ev) => {
setShowMissionTime(!showMissionTime);
ev.stopPropagation();
}}
>
{showMissionTime ? "MT" : "ET"}: {timeString}
</div>
</>
)}
{mapOptions.showMinimap ? (
<FaChevronDown className="cursor-pointer" />
) : (
<FaChevronUp
className={`cursor-pointer`}
/>
)}
</div>
</div>
);
}

View File

@@ -9,15 +9,18 @@ import { BindShortcutRequestEvent, MapOptionsChangedEvent, ShortcutsChangedEvent
import { OlAccordion } from "../components/olaccordion";
import { Shortcut } from "../../shortcut/shortcut";
import { OlSearchBar } from "../components/olsearchbar";
import { FaTrash, FaXmark } from "react-icons/fa6";
import { FaTrash, FaUserGroup, FaXmark } from "react-icons/fa6";
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
import { FaQuestionCircle } from "react-icons/fa";
import { FaCog, FaKey, FaPlus, FaQuestionCircle } from "react-icons/fa";
import { sha256 } from "js-sha256";
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
const enum Accordion {
NONE,
BINDINGS,
MAP_OPTIONS,
CAMERA_PLUGIN,
ADMIN,
}
export function OptionsMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
@@ -25,6 +28,30 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut });
const [openAccordion, setOpenAccordion] = useState(Accordion.NONE);
const [filterString, setFilterString] = useState("");
const [admin, setAdmin] = useState(false);
const [password, setPassword] = useState("");
const checkPassword = (password: string) => {
var hash = sha256.create();
const requestOptions: RequestInit = {
method: "GET", // Specify the request method
headers: {
Authorization: "Basic " + btoa(`Admin:${hash.update(password).hex()}`),
}, // Specify the content type
};
fetch(`./admin/config`, requestOptions).then((response) => {
if (response.status === 200) {
console.log(`Admin password correct`);
getApp().setAdminPassword(password);
getApp().setState(OlympusState.ADMIN);
return response.json();
} else {
throw new Error("Admin password incorrect");
}
});
};
useEffect(() => {
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
@@ -186,18 +213,21 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
}}
>
<div className="flex flex-col gap-2">
<div className="flex content-center gap-4">
<OlCoalitionToggle onClick={() => {}} coalition={mapOptions.AWACSCoalition} />
<span className="my-auto">Coalition of unit bullseye info</span>
</div>
<div className="flex gap-1 text-sm text-gray-400">
<FaQuestionCircle className={`my-auto w-8`} />{" "}
<div
className={`my-auto ml-2`}
>
Change the coalition of the bullseye to use to provide bullseye information in the unit tooltip.
<div className="flex content-center gap-4">
<OlCoalitionToggle
onClick={() => {
mapOptions.AWACSCoalition === "blue" && getApp().getMap().setOption("AWACSCoalition", "neutral");
mapOptions.AWACSCoalition === "neutral" && getApp().getMap().setOption("AWACSCoalition", "red");
mapOptions.AWACSCoalition === "red" && getApp().getMap().setOption("AWACSCoalition", "blue");
}}
coalition={mapOptions.AWACSCoalition}
/>
<span className="my-auto">Coalition of unit bullseye info</span>
</div>
<div className="flex gap-1 text-sm text-gray-400">
<FaQuestionCircle className={`my-auto w-8`} />{" "}
<div className={`my-auto ml-2`}>Change the coalition of the bullseye to use to provide bullseye information in the unit tooltip.</div>
</div>
</div>
</div>
</div>
</OlAccordion>
@@ -207,12 +237,6 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
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={`
flex flex-col content-center items-start justify-between gap-2 p-2
@@ -270,38 +294,65 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
</div>
</OlAccordion>
<div className="mt-auto flex">
<button
type="button"
onClick={() => getApp().resetProfile()}
className={`
flex w-full content-center items-center justify-center gap-2
rounded-sm border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
text-white
dark:border-red-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
`}
>
Reset all settings
<FaXmark />
</button>
<div className="mt-auto flex flex-col gap-2 p-2">
<div className="flex content-center justify-between gap-4">
<label
className={`
text-gray-800 text-md my-auto text-nowrap
dark:text-white
`}
>
Admin password
</label>
<input
type="password"
onChange={(ev) => {
setPassword(ev.currentTarget.value);
}}
className={`
max-w-44 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>
<button
type="button"
onClick={() => getApp().resetProfile()}
onClick={() => checkPassword(password)}
className={`
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
flex content-center items-center justify-center gap-2 rounded-sm
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
text-white
dark:border-red-600 dark:bg-gray-800 dark:text-gray-400
dark:border-white 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
`}
>
Reset profile
<FaXmark />
</button>
<button
type="button"
onClick={() => getApp().resetAllProfiles()}
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-red-600 dark:bg-red-800 dark:text-gray-400
dark:hover:bg-red-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-red-800
`}
>
Reset all profiles
<FaTrash />
Open advanced settings menu
<FaCog />
</button>
</div>
</div>

View File

@@ -19,12 +19,10 @@ export function RadiosSummaryPanel(props: {}) {
{audioSinks.length > 0 && (
<div
className={`
absolute bottom-[20px] right-[700px] flex w-fit flex-col
items-center justify-between gap-2 rounded-lg bg-transparent text-sm
text-gray-200
flex w-full gap-2 rounded-lg text-sm text-gray-200
`}
>
<div className="flex w-full items-center justify-between gap-2">
<div className="flex w-full flex-wrap gap-2">
{audioSinks.filter((audioSinks) => audioSinks instanceof RadioSink).length > 0 &&
audioSinks

View File

@@ -113,7 +113,6 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
{...props}
title="Spawn menu"
showBackButton={blueprint !== null || effect !== null}
canBeHidden={true}
onBack={() => {
getApp().setState(OlympusState.SPAWN);
setBlueprint(null);
@@ -125,12 +124,19 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
<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:
<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>
@@ -140,11 +146,18 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
</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)]
`} />
<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>
);
}}
@@ -203,6 +216,9 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
/>
);
})}
{filteredBlueprints.filter((blueprint) => blueprint.category === "aircraft").length === 0 && (
<span className={`text-gray-400`}>No aircraft available</span>
)}
</div>
</OlAccordion>
<OlAccordion
@@ -255,6 +271,9 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
/>
);
})}
{filteredBlueprints.filter((blueprint) => blueprint.category === "helicopter").length === 0 && (
<span className={`text-gray-400`}>No helicopter available</span>
)}
</div>
</OlAccordion>
<OlAccordion
@@ -286,6 +305,9 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
/>
);
})}
{filteredBlueprints.filter((blueprint) => blueprint.category === "groundunit" && blueprint.type === "SAM Site").length === 0 && (
<span className={`text-gray-400`}>No SAM sites available</span>
)}
</div>
</OlAccordion>
<OlAccordion
@@ -317,6 +339,9 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
/>
);
})}
{filteredBlueprints.filter((blueprint) => blueprint.canAAA).length === 0 && <span className={`
text-gray-400
`}>No AAA unit available</span>}
</div>
</OlAccordion>
<OlAccordion
@@ -372,6 +397,9 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
/>
);
})}
{filteredBlueprints.filter((blueprint) => blueprint.category === "groundunit" && blueprint.type !== "SAM Site").length === 0 && (
<span className={`text-gray-400`}>No ground unit available</span>
)}
</div>
</OlAccordion>
<OlAccordion
@@ -424,41 +452,46 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
/>
);
})}
{filteredBlueprints.filter((blueprint) => blueprint.category === "navyunit").length === 0 && (
<span className={`text-gray-400`}>No navy unit available</span>
)}
</div>
</OlAccordion>
<OlAccordion
title="Effects (smokes, explosions etc)"
open={openAccordion == CategoryAccordion.EFFECT}
onClick={() => {
setOpenAccordion(openAccordion === CategoryAccordion.EFFECT ? CategoryAccordion.NONE : CategoryAccordion.EFFECT);
setSelectedRole(null);
setSelectedType(null);
}}
>
<div
className={`
flex max-h-[450px] flex-col gap-1 overflow-y-scroll
no-scrollbar
`}
{commandModeOptions.commandMode === GAME_MASTER && (
<OlAccordion
title="Effects (smokes, explosions etc)"
open={openAccordion == CategoryAccordion.EFFECT}
onClick={() => {
setOpenAccordion(openAccordion === CategoryAccordion.EFFECT ? CategoryAccordion.NONE : CategoryAccordion.EFFECT);
setSelectedRole(null);
setSelectedType(null);
}}
>
<OlEffectListEntry
key={"explosion"}
icon={faExplosion}
label={"Explosion"}
onClick={() => {
setEffect("explosion");
}}
/>
<OlEffectListEntry
key={"smoke"}
icon={faSmog}
label={"Smoke"}
onClick={() => {
setEffect("smoke");
}}
/>
</div>
</OlAccordion>
<div
className={`
flex max-h-[450px] flex-col gap-1 overflow-y-scroll
no-scrollbar
`}
>
<OlEffectListEntry
key={"explosion"}
icon={faExplosion}
label={"Explosion"}
onClick={() => {
setEffect("explosion");
}}
/>
<OlEffectListEntry
key={"smoke"}
icon={faSmog}
label={"Smoke"}
onClick={() => {
setEffect("smoke");
}}
/>
</div>
</OlAccordion>
)}
</div>
)}

View File

@@ -9,6 +9,7 @@ import { OlCheckbox } from "../components/olcheckbox";
import {
ROEs,
alarmStates,
UnitState,
altitudeIncrements,
emissionsCountermeasures,
maxAltitudeValues,
@@ -47,7 +48,7 @@ import {
olButtonsVisibilityOlympus,
} from "../components/olicons";
import { Coalition } from "../../types/types";
import { convertROE, deepCopyTable, ftToM, knotsToMs, mToFt, msToKnots } from "../../other/utils";
import { convertROE, deepCopyTable, ftToM, knotsToMs, mToFt, msToKnots, zeroAppend } from "../../other/utils";
import { FaChevronLeft, FaCog, FaExclamationCircle, FaGasPump, FaQuestionCircle, FaSignal, FaTag } from "react-icons/fa";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OlSearchBar } from "../components/olsearchbar";
@@ -59,9 +60,10 @@ 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 { faCog, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { OlExpandingTooltip } from "../components/olexpandingtooltip";
import { OlLocation } from "../components/ollocation";
import { OlStateButton } from "../components/olstatebutton";
export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
function initializeUnitsData() {
@@ -131,6 +133,17 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | GeneralSettings);
const [lastUpdateTime, setLastUpdateTime] = useState(0);
const [showScenicModes, setShowScenicModes] = useState(true);
const [showEngagementSettings, setShowEngagementSettings] = useState(false);
const [barrelHeight, setBarrelHeight] = useState(0);
const [muzzleVelocity, setMuzzleVelocity] = useState(0);
const [aimTime, setAimTime] = useState(0);
const [shotsToFire, setShotsToFire] = useState(0);
const [shotsBaseInterval, setShotsBaseInterval] = useState(0);
const [shotsBaseScatter, setShotsBaseScatter] = useState(0);
const [engagementRange, setEngagementRange] = useState(0);
const [targetingRange, setTargetingRange] = useState(0);
const [aimMethodRange, setAimMethodRange] = useState(0);
const [acquisitionRange, setAcquisitionRange] = useState(0);
var searchBarRef = useRef(null);
@@ -208,6 +221,19 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
setForcedUnitsData(initializeUnitsData);
setShowRadioSettings(false);
setShowAdvancedSettings(false);
if (selectedUnits.length > 0) {
setBarrelHeight(selectedUnits[0].getBarrelHeight());
setMuzzleVelocity(selectedUnits[0].getMuzzleVelocity());
setAimTime(selectedUnits[0].getAimTime());
setShotsToFire(selectedUnits[0].getShotsToFire());
setShotsBaseInterval(selectedUnits[0].getShotsBaseInterval());
setShotsBaseScatter(selectedUnits[0].getShotsBaseScatter());
setEngagementRange(selectedUnits[0].getEngagementRange());
setTargetingRange(selectedUnits[0].getTargetingRange());
setAimMethodRange(selectedUnits[0].getAimMethodRange());
setAcquisitionRange(selectedUnits[0].getAcquisitionRange());
}
}, [selectedUnits]);
/* Count how many units are selected of each type, divided by coalition */
@@ -230,7 +256,9 @@ 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.getBlueprint()?.label ?? "").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) => {
@@ -270,25 +298,47 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
open={props.open}
title={selectedUnits.length > 0 ? `Units selected (x${selectedUnits.length})` : `No units selected`}
onClose={props.onClose}
canBeHidden={true}
autohide={true}
wiki={() => {
return <div className={`
h-full flex-col overflow-auto p-4 text-gray-400 no-scrollbar flex
gap-2
`}>
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.
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 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>
{" "}
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>
);
}}
>
<>
@@ -368,9 +418,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
return (
<tr key={idx}>
<td className="flex gap-2 text-lg text-gray-200">
<FontAwesomeIcon icon={entry[1][0] as IconDefinition} /> <div className={`
<FontAwesomeIcon icon={entry[1][0] as IconDefinition} />{" "}
<div
className={`
text-sm text-gray-400
`}>{entry[1][1] as string}</div>
`}
>
{entry[1][1] as string}
</div>
</td>
{["blue", "neutral", "red"].map((coalition) => {
return (
@@ -453,23 +508,25 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
onClick={() => {
setSelectionID(unit.ID);
}}
>
<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>
<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>
);
})}
@@ -743,53 +800,81 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<div className="flex flex-col gap-2 px-2">
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsRoeHold} className={`
<FontAwesomeIcon
icon={olButtonsRoeHold}
className={`
my-auto min-w-8 text-white
`} /> Hold fire: The unit will not shoot in
any circumstance
`}
/>{" "}
Hold fire: The unit will not shoot in any circumstance
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsRoeReturn} className={`
<FontAwesomeIcon
icon={olButtonsRoeReturn}
className={`
my-auto min-w-8 text-white
`} /> Return fire: The unit will not fire
unless fired upon
`}
/>{" "}
Return fire: The unit will not fire unless fired upon
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsRoeDesignated} className={`
<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={`
Fire on target: The unit will not fire unless fired upon{" "}
<p
className={`
inline font-bold
`}>or</p> ordered to do so{" "}
`}
>
or
</p>{" "}
ordered to do so{" "}
</div>
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsRoeFree} className={`
<FontAwesomeIcon
icon={olButtonsRoeFree}
className={`
my-auto min-w-8 text-white
`} /> Free: The unit will fire at any
detected enemy in range
`}
/>{" "}
Free: The unit will fire at any detected enemy in range
</div>
</div>
<div className="flex gap-4">
<div className="my-auto">
<FaExclamationCircle className={`
<FaExclamationCircle
className={`
animate-bounce text-xl
`} />
`}
/>
</div>
<div>
Currently, DCS blue and red ground units do not respect{" "}
<FontAwesomeIcon icon={olButtonsRoeReturn} className={`
<FontAwesomeIcon
icon={olButtonsRoeReturn}
className={`
my-auto text-white
`} /> and{" "}
<FontAwesomeIcon icon={olButtonsRoeDesignated} className={`
`}
/>{" "}
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.
`}
/>{" "}
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>
@@ -920,31 +1005,43 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<div className="flex flex-col gap-2 px-2">
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsThreatNone} className={`
<FontAwesomeIcon
icon={olButtonsThreatNone}
className={`
my-auto min-w-8 text-white
`} /> No reaction: The unit will not
react in any circumstance
`}
/>{" "}
No reaction: The unit will not react in any circumstance
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsThreatPassive} className={`
<FontAwesomeIcon
icon={olButtonsThreatPassive}
className={`
my-auto min-w-8 text-white
`} /> Passive: The unit will use
counter-measures, but will not alter its course
`}
/>{" "}
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={`
<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
`}
/>{" "}
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={`
<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
`}
/>{" "}
Full evasion: the unit will try to evade the threat both manoeuvering and using counter-measures
</div>
</div>
</div>
@@ -995,31 +1092,43 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<div className="flex flex-col gap-2 px-2">
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsEmissionsSilent} className={`
<FontAwesomeIcon
icon={olButtonsEmissionsSilent}
className={`
my-auto min-w-8 text-white
`} /> Radio silence: No radar or
ECM will be used
`}
/>{" "}
Radio silence: No radar or ECM will be used
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsEmissionsDefend} className={`
<FontAwesomeIcon
icon={olButtonsEmissionsDefend}
className={`
my-auto min-w-8 text-white
`} /> Defensive: The unit will turn
radar and ECM on only when threatened
`}
/>{" "}
Defensive: The unit will turn radar and ECM on only when threatened
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsEmissionsAttack} className={`
<FontAwesomeIcon
icon={olButtonsEmissionsAttack}
className={`
my-auto min-w-8 text-white
`} /> Attack: The unit will use
radar and ECM when engaging other units
`}
/>{" "}
Attack: The unit will use radar and ECM when engaging other units
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsEmissionsFree} className={`
<FontAwesomeIcon
icon={olButtonsEmissionsFree}
className={`
my-auto min-w-8 text-white
`} /> Free: the unit will use the
radar and ECM all the time
`}
/>{" "}
Free: the unit will use the radar and ECM all the time
</div>
</div>
</div>
@@ -1237,9 +1346,11 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
>
<div className="flex gap-4">
<div className="my-auto">
<FaExclamationCircle className={`
<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
@@ -1264,15 +1375,27 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.scenicAAA}
onClick={() => {
getApp()
.getUnitsManager()
.scenicAAA(null, () =>
setForcedUnitsData({
...forcedUnitsData,
scenicAAA: !selectedUnitsData.scenicAAA,
missOnPurpose: false,
})
);
if (selectedUnitsData.scenicAAA) {
getApp()
.getUnitsManager()
.stop(null, () =>
setForcedUnitsData({
...forcedUnitsData,
missOnPurpose: false,
scenicAAA: false,
})
);
} else {
getApp()
.getUnitsManager()
.scenicAAA(null, () =>
setForcedUnitsData({
...forcedUnitsData,
missOnPurpose: false,
scenicAAA: true,
})
);
}
}}
tooltip={() => (
<OlExpandingTooltip
@@ -1297,15 +1420,27 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlToggle
toggled={selectedUnitsData.missOnPurpose}
onClick={() => {
getApp()
.getUnitsManager()
.missOnPurpose(null, () =>
setForcedUnitsData({
...forcedUnitsData,
scenicAAA: false,
missOnPurpose: !selectedUnitsData.missOnPurpose,
})
);
if (selectedUnitsData.missOnPurpose) {
getApp()
.getUnitsManager()
.stop(null, () =>
setForcedUnitsData({
...forcedUnitsData,
scenicAAA: false,
missOnPurpose: false,
})
);
} else {
getApp()
.getUnitsManager()
.missOnPurpose(null, () =>
setForcedUnitsData({
...forcedUnitsData,
scenicAAA: false,
missOnPurpose: true,
})
);
}
}}
tooltip={() => (
<OlExpandingTooltip
@@ -1384,11 +1519,19 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</OlButtonGroup>
</div>
{/* ============== Shots intensity END ============== */}
<OlStateButton
className="mt-auto"
checked={showEngagementSettings}
onClick={() => setShowEngagementSettings(!showEngagementSettings)}
icon={faCog}
></OlStateButton>
</div>
{/* ============== Operate as toggle START ============== */}
{selectedUnits.every((unit) => unit.getCoalition() === "neutral") && (
<div
className={`flex content-center justify-between`}
className={`
flex content-center justify-between
`}
>
<span
className={`
@@ -1422,6 +1565,371 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</div>
)}
{/* ============== Operate as toggle END ============== */}
{showEngagementSettings && (
<div
className={`
flex flex-col gap-2 text-sm text-gray-200
`}
>
<div className="flex align-center gap-2">
<div
className={`
my-auto
`}
>
Barrel height:{" "}
</div>
<OlNumberInput
decimalPlaces={1}
className={`
ml-auto
`}
value={barrelHeight}
min={0}
max={100}
onChange={(ev) => {
setBarrelHeight(Number(ev.target.value));
}}
onIncrease={() => {
setBarrelHeight(barrelHeight + 0.1);
}}
onDecrease={() => {
setBarrelHeight(barrelHeight - 0.1);
}}
></OlNumberInput>
<div
className={`
my-auto
`}
>
m
</div>
</div>
<div className="flex align-center gap-2">
<div
className={`
my-auto
`}
>
Muzzle velocity:{" "}
</div>
<OlNumberInput
decimalPlaces={0}
className={`
ml-auto
`}
value={muzzleVelocity}
min={0}
max={10000}
onChange={(ev) => {
setMuzzleVelocity(Number(ev.target.value));
}}
onIncrease={() => {
setMuzzleVelocity(muzzleVelocity + 10);
}}
onDecrease={() => {
setMuzzleVelocity(muzzleVelocity - 10);
}}
></OlNumberInput>
<div
className={`
my-auto
`}
>
m/s
</div>
</div>
<div className="flex align-center gap-2">
<div
className={`
my-auto
`}
>
Aim time:{" "}
</div>
<OlNumberInput
decimalPlaces={2}
className={`
ml-auto
`}
value={aimTime}
min={0}
max={100}
onChange={(ev) => {
setAimTime(Number(ev.target.value));
}}
onIncrease={() => {
setAimTime(aimTime + 0.1);
}}
onDecrease={() => {
setAimTime(aimTime - 0.1);
}}
></OlNumberInput>
<div
className={`
my-auto
`}
>
s
</div>
</div>
<div className="flex align-center gap-2">
<div
className={`
my-auto
`}
>
Shots to fire:{" "}
</div>
<OlNumberInput
className={`
ml-auto
`}
value={shotsToFire}
min={0}
max={100}
onChange={(ev) => {
setShotsToFire(Number(ev.target.value));
}}
onIncrease={() => {
setShotsToFire(shotsToFire + 1);
}}
onDecrease={() => {
setShotsToFire(shotsToFire - 1);
}}
></OlNumberInput>
</div>
<div className="flex align-center gap-2">
<div
className={`
my-auto
`}
>
Shots base interval:{" "}
</div>
<OlNumberInput
decimalPlaces={2}
className={`
ml-auto
`}
value={shotsBaseInterval}
min={0}
max={100}
onChange={(ev) => {
setShotsBaseInterval(Number(ev.target.value));
}}
onIncrease={() => {
setShotsBaseInterval(shotsBaseInterval + 0.1);
}}
onDecrease={() => {
setShotsBaseInterval(shotsBaseInterval - 0.1);
}}
></OlNumberInput>
<div
className={`
my-auto
`}
>
s
</div>
</div>
<div className="flex align-center gap-2">
<div
className={`
my-auto
`}
>
Shots base scatter:{" "}
</div>
<OlNumberInput
decimalPlaces={2}
className={`
ml-auto
`}
value={shotsBaseScatter}
min={0}
max={50}
onChange={(ev) => {
setShotsBaseScatter(Number(ev.target.value));
}}
onIncrease={() => {
setShotsBaseScatter(shotsBaseScatter + 0.1);
}}
onDecrease={() => {
setShotsBaseScatter(shotsBaseScatter - 0.1);
}}
></OlNumberInput>
<div
className={`
my-auto
`}
>
deg
</div>
</div>
<div className="flex align-center gap-2">
<div
className={`
my-auto
`}
>
Engagement range:{" "}
</div>
<OlNumberInput
className={`
ml-auto
`}
value={engagementRange}
min={0}
max={100000}
onChange={(ev) => {
setEngagementRange(Number(ev.target.value));
}}
onIncrease={() => {
setEngagementRange(engagementRange + 100);
}}
onDecrease={() => {
setEngagementRange(engagementRange - 100);
}}
></OlNumberInput>
<div
className={`
my-auto
`}
>
m
</div>
</div>
<div className="flex align-center gap-2">
<div
className={`
my-auto
`}
>
Targeting range:{" "}
</div>
<OlNumberInput
className={`
ml-auto
`}
value={targetingRange}
min={0}
max={100000}
onChange={(ev) => {
setTargetingRange(Number(ev.target.value));
}}
onIncrease={() => {
setTargetingRange(targetingRange + 100);
}}
onDecrease={() => {
setTargetingRange(targetingRange - 100);
}}
></OlNumberInput>
<div
className={`
my-auto
`}
>
m
</div>
</div>
<div className="flex align-center gap-2">
<div
className={`
my-auto
`}
>
Aim method range:{" "}
</div>
<OlNumberInput
className={`
ml-auto
`}
value={aimMethodRange}
min={0}
max={100000}
onChange={(ev) => {
setAimMethodRange(Number(ev.target.value));
}}
onIncrease={() => {
setAimMethodRange(aimMethodRange + 100);
}}
onDecrease={() => {
setAimMethodRange(aimMethodRange - 100);
}}
></OlNumberInput>
<div
className={`
my-auto
`}
>
m
</div>
</div>
<div className="flex align-center gap-2">
<div
className={`
my-auto
`}
>
Acquisition range:{" "}
</div>
<OlNumberInput
className={`
ml-auto
`}
value={acquisitionRange}
min={0}
max={100000}
onChange={(ev) => {
setAcquisitionRange(Number(ev.target.value));
}}
onIncrease={() => {
setAcquisitionRange(acquisitionRange + 100);
}}
onDecrease={() => {
setAcquisitionRange(acquisitionRange - 100);
}}
></OlNumberInput>
<div
className={`
my-auto
`}
>
m
</div>
</div>
<button
className={`
mb-2 me-2 rounded-sm bg-blue-700 px-5 py-2.5
text-md 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
`}
onClick={() => {
getApp()
.getUnitsManager()
.setEngagementProperties(
barrelHeight,
muzzleVelocity,
aimTime,
shotsToFire,
shotsBaseInterval,
shotsBaseScatter,
engagementRange,
targetingRange,
aimMethodRange,
acquisitionRange
);
}}
>
Save
</button>
</div>
)}
</>
)}
</div>
@@ -1903,14 +2411,18 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</div>
</div>
<div className="my-auto text-sm text-gray-400">
{selectedUnits[0].getTask()}
</div>
<div className="my-auto text-sm text-gray-400">{selectedUnits[0].getTask()}</div>
{([UnitState.SIMULATE_FIRE_FIGHT, UnitState.MISS_ON_PURPOSE, UnitState.SCENIC_AAA] as string[]).includes(selectedUnits[0].getState()) && (
<div className="my-auto text-sm text-gray-400">
Time to next tasking: {zeroAppend(selectedUnits[0].getTimeToNextTasking(), 0, true, 2)}s
</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>
<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>