feat: Multiple improvements to audio backend

This commit is contained in:
Davide Passoni
2025-03-27 13:16:39 +01:00
parent 9cbfa2a8aa
commit c9b143b5e0
23 changed files with 819 additions and 444 deletions

View File

@@ -9,8 +9,8 @@ import { AudioSink } from "../../audio/audiosink";
import { RadioSink } from "../../audio/radiosink";
import { UnitSinkPanel } from "./components/unitsinkpanel";
import { UnitSink } from "../../audio/unitsink";
import { FaMinus, FaVolumeHigh } from "react-icons/fa6";
import { getRandomColor } from "../../other/utils";
import { FaMinus, FaPerson, FaVolumeHigh } from "react-icons/fa6";
import { enumToCoalition, getRandomColor, zeroAppend } from "../../other/utils";
import {
AudioManagerCoalitionChangedEvent,
AudioManagerDevicesChangedEvent,
@@ -21,11 +21,13 @@ import {
AudioSourcesChangedEvent,
CommandModeOptionsChangedEvent,
ShortcutsChangedEvent,
SRSClientsChangedEvent,
} from "../../events";
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
import { Coalition } from "../../types/types";
import { GAME_MASTER, NONE } from "../../constants/constants";
import { Coalition, SRSClientData } from "../../types/types";
import { AudioManagerState, GAME_MASTER, NONE } from "../../constants/constants";
import { AudioManager } from "../../audio/audiomanager";
export function AudioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
const [devices, setDevices] = useState([] as MediaDeviceInfo[]);
@@ -39,6 +41,8 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
const [output, setOutput] = useState(undefined as undefined | MediaDeviceInfo);
const [coalition, setCoalition] = useState("blue" as Coalition);
const [commandMode, setCommandMode] = useState(NONE as string);
const [clientsData, setClientsData] = useState([] as SRSClientData[]);
const [connectedClientsOpen, setConnectedClientsOpen] = useState(false);
/* Preallocate 128 references for the source and sink panels. If the number of references changes, React will give an error */
const sourceRefs = Array(128)
@@ -76,7 +80,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
});
AudioManagerStateChangedEvent.on(() => {
setAudioManagerEnabled(getApp().getAudioManager().isRunning());
setAudioManagerEnabled(getApp().getAudioManager().isRunning() === AudioManagerState.RUNNING);
});
ShortcutsChangedEvent.on((shortcuts) => setShortcuts(shortcuts));
@@ -86,6 +90,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
AudioManagerOutputChangedEvent.on((output) => setOutput(output));
AudioManagerCoalitionChangedEvent.on((coalition) => setCoalition(coalition));
CommandModeOptionsChangedEvent.on((options) => setCommandMode(options.commandMode));
SRSClientsChangedEvent.on((clientsData) => setClientsData(clientsData));
}, []);
/* When the sinks or sources change, use the count state to force a rerender to update the connection lines */
@@ -131,62 +136,78 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
>
<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.
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.
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.
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.
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.
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 */}
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.
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.
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 "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.
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).
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.
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.
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.
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>
);
@@ -224,17 +245,78 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
</div>
)}
</>
<>
{audioManagerEnabled && (
<>
<div className={`mb-4 flex flex-col content-center bg-olympus-500`}>
<div
className={`
flex cursor-pointer content-center gap-2 px-5 py-2
text-gray-200
hover:underline hover:underline-offset-4
hover:underline-gray-700
`}
onClick={() => setConnectedClientsOpen(!connectedClientsOpen)}
>
Connected clients <FaPerson className="my-auto ml-auto" /> <div className={``}>{clientsData.length}</div>
<svg
data-open={connectedClientsOpen}
className={`
my-auto h-3 w-3 shrink-0 -rotate-180 transition-transform
dark:text-olympus-50
data-[open='false']:-rotate-90
`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 10 6"
>
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5 5 1 1 5" />
</svg>
</div>
<div className="flex flex-col gap-3">
<div
className={`
flex flex-col gap-2 px-5 font-normal text-gray-800
dark:text-white
`}
style={{ paddingRight: `${paddingRight}px` }}
>
{audioManagerEnabled && (
<>
{connectedClientsOpen && (
<div className={`flex flex-col text-gray-200`}>
{clientsData.map((clientData, idx) => {
return (
<div
data-coalition={enumToCoalition(clientData.coalition)}
key={idx}
className={`
flex gap-2 border-l-4 px-4 py-2
data-[coalition='blue']:border-blue-500
data-[coalition='neutral']:border-gray-500
data-[coalition='red']:border-red-500
`}
>
<div className="text-gray-400">{clientData.name}</div>
<div
className={`
ml-auto cursor-pointer gap-2 rounded-md
bg-olympus-600 px-3 py-1 text-sm
hover:bg-olympus-400
`}
onClick={() => getApp().getAudioManager().tuneNewRadio(clientData.radios[0].frequency, clientData.radios[0].modulation)}
>
{`${zeroAppend(clientData.radios[0].frequency / 1e6, 3, true, 3)} ${clientData.radios[0].modulation ? "FM" : "AM"}`}{" "}
</div>
<div
className={`
cursor-pointer gap-2 rounded-md bg-olympus-600 px-3
py-1 text-sm
hover:bg-olympus-400
`}
onClick={() => getApp().getAudioManager().tuneNewRadio(clientData.radios[1].frequency, clientData.radios[1].modulation)}
>
{`${zeroAppend(clientData.radios[1].frequency / 1e6, 3, true, 3)} ${clientData.radios[1].modulation ? "FM" : "AM"}`}{" "}
</div>
</div>
);
})}
</div>
)}
</div>
<div className="my-4 flex flex-col gap-2 px-5 text-gray-200">
{commandMode === GAME_MASTER && (
<div className="flex justify-between">
<div>Radio coalition </div>
@@ -254,23 +336,18 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
)}
<span>Input</span>
<OlDropdown label={input ? input.label : "Default"}>
{devices
.filter((device) => device.kind === "audioinput")
.map((device, idx) => {
return (
<OlDropdownItem onClick={() => getApp().getAudioManager().setInput(device)}>
<div className="w-full truncate">{device.label}</div>
<div className="w-full truncate text-left">{device.label}</div>
</OlDropdownItem>
);
})}
</OlDropdown>
</>
)}
{audioManagerEnabled && (
<>
{" "}
<span>Output</span>
<OlDropdown label={output ? output.label : "Default"}>
{devices
@@ -278,13 +355,24 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
.map((device, idx) => {
return (
<OlDropdownItem onClick={() => getApp().getAudioManager().setOutput(device)}>
<div className="w-full truncate">{device.label}</div>
<div className="w-full truncate text-left">{device.label}</div>
</OlDropdownItem>
);
})}
</OlDropdown>
</>
)}
</div>
</>
)}
</>
<div className="flex flex-col gap-3">
<div
className={`
flex flex-col gap-2 px-5 font-normal text-gray-800
dark:text-white
`}
style={{ paddingRight: `${paddingRight}px` }}
>
{audioManagerEnabled && <span>Audio sources</span>}
<>
{sources.map((source, idx) => {
@@ -328,7 +416,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
if (sink instanceof RadioSink)
return (
<RadioSinkPanel
shortcutKeys={shortcuts[`PTT${idx}Active`].toActions()}
shortcutKeys={shortcuts[`PTT${idx}Active`]?.toActions() ?? []}
key={sink.getName()}
radio={sink}
onExpanded={() => {
@@ -366,7 +454,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
if (sink instanceof UnitSink)
return (
<UnitSinkPanel
shortcutKeys={shortcuts[`PTT${idx}Active`].toActions()}
shortcutKeys={shortcuts[`PTT${idx}Active`]?.toActions() ?? []}
key={sink.getName()}
sink={sink}
ref={sinkRefs[idx]}

View File

@@ -1,12 +1,13 @@
import React, { ForwardedRef, forwardRef, useEffect, useState } from "react";
import { OlFrequencyInput } from "../../components/olfrequencyinput";
import { FaChevronUp, FaVolumeHigh, FaXmark } from "react-icons/fa6";
import { FaChevronDown, FaChevronUp, FaPerson, FaVolumeHigh, FaXmark } from "react-icons/fa6";
import { OlLabelToggle } from "../../components/ollabeltoggle";
import { OlStateButton } from "../../components/olstatebutton";
import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icons";
import { RadioSink } from "../../../audio/radiosink";
import { getApp } from "../../../olympusapp";
import { OlRangeSlider } from "../../components/olrangeslider";
import { zeroAppend } from "../../../other/utils";
export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKeys: string[]; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
const [expanded, setExpanded] = useState(false);
@@ -24,21 +25,23 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
data-[receiving='true']:border-white
`}
ref={ref}
>
<div className="flex cursor-pointer content-center justify-between gap-2" onClick={() => {
setExpanded(!expanded);
}}>
<div
className="flex cursor-pointer content-center justify-between gap-2"
onClick={() => {
setExpanded(!expanded);
}}
>
<div
className={`h-fit w-fit cursor-pointer rounded-sm py-2`}
onClick={() => {
setExpanded(!expanded);
}}
>
<FaChevronUp
<FaChevronDown
className={`
text-gray-500 transition-transform
data-[expanded='false']:rotate-180
data-[expanded='false']:-rotate-90
`}
data-expanded={expanded}
/>
@@ -56,14 +59,18 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
</kbd>
</>
)}
<span className="my-auto w-full">{!expanded && `${props.radio.getFrequency() / 1e6} MHz ${props.radio.getModulation() ? "FM" : "AM"}`}</span>
<div className="my-auto flex w-full">
{!expanded && `${zeroAppend(props.radio.getFrequency() / 1e6, 3, true, 3)} ${props.radio.getModulation() ? "FM" : "AM"}`}
<FaPerson className="my-auto ml-auto" /> {props.radio.getConnectedClients()}
</div>
<div
className={`
mb-auto ml-auto aspect-square cursor-pointer rounded-md p-2
hover:bg-white/10
`}
onClick={() => {
onClick={(ev) => {
getApp().getAudioManager().removeSink(props.radio);
ev.stopPropagation();
}}
>
<FaXmark className={`my-auto text-gray-500`}></FaXmark>
@@ -71,14 +78,18 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
</div>
{expanded && (
<>
<OlFrequencyInput
value={props.radio.getFrequency()}
onChange={(value) => {
props.radio.setFrequency(value);
}}
/>
<div className="mt-2 flex w-full justify-center">
<OlFrequencyInput
value={props.radio.getFrequency()}
onChange={(value) => {
props.radio.setFrequency(value);
}}
/>
</div>
<div className="flex content-center gap-2 p-2">
<div><FaVolumeHigh className="text-xl"/></div>
<div>
<FaVolumeHigh className="text-xl" />
</div>
<OlRangeSlider
value={props.radio.getVolume() * 100}
onChange={(ev) => {
@@ -89,14 +100,14 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
</div>
<div className="flex content-center gap-2 p-2">
<div>Left</div>
<OlRangeSlider
value={props.radio.getPan() * 50 + 50}
onChange={(ev) => {
props.radio.setPan((Number(ev.currentTarget.value) - 50) / 50);
}}
className="my-auto"
></OlRangeSlider>
<div>Right</div>
<OlRangeSlider
value={props.radio.getPan() * 50 + 50}
onChange={(ev) => {
props.radio.setPan((Number(ev.currentTarget.value) - 50) / 50);
}}
className="my-auto"
></OlRangeSlider>
<div>Right</div>
</div>
<div className="flex flex-row gap-2">
<OlLabelToggle

View File

@@ -3,7 +3,7 @@ import { OlStateButton } from "../../components/olstatebutton";
import { faHourglass, faPause, faPlay, faRepeat, faStop } from "@fortawesome/free-solid-svg-icons";
import { getApp } from "../../../olympusapp";
import { AudioSource } from "../../../audio/audiosource";
import { FaChevronUp, FaVolumeHigh, FaXmark } from "react-icons/fa6";
import { FaChevronDown, FaChevronUp, FaVolumeHigh, FaXmark } from "react-icons/fa6";
import { OlRangeSlider } from "../../components/olrangeslider";
import { FileSource } from "../../../audio/filesource";
import { MicrophoneSource } from "../../../audio/microphonesource";
@@ -40,10 +40,10 @@ export const AudioSourcePanel = forwardRef((props: { source: AudioSource; onExpa
setExpanded(!expanded);
}}
>
<FaChevronUp
<FaChevronDown
className={`
text-gray-500 transition-transform
data-[expanded='false']:rotate-180
data-[expanded='false']:-rotate-90
`}
data-expanded={expanded}
/>

View File

@@ -1,9 +1,9 @@
import React, { ForwardedRef, forwardRef, useEffect, useState } from "react";
import { FaChevronUp, FaXmark } from "react-icons/fa6";
import { FaChevronDown, FaChevronUp, FaVolumeHigh, FaXmark } from "react-icons/fa6";
import { getApp } from "../../../olympusapp";
import { UnitSink } from "../../../audio/unitsink";
import { OlStateButton } from "../../components/olstatebutton";
import { faMicrophoneLines } from "@fortawesome/free-solid-svg-icons";
import { faMicrophoneLines, faVolumeHigh } from "@fortawesome/free-solid-svg-icons";
import { OlRangeSlider } from "../../components/olrangeslider";
export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKeys: string[]; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
@@ -28,10 +28,10 @@ export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKeys:
setExpanded(!expanded);
}}
>
<FaChevronUp
<FaChevronDown
className={`
text-gray-500 transition-transform
data-[expanded='false']:rotate-180
data-[expanded='false']:-rotate-90
`}
data-expanded={expanded}
/>
@@ -66,7 +66,7 @@ export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKeys:
</div>
{expanded && (
<div className="flex flex-row gap-2">
<span className="my-auto">Near</span>
<FaVolumeHigh className="my-auto w-8 text-xl" />
<OlRangeSlider
value={((props.sink.getMaxDistance() - 100) / (1852 - 100)) * 100}
min={0}
@@ -76,14 +76,13 @@ export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKeys:
}}
className="my-auto h-16"
/>
<span className="my-auto">Far</span>
<OlStateButton
checked={props.sink.getPtt()}
icon={faMicrophoneLines}
icon={faVolumeHigh}
onClick={() => {
props.sink.setPtt(!props.sink.getPtt());
}}
tooltip="Talk on frequency"
tooltip="Click to enable the loudspeaker"
></OlStateButton>
</div>
)}

View File

@@ -91,14 +91,6 @@ export function ControlsPanel(props: {}) {
target: faFighterJet,
text: "Show unit actions",
});
//controls.push({
// actions: shortcuts["toggleRelativePositions"]?.toActions(),
// text: "Activate group movement",
//});
//controls.push({
// actions: [...shortcuts["toggleRelativePositions"]?.toActions(), "Wheel"],
// text: "Rotate formation",
//});
} else if (appState === OlympusState.SPAWN) {
controls = [
{

View File

@@ -17,6 +17,7 @@ import {
} from "../components/olicons";
import { FaChevronLeft, FaChevronRight, FaFloppyDisk } from "react-icons/fa6";
import {
AudioManagerStateChangedEvent,
CommandModeOptionsChangedEvent,
ConfigLoadedEvent,
EnabledCommandModesChangedEvent,
@@ -27,6 +28,7 @@ import {
SessionDataSavedEvent,
} from "../../events";
import {
AudioManagerState,
BLUE_COMMANDER,
COMMAND_MODE_OPTIONS_DEFAULTS,
GAME_MASTER,
@@ -39,6 +41,7 @@ import {
import { OlympusConfig } from "../../interfaces";
import { FaCheck, FaRedo, FaSpinner } from "react-icons/fa";
import { OlExpandingTooltip } from "../components/olexpandingtooltip";
import { stat } from "fs";
export function Header() {
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
@@ -47,7 +50,7 @@ export function Header() {
const [mapSources, setMapSources] = useState([] as string[]);
const [scrolledLeft, setScrolledLeft] = useState(true);
const [scrolledRight, setScrolledRight] = useState(false);
const [audioEnabled, setAudioEnabled] = useState(false);
const [audioState, setAudioState] = useState(AudioManagerState.STOPPED);
const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS);
const [savingSessionData, setSavingSessionData] = useState(false);
const [latestVersion, setLatestVersion] = useState("");
@@ -77,6 +80,7 @@ export function Header() {
SessionDataChangedEvent.on(() => setSavingSessionData(true));
SessionDataSavedEvent.on(() => setSavingSessionData(false));
EnabledCommandModesChangedEvent.on((enabledCommandModes) => setEnabledCommandModes(enabledCommandModes));
AudioManagerStateChangedEvent.on((state) => setAudioState(state as AudioManagerState));
/* Check if we are running the latest version */
const request = new Request("https://raw.githubusercontent.com/Pax1601/DCSOlympus/main/version.json");
@@ -246,9 +250,15 @@ export function Header() {
>
<span className="my-auto font-bold">Game Master</span>
{enabledCommandModes.length > 0 && (
<>{loadingNewCommandMode ? <FaSpinner className={`
my-auto ml-2 animate-spin text-white
`} /> : <FaRedo className={`my-auto ml-2 text-gray-200`} />}</>
<>
{loadingNewCommandMode ? (
<FaSpinner
className={`my-auto ml-2 animate-spin text-white`}
/>
) : (
<FaRedo className={`my-auto ml-2 text-gray-200`} />
)}
</>
)}
</div>
)}
@@ -271,9 +281,15 @@ export function Header() {
>
<span className="my-auto font-bold">BLUE Commander</span>
{enabledCommandModes.length > 0 && (
<>{loadingNewCommandMode ? <FaSpinner className={`
my-auto ml-2 animate-spin text-gray-200
`} /> : <FaRedo className={`my-auto ml-2 text-gray-200`} />}</>
<>
{loadingNewCommandMode ? (
<FaSpinner
className={`my-auto ml-2 animate-spin text-gray-200`}
/>
) : (
<FaRedo className={`my-auto ml-2 text-gray-200`} />
)}
</>
)}
</div>
)}
@@ -296,9 +312,15 @@ export function Header() {
>
<span className="my-auto font-bold">RED Commander</span>
{enabledCommandModes.length > 0 && (
<>{loadingNewCommandMode ? <FaSpinner className={`
my-auto ml-2 animate-spin text-gray-200
`} /> : <FaRedo className={`my-auto ml-2 text-gray-200`} />}</>
<>
{loadingNewCommandMode ? (
<FaSpinner
className={`my-auto ml-2 animate-spin text-gray-200`}
/>
) : (
<FaRedo className={`my-auto ml-2 text-gray-200`} />
)}
</>
)}
</div>
)}
@@ -325,11 +347,13 @@ export function Header() {
)}
/>
<OlRoundStateButton
checked={audioEnabled}
checked={audioState === AudioManagerState.RUNNING}
onClick={() => {
audioEnabled ? getApp().getAudioManager().stop() : getApp().getAudioManager().start();
setAudioEnabled(!audioEnabled);
audioState === AudioManagerState.RUNNING ? getApp().getAudioManager().stop() : getApp().getAudioManager().start();
}}
className={audioState === AudioManagerState.ERROR ? `
animate-pulse !border-red-500 !text-red-500
` : ""}
tooltip={() => (
<OlExpandingTooltip
title="Enable/disable audio"

View File

@@ -1,17 +1,12 @@
import React, { useEffect, useState } from "react";
import { AppStateChangedEvent, ContextActionChangedEvent, InfoPopupEvent } from "../../events";
import { OlympusState } from "../../constants/constants";
import { ContextAction } from "../../unit/contextaction";
import { InfoPopupEvent } from "../../events";
export function InfoBar(props: {}) {
const [messages, setMessages] = useState([] as string[]);
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [contextAction, setContextAction] = useState(null as ContextAction | null);
useEffect(() => {
InfoPopupEvent.on((messages) => setMessages([...messages]));
AppStateChangedEvent.on((state, subState) => setAppState(state));
ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction));
}, []);
return (

View File

@@ -18,7 +18,7 @@ import {
SelectionEnabledChangedEvent,
ShortcutsChangedEvent,
} from "../../events";
import { faCopy, faEraser, faObjectGroup, faPaste, faTape } from "@fortawesome/free-solid-svg-icons";
import { faCopy, faEraser, faMinus, faObjectGroup, faPaste, faPlus, faTape } from "@fortawesome/free-solid-svg-icons";
import { Shortcut } from "../../shortcut/shortcut";
import { ShortcutOptions, UnitData } from "../../interfaces";
import { Unit } from "../../unit/unit";
@@ -146,12 +146,36 @@ export function MapToolBar(props: {}) {
`}
/>
)}
<div className={`
pointer-events-auto flex flex-col gap-2 overflow-y-auto no-scrollbar
p-2
`} onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
<div
className={`
pointer-events-auto flex flex-col gap-2 overflow-y-auto
no-scrollbar p-2
`}
onScroll={(ev) => onScroll(ev.target)}
ref={scrollRef}
>
<>
<div className="flex flex-col gap-1">
<OlStateButton
key={"select"}
checked={false}
icon={faPlus}
tooltip={() => <div>Zoom map in</div>}
tooltipPosition="side"
onClick={() => {
getApp().getMap().zoomIn();
}}
/>
<OlStateButton
key={"select"}
checked={false}
icon={faMinus}
tooltip={() => <div>Zoom map out</div>}
tooltipPosition="side"
onClick={() => {
getApp().getMap().zoomOut();
}}
/>
<OlStateButton
key={"select"}
checked={selectionEnabled}
@@ -220,39 +244,39 @@ export function MapToolBar(props: {}) {
</div>
)}
<div className="flex flex-col gap-1">
<OlStateButton
key={"measure"}
checked={appState === OlympusState.MEASURE}
icon={faTape}
tooltip={() => (
<div className="flex content-center gap-2">
{shortcutCombination(shortcuts["measure"]?.getOptions())}
<div className="my-auto">Enter measure mode</div>
</div>
)}
tooltipPosition="side"
onClick={() => {
getApp().setState(appState === OlympusState.MEASURE? OlympusState.IDLE : OlympusState.MEASURE);
}}
/>
</div>
<div className="flex flex-col gap-1">
<OlStateButton
key={"clearMeasures"}
checked={false}
icon={faEraser}
tooltip={() => (
<div className="flex content-center gap-2">
{shortcutCombination(shortcuts["clearMeasures"]?.getOptions())}
<div className="my-auto">Clear all measures</div>
</div>
)}
tooltipPosition="side"
onClick={() => {
getApp().getMap().clearMeasures();
}}
/>
</div>
<OlStateButton
key={"measure"}
checked={appState === OlympusState.MEASURE}
icon={faTape}
tooltip={() => (
<div className="flex content-center gap-2">
{shortcutCombination(shortcuts["measure"]?.getOptions())}
<div className="my-auto">Enter measure mode</div>
</div>
)}
tooltipPosition="side"
onClick={() => {
getApp().setState(appState === OlympusState.MEASURE ? OlympusState.IDLE : OlympusState.MEASURE);
}}
/>
</div>
<div className="flex flex-col gap-1">
<OlStateButton
key={"clearMeasures"}
checked={false}
icon={faEraser}
tooltip={() => (
<div className="flex content-center gap-2">
{shortcutCombination(shortcuts["clearMeasures"]?.getOptions())}
<div className="my-auto">Clear all measures</div>
</div>
)}
tooltipPosition="side"
onClick={() => {
getApp().getMap().clearMeasures();
}}
/>
</div>
</>
{reorderedActions.map((contextActionIt: ContextAction) => {

View File

@@ -57,7 +57,7 @@ export function MiniMapPanel(props: {}) {
absolute right-[10px]
${mapOptions.showMinimap ? `bottom-[188px]` : `bottom-[20px]`}
flex w-[288px] cursor-pointer flex-col items-center justify-between
gap-2 text-sm backdrop-blur-lg
gap-2 text-sm
`}
>
@@ -65,7 +65,7 @@ export function MiniMapPanel(props: {}) {
<CoordinatesPanel />
<div className={`
flex h-12 w-full items-center justify-between gap-2 px-3
backdrop-grayscale
backdrop-blur-lg backdrop-grayscale
dark:bg-olympus-800/90 dark:text-gray-200
${mapOptions.showMinimap ? `rounded-t-lg` : `rounded-lg`}
`}

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
import { AudioSinksChangedEvent } from "../../events";
import { AudioSink } from "../../audio/audiosink";
import { RadioSink } from "../../audio/radiosink";
import { FaJetFighter, FaRadio, FaVolumeHigh } from "react-icons/fa6";
import { FaRadio, FaVolumeHigh } from "react-icons/fa6";
import { OlStateButton } from "../components/olstatebutton";
import { UnitSink } from "../../audio/unitsink";
import { colors } from "../../constants/constants";
@@ -18,12 +18,9 @@ export function RadiosSummaryPanel(props: {}) {
<>
{audioSinks.length > 0 && (
<div
className={`
flex w-full gap-2 rounded-lg text-sm text-gray-200
`}
className={`flex w-full gap-2 rounded-lg text-sm text-gray-200`}
>
<div className="flex w-full flex-wrap gap-2">
{audioSinks.filter((audioSinks) => audioSinks instanceof RadioSink).length > 0 &&
audioSinks
.filter((audioSinks) => audioSinks instanceof RadioSink)
@@ -43,9 +40,12 @@ export function RadiosSummaryPanel(props: {}) {
buttonColor={radioSink.getReceiving() ? colors.WHITE : undefined}
className="min-h-12 min-w-12"
>
<span className={`text-gray-200`}><FaRadio className={`
-translate-x-2 translate-y-1
`} /> <div className="translate-x-2 font-bold">{idx + 1}</div></span>
<span className={`text-gray-200`}>
<FaRadio
className={`-translate-x-2 translate-y-1`}
/>{" "}
<div className="translate-x-2 font-bold">{idx + 1}</div>
</span>
</OlStateButton>
);
})}
@@ -67,9 +67,12 @@ export function RadiosSummaryPanel(props: {}) {
tooltip="Click to talk"
className="min-h-12 min-w-12"
>
<span className={`text-gray-200`}><FaVolumeHigh className={`
-translate-x-2 translate-y-1
`} /> <div className="translate-x-2 font-bold">{idx + 1}</div></span>
<span className={`text-gray-200`}>
<FaVolumeHigh
className={`-translate-x-2 translate-y-1`}
/>{" "}
<div className="translate-x-2 font-bold">{idx + 1}</div>
</span>
</OlStateButton>
);
})}

View File

@@ -7,6 +7,7 @@ import { getApp } from "../../olympusapp";
import { OlButtonGroup, OlButtonGroupItem } from "../components/olbuttongroup";
import { OlCheckbox } from "../components/olcheckbox";
import {
AudioManagerState,
ROEs,
UnitState,
altitudeIncrements,
@@ -95,7 +96,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
}
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
const [audioManagerState, setAudioManagerState] = useState(false);
const [audioManagerRunning, setAudioManagerRunning] = useState(false);
const [selectedUnitsData, setSelectedUnitsData] = useState(initializeUnitsData);
const [forcedUnitsData, setForcedUnitsData] = useState(initializeUnitsData);
const [selectionFilter, setSelectionFilter] = useState({
@@ -152,7 +153,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
useEffect(() => {
SelectedUnitsChangedEvent.on((units) => setSelectedUnits(units));
SelectionClearedEvent.on(() => setSelectedUnits([]));
AudioManagerStateChangedEvent.on((state) => setAudioManagerState(state));
AudioManagerStateChangedEvent.on((state) => setAudioManagerRunning(state === AudioManagerState.RUNNING));
UnitsUpdatedEvent.on((units) => units.find((unit) => unit.getSelected()) && setLastUpdateTime(Date.now()));
}, []);
@@ -210,8 +211,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
if (newDatum === forcedUnitsData[key]) {
anyForcedDataUpdated = true;
forcedUnitsData[key] = undefined;
}
else updatedData[key] = forcedUnitsData[key];
} else updatedData[key] = forcedUnitsData[key];
} else updatedData[key] = newDatum;
});
setSelectedUnitsData(updatedData as typeof selectedUnitsData);
@@ -420,9 +420,12 @@ 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={`
text-sm text-gray-400
`}>{entry[1][1] as string}</div>
<FontAwesomeIcon icon={entry[1][0] as IconDefinition} />{" "}
<div
className={`text-sm text-gray-400`}
>
{entry[1][1] as string}
</div>
</td>
{["blue", "neutral", "red"].map((coalition) => {
return (
@@ -797,53 +800,65 @@ 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={`
my-auto min-w-8 text-white
`} /> Hold fire: The unit will not shoot in
any circumstance
<FontAwesomeIcon
icon={olButtonsRoeHold}
className={`my-auto min-w-8 text-white`}
/>{" "}
Hold fire: The unit will not shoot in any circumstance
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsRoeReturn} className={`
my-auto min-w-8 text-white
`} /> Return fire: The unit will not fire
unless fired upon
<FontAwesomeIcon
icon={olButtonsRoeReturn}
className={`my-auto min-w-8 text-white`}
/>{" "}
Return fire: The unit will not fire unless fired upon
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsRoeDesignated} className={`
my-auto min-w-8 text-white
`} />{" "}
<FontAwesomeIcon
icon={olButtonsRoeDesignated}
className={`my-auto min-w-8 text-white`}
/>{" "}
<div>
{" "}
Fire on target: The unit will not fire unless fired upon <p className={`
inline font-bold
`}>or</p> ordered to do so{" "}
Fire on target: The unit will not fire unless fired upon{" "}
<p
className={`inline font-bold`}
>
or
</p>{" "}
ordered to do so{" "}
</div>
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsRoeFree} className={`
my-auto min-w-8 text-white
`} /> Free: The unit will fire at any
detected enemy in range
<FontAwesomeIcon
icon={olButtonsRoeFree}
className={`my-auto min-w-8 text-white`}
/>{" "}
Free: The unit will fire at any detected enemy in range
</div>
</div>
<div className="flex gap-4">
<div className="my-auto">
<FaExclamationCircle className={`
animate-bounce text-xl
`} />
<FaExclamationCircle
className={`animate-bounce text-xl`}
/>
</div>
<div>
Currently, DCS blue and red ground units do not respect{" "}
<FontAwesomeIcon icon={olButtonsRoeReturn} className={`
my-auto text-white
`} /> and{" "}
<FontAwesomeIcon icon={olButtonsRoeDesignated} className={`
my-auto text-white
`} /> rules of engagement, so be careful, they
may start shooting when you don't want them to. Use neutral units for finer control.
<FontAwesomeIcon
icon={olButtonsRoeReturn}
className={`my-auto text-white`}
/>{" "}
and{" "}
<FontAwesomeIcon
icon={olButtonsRoeDesignated}
className={`my-auto text-white`}
/>{" "}
rules of engagement, so be careful, they may start shooting when you don't want them to. Use neutral units for finer
control.
</div>
</div>
</div>
@@ -897,35 +912,25 @@ 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={olButtonsAlarmstateGreen}
className={`
<FontAwesomeIcon icon={olButtonsAlarmstateGreen} className={`
my-auto min-w-8 text-white
`}
/>{" "}
Green: The unit will not engage with its sensors in any circumstances. The unit will be able to move.
`} /> Green: The unit will not engage
with its sensors in any circumstances. The unit will be able to move.
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon
icon={olButtonsAlarmstateAuto}
className={`
<FontAwesomeIcon icon={olButtonsAlarmstateAuto} className={`
my-auto min-w-8 text-white
`}
/>{" "}
`} />{" "}
<div> Auto: The unit will use its sensors to engage based on its ROE.</div>
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon
icon={olButtonsAlarmstateRed}
className={`
<FontAwesomeIcon icon={olButtonsAlarmstateRed} className={`
my-auto min-w-8 text-white
`}
/>{" "}
Red: The unit will be actively searching for target with its sensors. For some units, this will deploy the radar and make
the unit not able to move.
`} /> Red: The unit will be actively
searching for target with its sensors. For some units, this will deploy the radar and make the unit not able to move.
</div>
</div>
</div>
@@ -982,31 +987,35 @@ 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={`
my-auto min-w-8 text-white
`} /> No reaction: The unit will not
react in any circumstance
<FontAwesomeIcon
icon={olButtonsThreatNone}
className={`my-auto min-w-8 text-white`}
/>{" "}
No reaction: The unit will not react in any circumstance
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsThreatPassive} className={`
my-auto min-w-8 text-white
`} /> Passive: The unit will use
counter-measures, but will not alter its course
<FontAwesomeIcon
icon={olButtonsThreatPassive}
className={`my-auto min-w-8 text-white`}
/>{" "}
Passive: The unit will use counter-measures, but will not alter its course
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsThreatManoeuvre} className={`
my-auto min-w-8 text-white
`} /> Manouevre: The unit will try
to evade the threat using manoeuvres, but no counter-measures
<FontAwesomeIcon
icon={olButtonsThreatManoeuvre}
className={`my-auto min-w-8 text-white`}
/>{" "}
Manouevre: The unit will try to evade the threat using manoeuvres, but no counter-measures
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsThreatEvade} className={`
my-auto min-w-8 text-white
`} /> Full evasion: the unit will try
to evade the threat both manoeuvering and using counter-measures
<FontAwesomeIcon
icon={olButtonsThreatEvade}
className={`my-auto min-w-8 text-white`}
/>{" "}
Full evasion: the unit will try to evade the threat both manoeuvering and using counter-measures
</div>
</div>
</div>
@@ -1057,31 +1066,35 @@ 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={`
my-auto min-w-8 text-white
`} /> Radio silence: No radar or
ECM will be used
<FontAwesomeIcon
icon={olButtonsEmissionsSilent}
className={`my-auto min-w-8 text-white`}
/>{" "}
Radio silence: No radar or ECM will be used
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsEmissionsDefend} className={`
my-auto min-w-8 text-white
`} /> Defensive: The unit will turn
radar and ECM on only when threatened
<FontAwesomeIcon
icon={olButtonsEmissionsDefend}
className={`my-auto min-w-8 text-white`}
/>{" "}
Defensive: The unit will turn radar and ECM on only when threatened
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsEmissionsAttack} className={`
my-auto min-w-8 text-white
`} /> Attack: The unit will use
radar and ECM when engaging other units
<FontAwesomeIcon
icon={olButtonsEmissionsAttack}
className={`my-auto min-w-8 text-white`}
/>{" "}
Attack: The unit will use radar and ECM when engaging other units
</div>
<div className="flex content-center gap-2">
{" "}
<FontAwesomeIcon icon={olButtonsEmissionsFree} className={`
my-auto min-w-8 text-white
`} /> Free: the unit will use the
radar and ECM all the time
<FontAwesomeIcon
icon={olButtonsEmissionsFree}
className={`my-auto min-w-8 text-white`}
/>{" "}
Free: the unit will use the radar and ECM all the time
</div>
</div>
</div>
@@ -1299,9 +1312,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
>
<div className="flex gap-4">
<div className="my-auto">
<FaExclamationCircle className={`
animate-bounce text-xl
`} />
<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
@@ -1401,7 +1414,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
{/* ============== Miss on purpose toggle END ============== */}
<div className="flex gap-4">
{/* ============== Shots scatter START ============== */}
<div className={`flex w-full justify-between gap-2`}>
<div
className={`flex w-full justify-between gap-2`}
>
<span
className={`
my-auto font-normal
@@ -1476,9 +1491,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</div>
{/* ============== Operate as toggle START ============== */}
{selectedUnits.every((unit) => unit.getCoalition() === "neutral") && (
<div className={`
flex content-center justify-between
`}>
<div
className={`flex content-center justify-between`}
>
<span
className={`
my-auto font-normal
@@ -1814,67 +1829,68 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
</>
)}
{/* ============== Audio sink toggle START ============== */}
<div className="flex content-center justify-between">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Loudspeakers
</span>
{audioManagerState ? (
<OlToggle
toggled={selectedUnitsData.isAudioSink}
onClick={() => {
selectedUnits.forEach((unit) => {
if (!selectedUnitsData.isAudioSink) {
getApp()?.getAudioManager().addUnitSink(unit);
setForcedUnitsData({
...forcedUnitsData,
isAudioSink: true,
});
} else {
let sink = getApp()
?.getAudioManager()
.getSinks()
.find((sink) => {
return sink instanceof UnitSink && sink.getUnit() === unit;
{selectedUnits.length === 1 && (
<div className="flex content-center justify-between">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Loudspeakers
</span>
{audioManagerRunning ? (
<OlToggle
toggled={selectedUnitsData.isAudioSink}
onClick={() => {
selectedUnits.forEach((unit) => {
if (!selectedUnitsData.isAudioSink) {
getApp()?.getAudioManager().addUnitSink(unit);
setForcedUnitsData({
...forcedUnitsData,
isAudioSink: true,
});
if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink);
setForcedUnitsData({
...forcedUnitsData,
isAudioSink: false,
});
}
});
}}
tooltip={() => (
<OlExpandingTooltip
title="Make the unit emit sounds"
content="This option allows the unit to emit sounds as if it had loudspeakers. Turn this on to enable the option, then open the audio menu to connect a sound source to the unit. This is useful to simulate 5MC calls on the carrier, or attach sirens to unit. "
/>
)}
tooltipRelativeToParent={true}
tooltipPosition="above"
/>
) : (
<div className="text-white">
Enable audio with{" "}
<span
className={`
mx-1 mt-[-7px] inline-block translate-y-2
rounded-full border-[1px] border-white p-1
`}
>
<FaVolumeHigh />
</span>{" "}
first
</div>
)}
</div>
} else {
let sink = getApp()
?.getAudioManager()
.getSinks()
.find((sink) => {
return sink instanceof UnitSink && sink.getUnit() === unit;
});
if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink);
setForcedUnitsData({
...forcedUnitsData,
isAudioSink: false,
});
}
});
}}
tooltip={() => (
<OlExpandingTooltip
title="Make the unit emit sounds"
content="This option allows the unit to emit sounds as if it had loudspeakers. Turn this on to enable the option, then open the audio menu to connect a sound source to the unit. This is useful to simulate 5MC calls on the carrier, or attach sirens to unit. "
/>
)}
tooltipRelativeToParent={true}
tooltipPosition="above"
/>
) : (
<div className="text-white">
Enable audio with{" "}
<span
className={`
mx-1 mt-[-7px] inline-block translate-y-2
rounded-full border-[1px] border-white p-1
`}
>
<FaVolumeHigh />
</span>{" "}
first
</div>
)}
</div>
)}
{/* ============== Audio sink toggle END ============== */}
</div>
)}
@@ -1973,9 +1989,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
value={activeRadioSettings ? activeRadioSettings.TACAN.channel : 1}
></OlNumberInput>
<OlDropdown label={activeRadioSettings ? activeRadioSettings.TACAN.XY : "X"} className={`
my-auto w-20
`}>
<OlDropdown
label={activeRadioSettings ? activeRadioSettings.TACAN.XY : "X"}
className={`my-auto w-20`}
>
<OlDropdownItem
key={"X"}
onClick={() => {
@@ -2208,9 +2225,11 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
className={`
flex content-center gap-2 rounded-full
${selectedUnits[0].getFuel() > 40 && `bg-green-700`}
${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && `
bg-yellow-700
`}
${
selectedUnits[0].getFuel() > 10 &&
selectedUnits[0].getFuel() <= 40 &&
`bg-yellow-700`
}
${selectedUnits[0].getFuel() <= 10 && `bg-red-700`}
px-2 py-1 text-sm font-bold text-white
`}
@@ -2228,10 +2247,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
)}
<div className="flex content-center gap-2">
<OlLocation
location={selectedUnits[0].getPosition()}
className={`w-[280px] text-sm`}
/>
<OlLocation location={selectedUnits[0].getPosition()} className={`
w-[280px] text-sm
`} />
<div className="my-auto text-gray-200">{Math.round(mToFt(selectedUnits[0].getPosition().alt ?? 0))} ft</div>
</div>
</div>

View File

@@ -26,8 +26,6 @@ import { GameMasterMenu } from "./panels/gamemastermenu";
import { InfoBar } from "./panels/infobar";
import { HotGroupBar } from "./panels/hotgroupsbar";
import { SpawnContextMenu } from "./contextmenus/spawncontextmenu";
import { CoordinatesPanel } from "./panels/coordinatespanel";
import { RadiosSummaryPanel } from "./panels/radiossummarypanel";
import { ServerOverlay } from "./serveroverlay";
import { ImportExportModal } from "./modals/importexportmodal";
import { WarningModal } from "./modals/warningmodal";